mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-17 11:25:20 +03:00
c8a182b6fb
xray's WireGuard inbound credits a packet to the first peer whose allowedIPs
contain its source address, and routes replies by the same table. The panel
only rejected an allowedIPs entry that was string-equal to another client's,
so a pre-assigned address typed in interface notation (10.10.2.9/24, as other
WireGuard tools export it) was accepted and claimed the whole /24: other
clients' traffic and online IPs were credited to that one email, and replies
went to a peer with no endpoint ("no known endpoint for peer").
The collision check now compares masked ranges on every path that uses it:
add, edit, the cross-inbound recheck inside the write transaction, and
AmneziaWG. Auto-allocation skips any address inside a prefix another client
holds. A /0 default route still claims nothing, as before, because legacy
migrated peers carry one. Clients already saved with overlapping ranges keep
working as they do today until edited. The API docs describing the error are
updated, including the stale claim that cross-inbound duplicates are accepted.
Closes #6623
120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
// defaultAmneziaWGSubnetBases resolves the /CIDR bases new peer addresses are
|
|
// allocated from, out of the inbound's own configured server subnet(s) —
|
|
// unlike WireGuard, which always falls back to a fixed 10.0.0.0/24. v6Base is
|
|
// "" when the server doesn't have IPv6 enabled.
|
|
func defaultAmneziaWGSubnetBases(settingsJSON string) (v4Base, v6Base string, err error) {
|
|
var parsed amneziawg.InboundSettings
|
|
if err := json.Unmarshal([]byte(settingsJSON), &parsed); err != nil {
|
|
return "", "", fmt.Errorf("amneziawg: invalid settings: %w", err)
|
|
}
|
|
if parsed.Server == nil {
|
|
return "", "", fmt.Errorf("amneziawg: settings missing server block")
|
|
}
|
|
cidr := parsed.Server.SubnetCIDR
|
|
if cidr <= 0 {
|
|
cidr = 24
|
|
}
|
|
v4Base = fmt.Sprintf("%s/%d", parsed.Server.SubnetIP, cidr)
|
|
if parsed.Server.IPv6Enabled && parsed.Server.IPv6Subnet != "" {
|
|
v6Base = parsed.Server.IPv6Subnet
|
|
}
|
|
return v4Base, v6Base, nil
|
|
}
|
|
|
|
// defaultAmneziaWGClients fills in blank credentials and a free tunnel address
|
|
// for new clients, mutating both the typed clients and the parallel raw maps
|
|
// persisted into the settings. Existing values are never overwritten, so an
|
|
// edit never rotates keys. Mirrors defaultWireguardClients; crossInboundUsed
|
|
// (see otherTunnelAllowedIPs) narrows which addresses are still free.
|
|
func defaultAmneziaWGClients(settingsJSON string, existing, clients []model.Client, interfaceClients []any, crossInboundUsed map[string]string) error {
|
|
v4Base, v6Base, err := defaultAmneziaWGSubnetBases(settingsJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
used := make([]string, 0)
|
|
for i := range existing {
|
|
used = append(used, existing[i].AllowedIPs...)
|
|
}
|
|
for addr := range crossInboundUsed {
|
|
used = append(used, addr)
|
|
}
|
|
for i := range clients {
|
|
c := &clients[i]
|
|
if c.PrivateKey == "" && c.PublicKey == "" {
|
|
priv, pub, err := wgutil.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.PrivateKey = priv
|
|
c.PublicKey = pub
|
|
} else if c.PublicKey == "" && c.PrivateKey != "" {
|
|
pub, err := wgutil.PublicKeyFromPrivate(c.PrivateKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.PublicKey = pub
|
|
}
|
|
if len(c.AllowedIPs) == 0 {
|
|
// allowWidening=false: unlike WireGuard's Xray-native inbound,
|
|
// AmneziaWG's kernel interface Address is exactly the configured
|
|
// subnet, so an address allocated outside it would be silently
|
|
// unroutable. Exhaustion here must fail loudly instead.
|
|
addr, err := allocateWireguardAddress(used, v4Base, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allowed := []string{addr}
|
|
if v6Base != "" {
|
|
addr6, err := allocateWireguardAddress(used, v6Base, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allowed = append(allowed, addr6)
|
|
}
|
|
c.AllowedIPs = allowed
|
|
} else {
|
|
normalized, err := normalizeWireguardAllowedIPs(c.AllowedIPs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(normalized) == 0 {
|
|
return common.NewError("amneziawg: allowedIPs has no usable entry")
|
|
}
|
|
if entry, taken := wireguardAllowedIPsOverlap(normalized, used); taken != "" {
|
|
if where := crossInboundUsed[taken]; where != "" {
|
|
return common.NewError("amneziawg: allowedIPs entry", entry, "overlaps", taken, "used by a client on", where)
|
|
}
|
|
return common.NewError("amneziawg: allowedIPs entry", entry, "overlaps", taken, "used by another client")
|
|
}
|
|
c.AllowedIPs = normalized
|
|
}
|
|
used = append(used, c.AllowedIPs...)
|
|
|
|
if i < len(interfaceClients) {
|
|
if m, ok := interfaceClients[i].(map[string]any); ok {
|
|
m["privateKey"] = c.PrivateKey
|
|
m["publicKey"] = c.PublicKey
|
|
m["allowedIPs"] = c.AllowedIPs
|
|
if c.PreSharedKey != "" {
|
|
m["preSharedKey"] = c.PreSharedKey
|
|
}
|
|
interfaceClients[i] = m
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|