diff --git a/internal/amneziawgnet/identity.go b/internal/amneziawgnet/identity.go index 087774f00..62e21369d 100644 --- a/internal/amneziawgnet/identity.go +++ b/internal/amneziawgnet/identity.go @@ -49,7 +49,9 @@ func (idx *PeerIndex) Lookup(addr netip.Addr) (amneziawg.Peer, bool) { bestBits := -1 var bestPeer amneziawg.Peer for _, e := range idx.entries { - if e.prefix.Bits() <= bestBits || !e.prefix.Contains(addr) { + // Equal length: the later peer wins, as in the device's allowed-IPs + // table, which buildUAPIConfig fills from the same peers in this order. + if e.prefix.Bits() < bestBits || !e.prefix.Contains(addr) { continue } bestBits = e.prefix.Bits() diff --git a/internal/amneziawgnet/identity_test.go b/internal/amneziawgnet/identity_test.go new file mode 100644 index 000000000..4a9fdb460 --- /dev/null +++ b/internal/amneziawgnet/identity_test.go @@ -0,0 +1,72 @@ +package amneziawgnet + +import ( + "net/netip" + "slices" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/amneziawg" + "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard" +) + +// TestPeerIndexLookupMatchesDeviceAllowedIPs catches Lookup attributing a +// prefix two peers both carry to a peer the device accepts no such source from. +func TestPeerIndexLookupMatchesDeviceAllowedIPs(t *testing.T) { + serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair() + if err != nil { + t.Fatalf("generate server keypair: %v", err) + } + var peers []amneziawg.Peer + for _, p := range []amneziawg.Peer{ + {Email: "first@example.com", AllowedIPs: []string{"10.202.0.2/32", "fd00:202::2/128", "0.0.0.0/0", "::/0"}}, + {Email: "last@example.com", AllowedIPs: []string{"10.202.0.3/32", "fd00:202::3/128", "0.0.0.0/0", "::/0"}}, + } { + if _, p.PublicKey, err = wireguard.GenerateWireguardKeypair(); err != nil { + t.Fatalf("generate peer keypair: %v", err) + } + peers = append(peers, p) + } + + dev, err := NewDevice(amneziawg.Instance{ + Id: 1, + PrivateKey: serverPriv, + PublicKey: serverPub, + Address: []string{"10.202.0.1/24"}, + MTU: 1420, + Peers: peers, + }, DeviceOptions{}) + if err != nil { + t.Fatalf("NewDevice: %v", err) + } + defer dev.Close() + + dump, err := dev.IpcGet() + if err != nil { + t.Fatalf("IpcGet: %v", err) + } + _, states := parseUAPIDump(dump) + + idx := NewPeerIndex(peers) + for _, tc := range []struct{ addr, prefix string }{ + {"10.99.0.1", "0.0.0.0/0"}, + {"fd00:99::1", "::/0"}, + {"10.202.0.2", "10.202.0.2/32"}, + {"10.202.0.3", "10.202.0.3/32"}, + {"fd00:202::2", "fd00:202::2/128"}, + {"fd00:202::3", "fd00:202::3/128"}, + } { + t.Run(tc.addr, func(t *testing.T) { + got, ok := idx.Lookup(netip.MustParseAddr(tc.addr)) + if !ok { + t.Fatalf("Lookup(%s) found no peer", tc.addr) + } + gotHex, err := wireguard.KeyToHex(got.PublicKey) + if err != nil { + t.Fatalf("KeyToHex: %v", err) + } + if !slices.Contains(states[gotHex].allowedIPs, tc.prefix) { + t.Errorf("Lookup(%s) = %s, but the device holds %s for another peer", tc.addr, got.Email, tc.prefix) + } + }) + } +}