diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a180412d26..d9a6d2198ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ NOTE: Add new changes BELOW THIS COMMENT. ### Fixed +- 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. @@ -51,6 +53,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/internal/client/index.go b/internal/client/index.go index c6a17cb3703..24ec6e30e2c 100644 --- a/internal/client/index.go +++ b/internal/client/index.go @@ -197,8 +197,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 @@ -214,6 +216,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 1474002bc5f..7a2487ca27b 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -412,7 +412,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