fix(amneziawg): attribute an equal-prefix source to the peer the device picks

PeerIndex names the peer a source address came from, but for two peers
holding the same prefix, which only a default route can be after the
overlap check, it kept the first while amneziawg-go's allowed-IPs table
hands the prefix to the peer configured last. Traffic was then billed
to a different client than the one the device accepted it from.

An equal-length match now goes to the later peer, as in the device.
This commit is contained in:
MHSanaei
2026-11-01 10:02:11 +01:00
parent efaa73db3a
commit ecd817658c
2 changed files with 75 additions and 1 deletions
+3 -1
View File
@@ -49,7 +49,9 @@ func (idx *PeerIndex) Lookup(addr netip.Addr) (amneziawg.Peer, bool) {
bestBits := -1 bestBits := -1
var bestPeer amneziawg.Peer var bestPeer amneziawg.Peer
for _, e := range idx.entries { 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 continue
} }
bestBits = e.prefix.Bits() bestBits = e.prefix.Bits()
+72
View File
@@ -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)
}
})
}
}