diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 0522439e..60fc986f 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -650,6 +650,10 @@ Response: safesearch_enabled: false use_global_blocked_services: true blocked_services: [ "name1", ... ] + whois_info: { + key: "value" + ... + } } ] auto_clients: [ @@ -657,10 +661,16 @@ Response: name: "host" ip: "..." source: "etc/hosts" || "rDNS" + whois_info: { + key: "value" + ... + } } ] } +Supported keys for `whois_info`: orgname, country, city. + ### Add client diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index d2d7bfe7..b800a9a8 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -392,5 +392,9 @@ "sign_in": "Sign in", "sign_out": "Sign out", "forgot_password": "Forgot password?", - "forgot_password_desc": "Please follow <0>these steps to create a new password for your user account." + "forgot_password_desc": "Please follow <0>these steps to create a new password for your user account.", + "city": "<0>City: {{value}}", + "country": "<0>Country: {{value}}", + "orgname": "<0>OrgName: {{value}}", + "whois": "Whois" } diff --git a/client/src/components/Dashboard/Clients.js b/client/src/components/Dashboard/Clients.js index d2e9d4a9..57308fd6 100644 --- a/client/src/components/Dashboard/Clients.js +++ b/client/src/components/Dashboard/Clients.js @@ -6,8 +6,9 @@ import { Trans, withNamespaces } from 'react-i18next'; import Card from '../ui/Card'; import Cell from '../ui/Cell'; -import { getPercent, getClientName } from '../../helpers/helpers'; +import { getPercent } from '../../helpers/helpers'; import { STATUS_COLORS } from '../../helpers/constants'; +import { formatClientCell } from '../../helpers/formatClientCell'; const getClientsPercentColor = (percent) => { if (percent > 50) { @@ -18,31 +19,6 @@ const getClientsPercentColor = (percent) => { return STATUS_COLORS.red; }; -const ipCell = (clients, autoClients) => - function cell(row) { - let client; - const { value } = row; - const clientName = getClientName(clients, value) || getClientName(autoClients, value); - - if (clientName) { - client = ( - - {clientName} ({value}) - - ); - } else { - client = value; - } - - return ( -
- - {client} - -
- ); - }; - const countCell = dnsQueries => function cell(row) { const { value } = row; @@ -52,6 +28,17 @@ const countCell = dnsQueries => return ; }; +const clientCell = (clients, autoClients) => + function cell(row) { + const { value } = row; + + return ( +
+ {formatClientCell(value, clients, autoClients)} +
+ ); + }; + const Clients = ({ t, refreshButton, topClients, subtitle, clients, autoClients, dnsQueries, }) => ( @@ -72,7 +59,7 @@ const Clients = ({ accessor: 'ip', sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10), - Cell: ipCell(clients, autoClients), + Cell: clientCell(clients, autoClients), }, { Header: requests_count, diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index a5c073af..86e03b2a 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -10,8 +10,9 @@ } .logs__row--column { - align-items: flex-start; flex-direction: column; + align-items: flex-start; + justify-content: center; } .logs__row--overflow { @@ -40,6 +41,11 @@ width: 100%; } +.logs__text--wrap { + line-height: 1.4; + white-space: normal; +} + .logs__row .tooltip-custom { top: 0; margin-left: 0; diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index 3f1493af..c2910b5e 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -6,9 +6,11 @@ import endsWith from 'lodash/endsWith'; import { Trans, withNamespaces } from 'react-i18next'; import { HashLink as Link } from 'react-router-hash-link'; -import { formatTime, formatDateTime, getClientName } from '../../helpers/helpers'; +import { formatTime, formatDateTime } from '../../helpers/helpers'; import { SERVICES, FILTERED_STATUS } from '../../helpers/constants'; import { getTrackerData } from '../../helpers/trackers/trackers'; +import { formatClientCell } from '../../helpers/formatClientCell'; + import PageTitle from '../ui/PageTitle'; import Card from '../ui/Card'; import Loading from '../ui/Loading'; @@ -190,24 +192,16 @@ class Logs extends Component { getClientCell = ({ original, value }) => { const { dashboard } = this.props; + const { clients, autoClients } = dashboard; const { reason, domain } = original; const isFiltered = this.checkFiltered(reason); const isRewrite = this.checkRewrite(reason); - const clientName = - getClientName(dashboard.clients, value) || getClientName(dashboard.autoClients, value); - let client = value; - - if (clientName) { - client = ( - - {clientName} ({value}) - - ); - } return ( -
{client}
+
+ {formatClientCell(value, clients, autoClients)} +
{isRewrite ? (
@@ -273,8 +267,8 @@ class Logs extends Component { { Header: t('client_table_header'), accessor: 'client', - maxWidth: 220, - minWidth: 220, + maxWidth: 240, + minWidth: 240, Cell: this.getClientCell, }, ]; diff --git a/client/src/components/Settings/Clients/AutoClients.js b/client/src/components/Settings/Clients/AutoClients.js index 98b11a52..1e8a9bdf 100644 --- a/client/src/components/Settings/Clients/AutoClients.js +++ b/client/src/components/Settings/Clients/AutoClients.js @@ -4,6 +4,8 @@ import { withNamespaces } from 'react-i18next'; import ReactTable from 'react-table'; import Card from '../../ui/Card'; +import WhoisCell from './WhoisCell'; +import WrapCell from './WrapCell'; class AutoClients extends Component { getStats = (ip, stats) => { @@ -15,29 +17,26 @@ class AutoClients extends Component { return ''; }; - cellWrap = ({ value }) => ( -
- - {value} - -
- ); - columns = [ { Header: this.props.t('table_client'), accessor: 'ip', - Cell: this.cellWrap, + Cell: WrapCell, }, { Header: this.props.t('table_name'), accessor: 'name', - Cell: this.cellWrap, + Cell: WrapCell, }, { Header: this.props.t('source_label'), accessor: 'source', - Cell: this.cellWrap, + Cell: WrapCell, + }, + { + Header: this.props.t('whois'), + accessor: 'whois_info', + Cell: WhoisCell, }, { Header: this.props.t('requests_count'), diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable.js index 27e4c738..53b4414e 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable.js @@ -6,6 +6,8 @@ import ReactTable from 'react-table'; import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants'; import Card from '../../ui/Card'; import Modal from './Modal'; +import WrapCell from './WrapCell'; +import WhoisCell from './WhoisCell'; class ClientsTable extends Component { handleFormAdd = (values) => { @@ -33,14 +35,6 @@ class ClientsTable extends Component { } }; - cellWrap = ({ value }) => ( -
- - {value} - -
- ); - getClient = (name, clients) => { const client = clients.find(item => name === item.name); @@ -82,6 +76,7 @@ class ClientsTable extends Component { { Header: this.props.t('table_client'), accessor: 'ip', + minWidth: 150, Cell: (row) => { if (row.original && row.original.mac) { return ( @@ -107,11 +102,13 @@ class ClientsTable extends Component { { Header: this.props.t('table_name'), accessor: 'name', - Cell: this.cellWrap, + minWidth: 120, + Cell: WrapCell, }, { Header: this.props.t('settings'), accessor: 'use_global_settings', + minWidth: 120, Cell: ({ value }) => { const title = value ? ( settings_global @@ -131,6 +128,7 @@ class ClientsTable extends Component { { Header: this.props.t('blocked_services'), accessor: 'blocked_services', + minWidth: 180, Cell: (row) => { const { value, original } = row; @@ -149,9 +147,16 @@ class ClientsTable extends Component { ); }, }, + { + Header: this.props.t('whois'), + accessor: 'whois_info', + minWidth: 200, + Cell: WhoisCell, + }, { Header: this.props.t('requests_count'), accessor: 'statistics', + minWidth: 120, Cell: (row) => { const clientIP = row.original.ip; const clientStats = clientIP && this.getStats(clientIP, this.props.topClients); @@ -172,7 +177,7 @@ class ClientsTable extends Component { { Header: this.props.t('actions_table_header'), accessor: 'actions', - maxWidth: 150, + maxWidth: 100, Cell: (row) => { const clientName = row.original.name; const { diff --git a/client/src/components/Settings/Clients/WhoisCell.js b/client/src/components/Settings/Clients/WhoisCell.js new file mode 100644 index 00000000..a41137fa --- /dev/null +++ b/client/src/components/Settings/Clients/WhoisCell.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Trans } from 'react-i18next'; + +const getFormattedWhois = (value) => { + const keys = Object.keys(value); + + if (keys.length > 0) { + return ( + keys.map(key => ( +
+ text]} + > + {key} + +
+ )) + ); + } + + return '–'; +}; + +const WhoisCell = ({ value }) => ( +
+ + {getFormattedWhois(value)} + +
+); + +WhoisCell.propTypes = { + value: PropTypes.object.isRequired, +}; + +export default WhoisCell; diff --git a/client/src/components/Settings/Clients/WrapCell.js b/client/src/components/Settings/Clients/WrapCell.js new file mode 100644 index 00000000..efc3b100 --- /dev/null +++ b/client/src/components/Settings/Clients/WrapCell.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const WrapCell = ({ value }) => ( +
+ + {value || '–'} + +
+); + +WrapCell.propTypes = { + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), +}; + +export default WrapCell; diff --git a/client/src/helpers/formatClientCell.js b/client/src/helpers/formatClientCell.js new file mode 100644 index 00000000..1facf7c9 --- /dev/null +++ b/client/src/helpers/formatClientCell.js @@ -0,0 +1,32 @@ +import React, { Fragment } from 'react'; +import { getClientInfo } from './helpers'; + +export const formatClientCell = (value, clients, autoClients) => { + const clientInfo = getClientInfo(clients, value) || getClientInfo(autoClients, value); + const { name, whois } = clientInfo; + + if (whois && name) { + return ( + +
+ {name} ({value}) +
+
+ {whois} +
+
+ ); + } else if (name) { + return ( + + {name} ({value}) + + ); + } + + return ( + + {value} + + ); +}; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 2c70d709..9b35c635 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -245,9 +245,33 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => { export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n); -export const getClientName = (clients, ip) => { +const formatWhois = (whois) => { + if (!whois) { + return ''; + } + + const keys = Object.keys(whois); + if (keys.length > 0) { + return ( + keys.map(key => whois[key]) + ); + } + + return ''; +}; + +export const getClientInfo = (clients, ip) => { const client = clients.find(item => ip === item.ip); - return (client && client.name) || ''; + + if (!client) { + return ''; + } + + const { name, whois_info } = client; + const formattedWhois = formatWhois(whois_info); + const whois = formattedWhois && formattedWhois.length > 0 && formattedWhois.join(' | '); + + return { name, whois }; }; export const sortClients = (clients) => { diff --git a/go.mod b/go.mod index f15b6e19..49f0f47f 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 + github.com/likexian/whois-go v0.0.0-20190627090909-384b3df3fc49 github.com/miekg/dns v1.1.8 github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum index c26688a1..bea03755 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,16 @@ github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b h1:vfiqKno48aUnd github.com/kardianos/service v0.0.0-20181115005516-4c239ee84e7b/go.mod h1:10UU/bEkzh2iEN6aYzbevY7J6p03KO5siTxQWXMEerg= github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32BM74ph7bPgqMztWopMYKgVyEho= github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o= +github.com/likexian/gokit v0.0.0-20190309162924-0a377eecf7aa/go.mod h1:QdfYv6y6qPA9pbBA2qXtoT8BMKha6UyNbxWGWl/9Jfk= +github.com/likexian/gokit v0.0.0-20190418170008-ace88ad0983b/go.mod h1:KKqSnk/VVSW8kEyO2vVCXoanzEutKdlBAPohmGXkxCk= +github.com/likexian/gokit v0.0.0-20190501133040-e77ea8b19cdc/go.mod h1:3kvONayqCaj+UgrRZGpgfXzHdMYCAO0KAt4/8n0L57Y= +github.com/likexian/gokit v0.0.0-20190604165112-68b8a4ba758c h1:KByA4IxKqqYwpqzk/P+w1DBpkPbvy3DArTP/U3LSxTQ= +github.com/likexian/gokit v0.0.0-20190604165112-68b8a4ba758c/go.mod h1:kn+nTv3tqh6yhor9BC4Lfiu58SmH8NmQ2PmEl+uM6nU= +github.com/likexian/simplejson-go v0.0.0-20190409170913-40473a74d76d/go.mod h1:Typ1BfnATYtZ/+/shXfFYLrovhFyuKvzwrdOnIDHlmg= +github.com/likexian/simplejson-go v0.0.0-20190419151922-c1f9f0b4f084/go.mod h1:U4O1vIJvIKwbMZKUJ62lppfdvkCdVd2nfMimHK81eec= +github.com/likexian/simplejson-go v0.0.0-20190502021454-d8787b4bfa0b/go.mod h1:3BWwtmKP9cXWwYCr5bkoVDEfLywacOv0s06OBEDpyt8= +github.com/likexian/whois-go v0.0.0-20190627090909-384b3df3fc49 h1:xGa+flE6p2UnMgxIS8bm7Q9JSt47HRuYVtwneDVnfLk= +github.com/likexian/whois-go v0.0.0-20190627090909-384b3df3fc49/go.mod h1:oR3bJMzrOb55cqTAn14DEzYFLDpSPTXJ3ORe7go9Hc8= github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc= github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI= diff --git a/home/clients.go b/home/clients.go index e80a7a00..7fcfeed3 100644 --- a/home/clients.go +++ b/home/clients.go @@ -31,6 +31,7 @@ type Client struct { SafeSearchEnabled bool SafeBrowsingEnabled bool ParentalEnabled bool + WhoisInfo [][]string // [[key,value], ...] UseOwnBlockedServices bool // false: use global settings BlockedServices []string @@ -46,29 +47,34 @@ type clientJSON struct { SafeSearchEnabled bool `json:"safebrowsing_enabled"` SafeBrowsingEnabled bool `json:"safesearch_enabled"` + WhoisInfo map[string]interface{} `json:"whois_info"` + UseGlobalBlockedServices bool `json:"use_global_blocked_services"` BlockedServices []string `json:"blocked_services"` } type clientSource uint +// Client sources const ( - // Priority: etc/hosts > DHCP > ARP > rDNS - ClientSourceRDNS clientSource = 0 // from rDNS - ClientSourceDHCP clientSource = 1 // from DHCP - ClientSourceARP clientSource = 2 // from 'arp -a' - ClientSourceHostsFile clientSource = 3 // from /etc/hosts + // Priority: etc/hosts > DHCP > ARP > rDNS > WHOIS + ClientSourceWHOIS clientSource = iota // from WHOIS + ClientSourceRDNS // from rDNS + ClientSourceDHCP // from DHCP + ClientSourceARP // from 'arp -a' + ClientSourceHostsFile // from /etc/hosts ) // ClientHost information type ClientHost struct { - Host string - Source clientSource + Host string + Source clientSource + WhoisInfo [][]string // [[key,value], ...] } type clientsContainer struct { - list map[string]*Client - ipIndex map[string]*Client + list map[string]*Client // name -> client + ipIndex map[string]*Client // IP -> client ipHost map[string]ClientHost // IP -> Hostname lock sync.Mutex } @@ -101,7 +107,7 @@ func (clients *clientsContainer) GetList() map[string]*Client { } // Exists checks if client with this IP already exists -func (clients *clientsContainer) Exists(ip string) bool { +func (clients *clientsContainer) Exists(ip string, source clientSource) bool { clients.lock.Lock() defer clients.lock.Unlock() @@ -110,8 +116,14 @@ func (clients *clientsContainer) Exists(ip string) bool { return true } - _, ok = clients.ipHost[ip] - return ok + ch, ok := clients.ipHost[ip] + if !ok { + return false + } + if source > ch.Source { + return false // we're going to overwrite this client's info with a stronger source + } + return true } // Find searches for a client by IP @@ -266,6 +278,31 @@ func (clients *clientsContainer) Update(name string, c Client) error { return nil } +// SetWhoisInfo - associate WHOIS information with a client +func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) { + clients.lock.Lock() + defer clients.lock.Unlock() + + c, ok := clients.ipIndex[ip] + if ok { + c.WhoisInfo = info + log.Debug("Clients: set WHOIS info for client %s: %v", c.Name, c.WhoisInfo) + } + + ch, ok := clients.ipHost[ip] + if ok { + ch.WhoisInfo = info + log.Debug("Clients: set WHOIS info for auto-client %s: %v", ch.Host, ch.WhoisInfo) + } + + ch = ClientHost{ + Source: ClientSourceWHOIS, + } + ch.WhoisInfo = info + clients.ipHost[ip] = ch + log.Debug("Clients: set WHOIS info for auto-client with IP %s: %v", ip, ch.WhoisInfo) +} + // AddHost adds new IP -> Host pair // Use priority of the source (etc/hosts > ARP > rDNS) // so we overwrite existing entries with an equal or higher priority @@ -280,8 +317,9 @@ func (clients *clientsContainer) AddHost(ip, host string, source clientSource) ( } clients.ipHost[ip] = ClientHost{ - Host: host, - Source: source, + Host: host, + Source: source, + WhoisInfo: c.WhoisInfo, } log.Tracef("'%s' -> '%s' [%d]", ip, host, len(clients.ipHost)) return true, nil @@ -386,6 +424,8 @@ type clientHostJSON struct { IP string `json:"ip"` Name string `json:"name"` Source string `json:"source"` + + WhoisInfo map[string]interface{} `json:"whois_info"` } type clientListJSON struct { @@ -421,6 +461,11 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) { } } + cj.WhoisInfo = make(map[string]interface{}) + for _, wi := range c.WhoisInfo { + cj.WhoisInfo[wi[0]] = wi[1] + } + data.Clients = append(data.Clients, cj) } for ip, ch := range config.clients.ipHost { @@ -428,6 +473,7 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) { IP: ip, Name: ch.Host, } + cj.Source = "etc/hosts" switch ch.Source { case ClientSourceDHCP: @@ -436,7 +482,15 @@ func handleGetClients(w http.ResponseWriter, r *http.Request) { cj.Source = "rDNS" case ClientSourceARP: cj.Source = "ARP" + case ClientSourceWHOIS: + cj.Source = "WHOIS" } + + cj.WhoisInfo = make(map[string]interface{}) + for _, wi := range ch.WhoisInfo { + cj.WhoisInfo[wi[0]] = wi[1] + } + data.AutoClients = append(data.AutoClients, cj) } config.clients.lock.Unlock() diff --git a/home/clients_test.go b/home/clients_test.go index d5dc5143..c4d1837e 100644 --- a/home/clients_test.go +++ b/home/clients_test.go @@ -1,6 +1,10 @@ package home -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestClients(t *testing.T) { var c Client @@ -61,15 +65,9 @@ func TestClients(t *testing.T) { } // get - if clients.Exists("1.2.3.4") { - t.Fatalf("Exists") - } - if !clients.Exists("1.1.1.1") { - t.Fatalf("Exists #1") - } - if !clients.Exists("2.2.2.2") { - t.Fatalf("Exists #2") - } + assert.True(t, !clients.Exists("1.2.3.4", ClientSourceHostsFile)) + assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) + assert.True(t, clients.Exists("2.2.2.2", ClientSourceHostsFile)) // failed update - no such name c.IP = "1.2.3.0" @@ -100,9 +98,7 @@ func TestClients(t *testing.T) { } // get after update - if clients.Exists("1.1.1.1") || !clients.Exists("1.1.1.2") { - t.Fatalf("Exists - get after update") - } + assert.True(t, !(clients.Exists("1.1.1.1", ClientSourceHostsFile) || !clients.Exists("1.1.1.2", ClientSourceHostsFile))) // failed remove - no such name if clients.Del("client3") { @@ -110,9 +106,7 @@ func TestClients(t *testing.T) { } // remove - if !clients.Del("client1") || clients.Exists("1.1.1.2") { - t.Fatalf("Del") - } + assert.True(t, !(!clients.Del("client1") || clients.Exists("1.1.1.2", ClientSourceHostsFile))) // add host client b, e = clients.AddHost("1.1.1.1", "host", ClientSourceARP) @@ -139,7 +133,5 @@ func TestClients(t *testing.T) { } // get - if !clients.Exists("1.1.1.1") { - t.Fatalf("clientAddHost") - } + assert.True(t, clients.Exists("1.1.1.1", ClientSourceHostsFile)) } diff --git a/home/dns.go b/home/dns.go index bf5a2daa..e51cd9a2 100644 --- a/home/dns.go +++ b/home/dns.go @@ -5,26 +5,20 @@ import ( "net" "os" "path/filepath" - "sync" "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsforward" "github.com/AdguardTeam/AdGuardHome/querylog" "github.com/AdguardTeam/AdGuardHome/stats" "github.com/AdguardTeam/dnsproxy/proxy" - "github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/golibs/log" "github.com/joomcode/errorx" "github.com/miekg/dns" ) type dnsContext struct { - rdnsChannel chan string // pass data from DNS request handling thread to rDNS thread - // contains IP addresses of clients to be resolved by rDNS - // if IP address couldn't be resolved, it stays here forever to prevent further attempts to resolve the same IP - rdnsIP map[string]bool - rdnsLock sync.Mutex // synchronize access to rdnsIP - upstream upstream.Upstream // Upstream object for our own DNS server + rdns *RDNS + whois *Whois } // initDNSServer creates an instance of the dnsforward.Server @@ -55,7 +49,8 @@ func initDNSServer(baseDir string) { config.auth = InitAuth(sessFilename, config.Users) config.Users = nil - initRDNS() + config.dnsctx.rdns = InitRDNS(&config.clients) + config.dnsctx.whois = initWhois(&config.clients) initFiltering() } @@ -63,6 +58,59 @@ func isRunning() bool { return config.dnsServer != nil && config.dnsServer.IsRunning() } +// Return TRUE if IP is within public Internet IP range +func isPublicIP(ip net.IP) bool { + ip4 := ip.To4() + if ip4 != nil { + switch ip4[0] { + case 0: + return false //software + case 10: + return false //private network + case 127: + return false //loopback + case 169: + if ip4[1] == 254 { + return false //link-local + } + case 172: + if ip4[1] >= 16 && ip4[1] <= 31 { + return false //private network + } + case 192: + if (ip4[1] == 0 && ip4[2] == 0) || //private network + (ip4[1] == 0 && ip4[2] == 2) || //documentation + (ip4[1] == 88 && ip4[2] == 99) || //reserved + (ip4[1] == 168) { //private network + return false + } + case 198: + if (ip4[1] == 18 || ip4[2] == 19) || //private network + (ip4[1] == 51 || ip4[2] == 100) { //documentation + return false + } + case 203: + if ip4[1] == 0 && ip4[2] == 113 { //documentation + return false + } + case 224: + if ip4[1] == 0 && ip4[2] == 0 { //multicast + return false + } + case 255: + if ip4[1] == 255 && ip4[2] == 255 && ip4[3] == 255 { //subnet + return false + } + } + } else { + if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() { + return false + } + } + + return true +} + func onDNSRequest(d *proxy.DNSContext) { qType := d.Req.Question[0].Qtype if qType != dns.TypeA && qType != dns.TypeAAAA { @@ -77,7 +125,10 @@ func onDNSRequest(d *proxy.DNSContext) { ipAddr := net.ParseIP(ip) if !ipAddr.IsLoopback() { - beginAsyncRDNS(ip) + config.dnsctx.rdns.Begin(ip) + } + if isPublicIP(ipAddr) { + config.dnsctx.whois.Begin(ip) } } diff --git a/home/dns_test.go b/home/dns_test.go index dd6ba8cb..67d7b3ed 100644 --- a/home/dns_test.go +++ b/home/dns_test.go @@ -7,7 +7,7 @@ import ( func TestResolveRDNS(t *testing.T) { config.DNS.BindHost = "1.1.1.1" initDNSServer(".") - if r := resolveRDNS("1.1.1.1"); r != "one.one.one.one" { + if r := config.dnsctx.rdns.resolve("1.1.1.1"); r != "one.one.one.one" { t.Errorf("resolveRDNS(): %s", r) } } diff --git a/home/rdns.go b/home/rdns.go index 9ea11a26..c8a39974 100644 --- a/home/rdns.go +++ b/home/rdns.go @@ -3,6 +3,7 @@ package home import ( "fmt" "strings" + "sync" "time" "github.com/AdguardTeam/dnsproxy/upstream" @@ -14,7 +15,21 @@ const ( rdnsTimeout = 3 * time.Second // max time to wait for rDNS response ) -func initRDNS() { +// RDNS - module context +type RDNS struct { + clients *clientsContainer + ipChannel chan string // pass data from DNS request handling thread to rDNS thread + // contains IP addresses of clients to be resolved by rDNS + // if IP address couldn't be resolved, it stays here forever to prevent further attempts to resolve the same IP + ips map[string]bool + lock sync.Mutex // synchronize access to 'ips' + upstream upstream.Upstream // Upstream object for our own DNS server +} + +// InitRDNS - create module context +func InitRDNS(clients *clientsContainer) *RDNS { + r := RDNS{} + r.clients = clients var err error bindhost := config.DNS.BindHost @@ -26,35 +41,36 @@ func initRDNS() { opts := upstream.Options{ Timeout: rdnsTimeout, } - config.dnsctx.upstream, err = upstream.AddressToUpstream(resolverAddress, opts) + r.upstream, err = upstream.AddressToUpstream(resolverAddress, opts) if err != nil { log.Error("upstream.AddressToUpstream: %s", err) - return + return nil } - config.dnsctx.rdnsIP = make(map[string]bool) - config.dnsctx.rdnsChannel = make(chan string, 256) - go asyncRDNSLoop() + r.ips = make(map[string]bool) + r.ipChannel = make(chan string, 256) + go r.workerLoop() + return &r } -// Add IP address to the rDNS queue -func beginAsyncRDNS(ip string) { - if config.clients.Exists(ip) { +// Begin - add IP address to rDNS queue +func (r *RDNS) Begin(ip string) { + if r.clients.Exists(ip, ClientSourceRDNS) { return } - // add IP to rdnsIP, if not exists - config.dnsctx.rdnsLock.Lock() - defer config.dnsctx.rdnsLock.Unlock() - _, ok := config.dnsctx.rdnsIP[ip] + // add IP to ips, if not exists + r.lock.Lock() + defer r.lock.Unlock() + _, ok := r.ips[ip] if ok { return } - config.dnsctx.rdnsIP[ip] = true + r.ips[ip] = true log.Tracef("Adding %s for rDNS resolve", ip) select { - case config.dnsctx.rdnsChannel <- ip: + case r.ipChannel <- ip: // default: log.Tracef("rDNS queue is full") @@ -62,7 +78,7 @@ func beginAsyncRDNS(ip string) { } // Use rDNS to get hostname by IP address -func resolveRDNS(ip string) string { +func (r *RDNS) resolve(ip string) string { log.Tracef("Resolving host for %s", ip) req := dns.Msg{} @@ -81,7 +97,7 @@ func resolveRDNS(ip string) string { return "" } - resp, err := config.dnsctx.upstream.Exchange(&req) + resp, err := r.upstream.Exchange(&req) if err != nil { log.Debug("Error while making an rDNS lookup for %s: %s", ip, err) return "" @@ -106,19 +122,19 @@ func resolveRDNS(ip string) string { // Wait for a signal and then synchronously resolve hostname by IP address // Add the hostname:IP pair to "Clients" array -func asyncRDNSLoop() { +func (r *RDNS) workerLoop() { for { var ip string - ip = <-config.dnsctx.rdnsChannel + ip = <-r.ipChannel - host := resolveRDNS(ip) + host := r.resolve(ip) if len(host) == 0 { continue } - config.dnsctx.rdnsLock.Lock() - delete(config.dnsctx.rdnsIP, ip) - config.dnsctx.rdnsLock.Unlock() + r.lock.Lock() + delete(r.ips, ip) + r.lock.Unlock() _, _ = config.clients.AddHost(ip, host, ClientSourceRDNS) } diff --git a/home/whois.go b/home/whois.go new file mode 100644 index 00000000..f5dd2ab2 --- /dev/null +++ b/home/whois.go @@ -0,0 +1,118 @@ +package home + +import ( + "strings" + "sync" + + "github.com/AdguardTeam/golibs/log" + whois "github.com/likexian/whois-go" +) + +// Whois - module context +type Whois struct { + clients *clientsContainer + ips map[string]bool + lock sync.Mutex + ipChan chan string +} + +// Create module context +func initWhois(clients *clientsContainer) *Whois { + w := Whois{} + w.clients = clients + w.ips = make(map[string]bool) + w.ipChan = make(chan string, 255) + go w.workerLoop() + return &w +} + +// Parse plain-text data from the response +func whoisParse(data string) map[string]string { + m := map[string]string{} + lines := strings.Split(data, "\n") + for _, ln := range lines { + ln = strings.TrimSpace(ln) + + if len(ln) == 0 || ln[0] == '#' { + continue + } + + kv := strings.SplitN(ln, ":", 2) + if len(kv) != 2 { + continue + } + k := strings.TrimSpace(kv[0]) + k = strings.ToLower(k) + v := strings.TrimSpace(kv[1]) + + if k == "orgname" || k == "org-name" { + m["orgname"] = v + } else if k == "city" { + m["city"] = v + } else if k == "country" { + m["country"] = v + } + } + return m +} + +// Request WHOIS information +func whoisProcess(ip string) [][]string { + data := [][]string{} + resp, err := whois.Whois(ip) + if err != nil { + log.Debug("Whois: error: %s IP:%s", err, ip) + return data + } + + log.Debug("Whois: IP:%s response: %d bytes", ip, len(resp)) + + m := whoisParse(resp) + + keys := []string{"orgname", "country", "city"} + for _, k := range keys { + v, found := m[k] + if !found { + continue + } + pair := []string{k, v} + data = append(data, pair) + } + + return data +} + +// Begin - begin requesting WHOIS info +func (w *Whois) Begin(ip string) { + w.lock.Lock() + _, found := w.ips[ip] + if found { + w.lock.Unlock() + return + } + w.ips[ip] = true + w.lock.Unlock() + + log.Debug("Whois: adding %s", ip) + select { + case w.ipChan <- ip: + // + default: + log.Debug("Whois: queue is full") + } +} + +// Get IP address from channel; get WHOIS info; associate info with a client +func (w *Whois) workerLoop() { + for { + var ip string + ip = <-w.ipChan + + info := whoisProcess(ip) + if len(info) == 0 { + continue + } + + w.clients.SetWhoisInfo(ip, info) + } +} diff --git a/home/whois_test.go b/home/whois_test.go new file mode 100644 index 00000000..7a841110 --- /dev/null +++ b/home/whois_test.go @@ -0,0 +1,21 @@ +package home + +import ( + "strings" + "testing" + + whois "github.com/likexian/whois-go" + "github.com/stretchr/testify/assert" +) + +func TestWhois(t *testing.T) { + resp, err := whois.Whois("8.8.8.8") + assert.True(t, err == nil) + assert.True(t, strings.Index(resp, "OrgName: Google LLC") != -1) + assert.True(t, strings.Index(resp, "City: Mountain View") != -1) + assert.True(t, strings.Index(resp, "Country: US") != -1) + m := whoisParse(resp) + assert.True(t, m["orgname"] == "Google LLC") + assert.True(t, m["country"] == "US") + assert.True(t, m["city"] == "Mountain View") +}