diff --git a/CHANGELOG.md b/CHANGELOG.md index 4760e0c0549..5a440bbfe55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ NOTE: Add new changes BELOW THIS COMMENT. - Acceptance of duplicate UIDs for persistent clients at startup. See also the section on client settings on the [Wiki page][wiki-config]. +- Support for link-local subnets, i.e. `fe80::/16`, as client identifiers + ([#6312]). - Issues with QUIC and HTTP/3 upstreams on older Linux kernel versions ([#6422]). - YouTube restricted mode is not enforced by HTTPS queries on Firefox. @@ -59,6 +61,7 @@ NOTE: Add new changes BELOW THIS COMMENT. [#5345]: https://github.com/AdguardTeam/AdGuardHome/issues/5345 [#5812]: https://github.com/AdguardTeam/AdGuardHome/issues/5812 [#6192]: https://github.com/AdguardTeam/AdGuardHome/issues/6192 +[#6312]: https://github.com/AdguardTeam/AdGuardHome/issues/6312 [#6422]: https://github.com/AdguardTeam/AdGuardHome/issues/6422 [#6854]: https://github.com/AdguardTeam/AdGuardHome/issues/6854 [#6875]: https://github.com/AdguardTeam/AdGuardHome/issues/6875 diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index c5b94bf0d60..1ffe7bf1353 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -13,14 +13,14 @@ "fallback_dns_desc": "List of fallback DNS servers used when upstream DNS servers are not responding. The syntax is the same as in the main upstreams field above.", "fallback_dns_placeholder": "Enter one fallback DNS server per line", "local_ptr_title": "Private reverse DNS servers", - "local_ptr_desc": "The DNS servers that AdGuard Home uses for private PTR, SOA, and NS queries. The request is considered private if it asks for ARPA domain containing a subnet within private IP ranges, for example \"192.168.12.34\", and came from a client with private address. If not set, AdGuard Home uses the addresses of the default DNS resolvers of your OS except for the addresses of AdGuard Home itself.", + "local_ptr_desc": "DNS servers used by AdGuard Home for private PTR, SOA, and NS requests. A request is considered private if it asks for an ARPA domain containing a subnet within private IP ranges (such as \"192.168.12.34\") and comes from a client with a private IP address. If not set, the default DNS resolvers of your OS will be used, except for the AdGuard Home IP addresses.", "local_ptr_default_resolver": "By default, AdGuard Home uses the following reverse DNS resolvers: {{ip}}.", "local_ptr_no_default_resolver": "AdGuard Home could not determine suitable private reverse DNS resolvers for this system.", "local_ptr_placeholder": "Enter one IP address per line", "resolve_clients_title": "Enable reverse resolving of clients' IP addresses", "resolve_clients_desc": "Reversely resolve clients' IP addresses into their hostnames by sending PTR queries to corresponding resolvers (private DNS servers for local clients, upstream servers for clients with public IP addresses).", "use_private_ptr_resolvers_title": "Use private reverse DNS resolvers", - "use_private_ptr_resolvers_desc": "Resolve PTR, SOA, and NS requests for ARPA domains containing private addresses using private upstream servers, DHCP, /etc/hosts, and so on. If disabled, AdGuard Home responds with NXDOMAIN to all such queries.", + "use_private_ptr_resolvers_desc": "Resolve PTR, SOA, and NS requests for ARPA domains containing private IP addresses through private upstream servers, DHCP, /etc/hosts, etc. If disabled, AdGuard Home will respond to all such requests with NXDOMAIN.", "check_dhcp_servers": "Check for DHCP servers", "save_config": "Save configuration", "enabled_dhcp": "DHCP server enabled", diff --git a/internal/client/index.go b/internal/client/index.go index 6213190677a..18f826deb48 100644 --- a/internal/client/index.go +++ b/internal/client/index.go @@ -208,8 +208,10 @@ func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) { return ci.uidToClient[uid], true } + ipWithoutZone := ip.WithZone("") ci.subnetToUID.Range(func(pref netip.Prefix, id UID) (cont bool) { - if pref.Contains(ip) { + // Remove zone before checking because prefixes strip zones. + if pref.Contains(ipWithoutZone) { uid, found = id, true return false @@ -225,6 +227,26 @@ func (ci *Index) findByIP(ip netip.Addr) (c *Persistent, found bool) { return nil, false } +// FindByIPWithoutZone finds a persistent client by IP address without zone. It +// strips the IPv6 zone index from the stored IP addresses before comparing, +// because querylog entries don't have it. See TODO on [querylog.logEntry.IP]. +// +// Note that multiple clients can have the same IP address with different zones. +// Therefore, the result of this method is indeterminate. +func (ci *Index) FindByIPWithoutZone(ip netip.Addr) (c *Persistent) { + if (ip == netip.Addr{}) { + return nil + } + + for addr, uid := range ci.ipToUID { + if addr.WithZone("") == ip { + return ci.uidToClient[uid] + } + } + + return nil +} + // find finds persistent client by MAC. func (ci *Index) findByMAC(mac net.HardwareAddr) (c *Persistent, found bool) { k := macToKey(mac) diff --git a/internal/client/index_internal_test.go b/internal/client/index_internal_test.go index abf38710427..4e478462050 100644 --- a/internal/client/index_internal_test.go +++ b/internal/client/index_internal_test.go @@ -35,27 +35,49 @@ func TestClientIndex(t *testing.T) { cliID = "client-id" cliMAC = "11:11:11:11:11:11" + + linkLocalIP = "fe80::abcd:abcd:abcd:ab%eth0" + linkLocalSubnet = "fe80::/16" ) - clients := []*Persistent{{ - Name: "client1", - IPs: []netip.Addr{ - netip.MustParseAddr(cliIP1), - netip.MustParseAddr(cliIPv6), - }, - }, { - Name: "client2", - IPs: []netip.Addr{netip.MustParseAddr(cliIP2)}, - Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)}, - }, { - Name: "client_with_mac", - MACs: []net.HardwareAddr{mustParseMAC(cliMAC)}, - }, { - Name: "client_with_id", - ClientIDs: []string{cliID}, - }} + var ( + clientWithBothFams = &Persistent{ + Name: "client1", + IPs: []netip.Addr{ + netip.MustParseAddr(cliIP1), + netip.MustParseAddr(cliIPv6), + }, + } - ci := newIDIndex(clients) + clientWithSubnet = &Persistent{ + Name: "client2", + IPs: []netip.Addr{netip.MustParseAddr(cliIP2)}, + Subnets: []netip.Prefix{netip.MustParsePrefix(cliSubnet)}, + } + + clientWithMAC = &Persistent{ + Name: "client_with_mac", + MACs: []net.HardwareAddr{mustParseMAC(cliMAC)}, + } + + clientWithID = &Persistent{ + Name: "client_with_id", + ClientIDs: []string{cliID}, + } + + clientLinkLocal = &Persistent{ + Name: "client_link_local", + Subnets: []netip.Prefix{netip.MustParsePrefix(linkLocalSubnet)}, + } + ) + + ci := newIDIndex([]*Persistent{ + clientWithBothFams, + clientWithSubnet, + clientWithMAC, + clientWithID, + clientLinkLocal, + }) testCases := []struct { want *Persistent @@ -64,19 +86,23 @@ func TestClientIndex(t *testing.T) { }{{ name: "ipv4_ipv6", ids: []string{cliIP1, cliIPv6}, - want: clients[0], + want: clientWithBothFams, }, { name: "ipv4_subnet", ids: []string{cliIP2, cliSubnetIP}, - want: clients[1], + want: clientWithSubnet, }, { name: "mac", ids: []string{cliMAC}, - want: clients[2], + want: clientWithMAC, }, { name: "client_id", ids: []string{cliID}, - want: clients[3], + want: clientWithID, + }, { + name: "client_link_local_subnet", + ids: []string{linkLocalIP}, + want: clientLinkLocal, }} for _, tc := range testCases { @@ -221,3 +247,52 @@ func TestMACToKey(t *testing.T) { _ = macToKey(mac) }) } + +func TestIndex_FindByIPWithoutZone(t *testing.T) { + var ( + ip = netip.MustParseAddr("fe80::a098:7654:32ef:ff1") + ipWithZone = netip.MustParseAddr("fe80::1ff:fe23:4567:890a%eth2") + ) + + var ( + clientNoZone = &Persistent{ + Name: "client", + IPs: []netip.Addr{ip}, + } + + clientWithZone = &Persistent{ + Name: "client_with_zone", + IPs: []netip.Addr{ipWithZone}, + } + ) + + ci := newIDIndex([]*Persistent{ + clientNoZone, + clientWithZone, + }) + + testCases := []struct { + ip netip.Addr + want *Persistent + name string + }{{ + name: "without_zone", + ip: ip, + want: clientNoZone, + }, { + name: "with_zone", + ip: ipWithZone, + want: clientWithZone, + }, { + name: "zero_address", + ip: netip.Addr{}, + want: nil, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := ci.FindByIPWithoutZone(tc.ip.WithZone("")) + require.Equal(t, tc.want, c) + }) + } +} diff --git a/internal/home/clients.go b/internal/home/clients.go index ef1d1688464..9cc210ab818 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -418,7 +418,11 @@ func (clients *clientsContainer) clientOrArtificial( }() cli, ok := clients.find(id) - if ok { + if !ok { + cli = clients.clientIndex.FindByIPWithoutZone(ip) + } + + if cli != nil { return &querylog.Client{ Name: cli.Name, IgnoreQueryLog: cli.IgnoreQueryLog, diff --git a/internal/querylog/entry.go b/internal/querylog/entry.go index c3c800ed131..ed3319b069b 100644 --- a/internal/querylog/entry.go +++ b/internal/querylog/entry.go @@ -31,6 +31,7 @@ type logEntry struct { Answer []byte `json:",omitempty"` OrigAnswer []byte `json:",omitempty"` + // TODO(s.chzhen): Use netip.Addr. IP net.IP `json:"IP"` Result filtering.Result