Merge: Add WHOIS info for clients

* commit 'a52715e0863af0a9e1b26dbf96fc7cced02ae4f6':
  + client: add whois info to dashboard and logs
  + client: add whois info to clients and auto clients table
  * rDNS: refactor
  + whois: add WHOIS information for a client
This commit is contained in:
Andrey Meshkov
2019-09-23 20:07:05 +03:00
20 changed files with 515 additions and 134 deletions

View File

@@ -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()

View File

@@ -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))
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}

118
home/whois.go Normal file
View File

@@ -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)
}
}

21
home/whois_test.go Normal file
View File

@@ -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")
}