fix(wireguard): hold /0 allowedIPs to the overlap guard

The tunnel overlap guard treated every /0 allowedIPs entry as claiming
no address, while the entry was stored with its host bits and the
consumers read them back: the AmneziaWG port forwarder resolves a peer's
target from them and the device routes the /0 by longest prefix. An
entry such as 10.8.1.2/0 therefore passed next to another client's
10.8.1.2/32, and forwarded ports reached that client's device.

A written entry now counts with its whole range, so a /0 collides with
every held address of its family. On the stored side only a literal
0.0.0.0/0 or ::/0 default route keeps nobody out, and a /0 with host
bits claims that host. Editing a legacy peer that echoes its stored
default route still works.
This commit is contained in:
MHSanaei
2026-11-01 00:06:09 +01:00
parent d114845e44
commit daf5b16eda
8 changed files with 196 additions and 14 deletions
+38 -7
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/netip"
"slices"
"strconv"
"strings"
@@ -188,9 +189,9 @@ func normalizeWireguardAllowedIPs(values []string) ([]string, error) {
return out, nil
}
// wireguardClaimedPrefix is the masked range an allowedIPs entry claims, as xray
// reads it. A /0 default route claims no tunnel address, as legacy peers carry it.
func wireguardClaimedPrefix(s string) (netip.Prefix, bool) {
// parseWireguardAllowedIP reads an allowedIPs entry unmasked, a bare address as
// a single host.
func parseWireguardAllowedIP(s string) (netip.Prefix, bool) {
s = strings.TrimSpace(s)
p, err := netip.ParsePrefix(s)
if err != nil {
@@ -200,14 +201,35 @@ func wireguardClaimedPrefix(s string) (netip.Prefix, bool) {
}
p = netip.PrefixFrom(a, a.BitLen())
}
if p.Bits() == 0 {
return p, true
}
// isWireguardDefaultRoute reports whether an allowedIPs entry carries a /0 mask.
func isWireguardDefaultRoute(s string) bool {
p, ok := parseWireguardAllowedIP(s)
return ok && p.Bits() == 0
}
// wireguardClaimedPrefix is the range a stored allowedIPs entry keeps other clients
// out of. Only a 0.0.0.0/0 or ::/0 default route claims nothing (legacy peers carry it).
func wireguardClaimedPrefix(s string) (netip.Prefix, bool) {
p, ok := parseWireguardAllowedIP(s)
if !ok {
return netip.Prefix{}, false
}
if p.Bits() == 0 {
if p.Addr().IsUnspecified() {
return netip.Prefix{}, false
}
// The host bits are stored with it and read back as the peer's tunnel
// address (amneziawg.FirstIPv4 / FirstIPv6), so they are what it claims.
return netip.PrefixFrom(p.Addr(), p.Addr().BitLen()), true
}
return p.Masked(), true
}
// wireguardAllowedIPsOverlap returns the first entry whose range overlaps a used
// one, and that used entry; xray routes and attributes by containment, not equality.
// wireguardAllowedIPsOverlap returns the first entry whose whole range (a /0 holds its entire
// family) overlaps a used one, and that entry; xray routes by containment, not equality.
func wireguardAllowedIPsOverlap(entries, used []string) (entry, taken string) {
usedPrefixes := make([]netip.Prefix, len(used))
usedOK := make([]bool, len(used))
@@ -215,10 +237,11 @@ func wireguardAllowedIPsOverlap(entries, used []string) (entry, taken string) {
usedPrefixes[i], usedOK[i] = wireguardClaimedPrefix(u)
}
for _, e := range entries {
ep, ok := wireguardClaimedPrefix(e)
ep, ok := parseWireguardAllowedIP(e)
if !ok {
continue
}
ep = ep.Masked()
for i, up := range usedPrefixes {
if usedOK[i] && ep.Overlaps(up) {
return e, used[i]
@@ -228,6 +251,14 @@ func wireguardAllowedIPsOverlap(entries, used []string) (entry, taken string) {
return "", ""
}
// withoutKeptDefaultRoutes drops the /0 entries stored already holds: an edit that
// echoes a legacy peer's default route claims nothing new and must stay possible.
func withoutKeptDefaultRoutes(entries, stored []string) []string {
return slices.DeleteFunc(slices.Clone(entries), func(e string) bool {
return isWireguardDefaultRoute(e) && slices.Contains(stored, e)
})
}
// defaultWireguardClients fills in blank WireGuard credentials for newly added
// clients: a generated keypair when none was provided, a derived public key when
// only a private key was given, and a unique tunnel address allocated from the