+ {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")
+}