diff --git a/internal/client/client.go b/internal/client/client.go index 6b81b9b83af..d0a75045664 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -7,6 +7,8 @@ package client import ( "encoding" "fmt" + + "github.com/AdguardTeam/AdGuardHome/internal/whois" ) // Source represents the source from which the information about the client has @@ -15,8 +17,7 @@ type Source uint8 // Clients information sources. The order determines the priority. const ( - SourceNone Source = iota - SourceWHOIS + SourceWHOIS Source = iota + 1 SourceARP SourceRDNS SourceDHCP @@ -52,3 +53,107 @@ var _ encoding.TextMarshaler = Source(0) func (cs Source) MarshalText() (text []byte, err error) { return []byte(cs.String()), nil } + +// Runtime is a client information from different sources. +type Runtime struct { + // whois is the filtered WHOIS information of a client. + whois *whois.Info + + // arp is the ARP information of a client. nil indicates that there is no + // information from the source. Empty non-nil slice indicates that the data + // from the source is present, but empty. + arp []string + + // rdns is the RDNS information of a client. nil indicates that there is no + // information from the source. Empty non-nil slice indicates that the data + // from the source is present, but empty. + rdns []string + + // dhcp is the DHCP information of a client. nil indicates that there is no + // information from the source. Empty non-nil slice indicates that the data + // from the source is present, but empty. + dhcp []string + + // hostsFile is the information from the hosts file. nil indicates that + // there is no information from the source. Empty non-nil slice indicates + // that the data from the source is present, but empty. + hostsFile []string +} + +// Info returns a client information from the highest-priority source. +func (r *Runtime) Info() (cs Source, host string) { + info := []string{} + + switch { + case r.hostsFile != nil: + cs, info = SourceHostsFile, r.hostsFile + case r.dhcp != nil: + cs, info = SourceDHCP, r.dhcp + case r.rdns != nil: + cs, info = SourceRDNS, r.rdns + case r.arp != nil: + cs, info = SourceARP, r.arp + case r.whois != nil: + cs = SourceWHOIS + } + + if len(info) == 0 { + return cs, "" + } + + // TODO(s.chzhen): Return the full information. + return cs, info[0] +} + +// SetInfo sets a host as a client information from the cs. +func (r *Runtime) SetInfo(cs Source, hosts []string) { + if len(hosts) == 1 && hosts[0] == "" { + hosts = []string{} + } + + switch cs { + case SourceARP: + r.arp = hosts + case SourceRDNS: + r.rdns = hosts + case SourceDHCP: + r.dhcp = hosts + case SourceHostsFile: + r.hostsFile = hosts + } +} + +// WHOIS returns a WHOIS client information. +func (r *Runtime) WHOIS() (info *whois.Info) { + return r.whois +} + +// SetWHOIS sets a WHOIS client information. info must be non-nil. +func (r *Runtime) SetWHOIS(info *whois.Info) { + r.whois = info +} + +// Unset clears a cs information. +func (r *Runtime) Unset(cs Source) { + switch cs { + case SourceWHOIS: + r.whois = nil + case SourceARP: + r.arp = nil + case SourceRDNS: + r.rdns = nil + case SourceDHCP: + r.dhcp = nil + case SourceHostsFile: + r.hostsFile = nil + } +} + +// IsEmpty returns true if there is no information from any source. +func (r *Runtime) IsEmpty() (ok bool) { + return r.whois == nil && + r.arp == nil && + r.rdns == nil && + r.dhcp == nil && + r.hostsFile == nil +} diff --git a/internal/home/client.go b/internal/home/client.go index 70ce112ec78..28edccf502b 100644 --- a/internal/home/client.go +++ b/internal/home/client.go @@ -4,10 +4,8 @@ import ( "fmt" "time" - "github.com/AdguardTeam/AdGuardHome/internal/client" "github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch" - "github.com/AdguardTeam/AdGuardHome/internal/whois" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/golibs/stringutil" ) @@ -85,17 +83,3 @@ func (c *Client) setSafeSearch( return nil } - -// RuntimeClient is a client information about which has been obtained using the -// source described in the Source field. -type RuntimeClient struct { - // WHOIS is the filtered WHOIS data of a client. - WHOIS *whois.Info - - // Host is the host name of a client. - Host string - - // Source is the source from which the information about the client has - // been obtained. - Source client.Source -} diff --git a/internal/home/clients.go b/internal/home/clients.go index 4d52f81b5a7..dc1b362d68a 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -51,8 +51,8 @@ type clientsContainer struct { list map[string]*Client // name -> client idIndex map[string]*Client // ID -> client - // ipToRC is the IP address to *RuntimeClient map. - ipToRC map[netip.Addr]*RuntimeClient + // ipToRC maps IP addresses to runtime client information. + ipToRC map[netip.Addr]*client.Runtime allTags *stringutil.Set @@ -103,9 +103,9 @@ func (clients *clientsContainer) Init( log.Fatal("clients.list != nil") } - clients.list = make(map[string]*Client) - clients.idIndex = make(map[string]*Client) - clients.ipToRC = map[netip.Addr]*RuntimeClient{} + clients.list = map[string]*Client{} + clients.idIndex = map[string]*Client{} + clients.ipToRC = map[netip.Addr]*client.Runtime{} clients.allTags = stringutil.NewSet(clientTags...) @@ -342,7 +342,7 @@ func (clients *clientsContainer) clientSource(ip netip.Addr) (src client.Source) rc, ok := clients.ipToRC[ip] if ok { - src = rc.Source + src, _ = rc.Info() } if src < client.SourceDHCP && clients.dhcp.HostByIP(ip) != "" { @@ -389,20 +389,22 @@ func (clients *clientsContainer) clientOrArtificial( } }() - client, ok := clients.Find(id) + cli, ok := clients.Find(id) if ok { return &querylog.Client{ - Name: client.Name, - IgnoreQueryLog: client.IgnoreQueryLog, + Name: cli.Name, + IgnoreQueryLog: cli.IgnoreQueryLog, }, false } - var rc *RuntimeClient + var rc *client.Runtime rc, ok = clients.findRuntimeClient(ip) if ok { + _, host := rc.Info() + return &querylog.Client{ - Name: rc.Host, - WHOIS: rc.WHOIS, + Name: host, + WHOIS: rc.WHOIS(), }, false } @@ -549,7 +551,7 @@ func (clients *clientsContainer) findDHCP(ip netip.Addr) (c *Client, ok bool) { // runtimeClient returns a runtime client from internal index. Note that it // doesn't include DHCP clients. -func (clients *clientsContainer) runtimeClient(ip netip.Addr) (rc *RuntimeClient, ok bool) { +func (clients *clientsContainer) runtimeClient(ip netip.Addr) (rc *client.Runtime, ok bool) { if ip == (netip.Addr{}) { return nil, false } @@ -563,21 +565,21 @@ func (clients *clientsContainer) runtimeClient(ip netip.Addr) (rc *RuntimeClient } // findRuntimeClient finds a runtime client by their IP. -func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *RuntimeClient, ok bool) { - if rc, ok = clients.runtimeClient(ip); ok && rc.Source > client.SourceDHCP { - return rc, ok - } - +func (clients *clientsContainer) findRuntimeClient(ip netip.Addr) (rc *client.Runtime, ok bool) { + rc, ok = clients.runtimeClient(ip) host := clients.dhcp.HostByIP(ip) - if host == "" { - return rc, ok + + if host != "" { + if !ok { + rc = &client.Runtime{} + } + + rc.SetInfo(client.SourceDHCP, []string{host}) + + return rc, true } - return &RuntimeClient{ - Host: host, - Source: client.SourceDHCP, - WHOIS: &whois.Info{}, - }, true + return rc, ok } // check validates the client. @@ -768,23 +770,20 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) { return } - // TODO(e.burkov): Consider storing WHOIS information separately and - // potentially get rid of [RuntimeClient]. rc, ok := clients.ipToRC[ip] if !ok { // Create a RuntimeClient implicitly so that we don't do this check // again. - rc = &RuntimeClient{ - Source: client.SourceWHOIS, - } + rc = &client.Runtime{} clients.ipToRC[ip] = rc log.Debug("clients: set whois info for runtime client with ip %s: %+v", ip, wi) } else { - log.Debug("clients: set whois info for runtime client %s: %+v", rc.Host, wi) + host, _ := rc.Info() + log.Debug("clients: set whois info for runtime client %s: %+v", host, wi) } - rc.WHOIS = wi + rc.SetWHOIS(wi) } // addHost adds a new IP-hostname pairing. The priorities of the sources are @@ -843,18 +842,13 @@ func (clients *clientsContainer) addHostLocked( } } - rc = &RuntimeClient{ - WHOIS: &whois.Info{}, - } + rc = &client.Runtime{} clients.ipToRC[ip] = rc - } else if src < rc.Source { - return false } - rc.Host = host - rc.Source = src + rc.SetInfo(src, []string{host}) - log.Debug("clients: added %s -> %q [%d]", ip, host, len(clients.ipToRC)) + log.Debug("clients: adding client info %s -> %q %q [%d]", ip, src, host, len(clients.ipToRC)) return true } @@ -863,7 +857,8 @@ func (clients *clientsContainer) addHostLocked( func (clients *clientsContainer) rmHostsBySrc(src client.Source) { n := 0 for ip, rc := range clients.ipToRC { - if rc.Source == src { + rc.Unset(src) + if rc.IsEmpty() { delete(clients.ipToRC, ip) n++ } diff --git a/internal/home/clients_internal_test.go b/internal/home/clients_internal_test.go index 30d735bb053..93a84b0b321 100644 --- a/internal/home/clients_internal_test.go +++ b/internal/home/clients_internal_test.go @@ -60,9 +60,8 @@ func TestClients(t *testing.T) { cli1 = "1.1.1.1" cli2 = "2.2.2.2" - cliNoneIP = netip.MustParseAddr(cliNone) - cli1IP = netip.MustParseAddr(cli1) - cli2IP = netip.MustParseAddr(cli2) + cli1IP = netip.MustParseAddr(cli1) + cli2IP = netip.MustParseAddr(cli2) ) c := &Client{ @@ -100,7 +99,9 @@ func TestClients(t *testing.T) { assert.Equal(t, "client2", c.Name) - assert.Equal(t, clients.clientSource(cliNoneIP), client.SourceNone) + _, ok = clients.Find(cliNone) + assert.False(t, ok) + assert.Equal(t, clients.clientSource(cli1IP), client.SourcePersistent) assert.Equal(t, clients.clientSource(cli2IP), client.SourcePersistent) }) @@ -136,7 +137,6 @@ func TestClients(t *testing.T) { cliOld = "1.1.1.1" cliNew = "1.1.1.2" - cliOldIP = netip.MustParseAddr(cliOld) cliNewIP = netip.MustParseAddr(cliNew) ) @@ -149,7 +149,9 @@ func TestClients(t *testing.T) { }) require.NoError(t, err) - assert.Equal(t, clients.clientSource(cliOldIP), client.SourceNone) + _, ok = clients.Find(cliOld) + assert.False(t, ok) + assert.Equal(t, clients.clientSource(cliNewIP), client.SourcePersistent) prev, ok = clients.list["client1"] @@ -182,7 +184,8 @@ func TestClients(t *testing.T) { ok := clients.Del("client1-renamed") require.True(t, ok) - assert.Equal(t, clients.clientSource(netip.MustParseAddr("1.1.1.2")), client.SourceNone) + _, ok = clients.Find("1.1.1.2") + assert.False(t, ok) }) t.Run("del_fail", func(t *testing.T) { @@ -215,10 +218,12 @@ func TestClients(t *testing.T) { assert.Equal(t, clients.clientSource(ip), client.SourceDHCP) }) - t.Run("addhost_fail", func(t *testing.T) { + t.Run("addhost_priority", func(t *testing.T) { ip := netip.MustParseAddr("1.1.1.1") ok := clients.addHost(ip, "host1", client.SourceRDNS) - assert.False(t, ok) + assert.True(t, ok) + + assert.Equal(t, client.SourceHostsFile, clients.clientSource(ip)) }) } @@ -235,7 +240,7 @@ func TestClientsWHOIS(t *testing.T) { rc := clients.ipToRC[ip] require.NotNil(t, rc) - assert.Equal(t, rc.WHOIS, whois) + assert.Equal(t, whois, rc.WHOIS()) }) t.Run("existing_auto-client", func(t *testing.T) { @@ -247,7 +252,7 @@ func TestClientsWHOIS(t *testing.T) { rc := clients.ipToRC[ip] require.NotNil(t, rc) - assert.Equal(t, rc.WHOIS, whois) + assert.Equal(t, whois, rc.WHOIS()) }) t.Run("can't_set_manually-added", func(t *testing.T) { diff --git a/internal/home/clientshttp.go b/internal/home/clientshttp.go index ad51e944fd5..20d6080f1fb 100644 --- a/internal/home/clientshttp.go +++ b/internal/home/clientshttp.go @@ -75,6 +75,17 @@ type clientListJSON struct { Tags []string `json:"supported_tags"` } +// whoisOrEmpty returns a WHOIS client information or a pointer to an empty +// struct. Frontend expects a non-nil value. +func whoisOrEmpty(r *client.Runtime) (info *whois.Info) { + info = r.WHOIS() + if info != nil { + return info + } + + return &whois.Info{} +} + // handleGetClients is the handler for GET /control/clients HTTP API. func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http.Request) { data := clientListJSON{} @@ -88,11 +99,11 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http } for ip, rc := range clients.ipToRC { + src, host := rc.Info() cj := runtimeClientJSON{ - WHOIS: rc.WHOIS, - - Name: rc.Host, - Source: rc.Source, + WHOIS: whoisOrEmpty(rc), + Name: host, + Source: src, IP: ip, } @@ -437,10 +448,11 @@ func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *c return cj } + _, host := rc.Info() cj = &clientJSON{ - Name: rc.Host, + Name: host, IDs: []string{idStr}, - WHOIS: rc.WHOIS, + WHOIS: whoisOrEmpty(rc), } disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)