mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-08 19:27:14 +00:00
e7c6f92e7f
defaultWireguardClients/defaultAmneziaWGClients only ever checked uniqueness against their own inbound's client list, so two inbounds sharing a subnet (same protocol or not) could silently hand out or accept the same address -- the exact scenario behind a real duplicate-IP incident where a WireGuard and an AmneziaWG client both ended up on the same address. otherTunnelAllowedIPs now collects every address already claimed on every other tunnel inbound and folds it into both the auto-allocation pool and the manual-entry collision check, naming the other inbound in the error when it fires.
129 lines
5.2 KiB
Go
129 lines
5.2 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
const amneziawgClientTestSettings = `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24}}`
|
|
|
|
func TestDefaultAmneziaWGSubnetBases(t *testing.T) {
|
|
v4, v6, err := defaultAmneziaWGSubnetBases(amneziawgClientTestSettings)
|
|
if err != nil {
|
|
t.Fatalf("defaultAmneziaWGSubnetBases: %v", err)
|
|
}
|
|
if v4 != "10.8.1.0/24" {
|
|
t.Fatalf("v4Base = %q, want 10.8.1.0/24", v4)
|
|
}
|
|
if v6 != "" {
|
|
t.Fatalf("v6Base = %q, want empty when IPv6 is not enabled", v6)
|
|
}
|
|
}
|
|
|
|
func TestDefaultAmneziaWGSubnetBasesIncludesIPv6WhenEnabled(t *testing.T) {
|
|
settings := `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24,"ipv6Enabled":true,"ipv6Subnet":"fd00::/64"}}`
|
|
v4, v6, err := defaultAmneziaWGSubnetBases(settings)
|
|
if err != nil {
|
|
t.Fatalf("defaultAmneziaWGSubnetBases: %v", err)
|
|
}
|
|
if v4 != "10.8.1.0/24" || v6 != "fd00::/64" {
|
|
t.Fatalf("got v4=%q v6=%q", v4, v6)
|
|
}
|
|
}
|
|
|
|
func TestDefaultAmneziaWGSubnetBasesRejectsMissingServer(t *testing.T) {
|
|
if _, _, err := defaultAmneziaWGSubnetBases(`{}`); err == nil {
|
|
t.Fatal("expected an error when the settings have no server block")
|
|
}
|
|
}
|
|
|
|
func TestDefaultAmneziaWGClientsGeneratesKeypairAndAllocatesFromOwnSubnet(t *testing.T) {
|
|
clients := []model.Client{{Email: "a@awg"}}
|
|
ifaces := []any{map[string]any{"email": "a@awg"}}
|
|
if err := defaultAmneziaWGClients(amneziawgClientTestSettings, nil, clients, ifaces, nil); err != nil {
|
|
t.Fatalf("defaultAmneziaWGClients: %v", err)
|
|
}
|
|
c := clients[0]
|
|
if c.PrivateKey == "" || c.PublicKey == "" {
|
|
t.Fatalf("keypair not generated: priv=%q pub=%q", c.PrivateKey, c.PublicKey)
|
|
}
|
|
if len(c.AllowedIPs) != 1 || c.AllowedIPs[0] != "10.8.1.2/32" {
|
|
t.Fatalf("allowedIPs not allocated from the inbound's own subnet: %v", c.AllowedIPs)
|
|
}
|
|
}
|
|
|
|
func TestDefaultAmneziaWGClientsPreservesProvided(t *testing.T) {
|
|
clients := []model.Client{{
|
|
Email: "b@awg",
|
|
PrivateKey: "keep-priv",
|
|
PublicKey: "keep-pub",
|
|
AllowedIPs: []string{"10.8.1.50/32"},
|
|
}}
|
|
ifaces := []any{map[string]any{"email": "b@awg"}}
|
|
if err := defaultAmneziaWGClients(amneziawgClientTestSettings, nil, clients, ifaces, nil); err != nil {
|
|
t.Fatalf("defaultAmneziaWGClients: %v", err)
|
|
}
|
|
if clients[0].PrivateKey != "keep-priv" || clients[0].PublicKey != "keep-pub" {
|
|
t.Fatalf("provided keys were rotated: %+v", clients[0])
|
|
}
|
|
if clients[0].AllowedIPs[0] != "10.8.1.50/32" {
|
|
t.Fatalf("provided allowedIPs changed: %v", clients[0].AllowedIPs)
|
|
}
|
|
}
|
|
|
|
func TestDefaultAmneziaWGClientsRejectsSameInboundDuplicate(t *testing.T) {
|
|
existing := []model.Client{{Email: "old@awg", AllowedIPs: []string{"10.8.1.9/32"}}}
|
|
dup := []model.Client{{Email: "new@awg", AllowedIPs: []string{"10.8.1.9/32"}}}
|
|
err := defaultAmneziaWGClients(amneziawgClientTestSettings, existing, dup, []any{map[string]any{"email": "new@awg"}}, nil)
|
|
if err == nil {
|
|
t.Fatal("duplicate allowedIPs on the same inbound must be rejected")
|
|
}
|
|
}
|
|
|
|
// The exact real-world scenario that motivated crossInboundUsed: a WireGuard
|
|
// client and an AmneziaWG peer given the same address by habit. The
|
|
// collision must be caught even though the two live on different inbounds
|
|
// and neither appears in the other's own "existing" client list, and the
|
|
// error should name the other inbound so an admin isn't left guessing.
|
|
func TestDefaultAmneziaWGClientsRejectsCrossInboundDuplicate(t *testing.T) {
|
|
crossUsed := map[string]string{"10.8.1.21/32": "inbound 'wg' (#12)"}
|
|
dup := []model.Client{{Email: "c@awg", AllowedIPs: []string{"10.8.1.21/32"}}}
|
|
err := defaultAmneziaWGClients(amneziawgClientTestSettings, nil, dup, []any{map[string]any{"email": "c@awg"}}, crossUsed)
|
|
if err == nil {
|
|
t.Fatal("allowedIPs already used on another inbound must be rejected")
|
|
}
|
|
if !strings.Contains(err.Error(), "inbound 'wg' (#12)") {
|
|
t.Fatalf("error should name the other inbound holding the address, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultAmneziaWGClientsAutoAllocateSkipsCrossInboundUsed(t *testing.T) {
|
|
crossUsed := map[string]string{"10.8.1.2/32": "inbound 'other-awg' (#3)"}
|
|
clients := []model.Client{{Email: "d@awg"}}
|
|
ifaces := []any{map[string]any{"email": "d@awg"}}
|
|
if err := defaultAmneziaWGClients(amneziawgClientTestSettings, nil, clients, ifaces, crossUsed); err != nil {
|
|
t.Fatalf("defaultAmneziaWGClients: %v", err)
|
|
}
|
|
if clients[0].AllowedIPs[0] != "10.8.1.3/32" {
|
|
t.Fatalf("auto-allocation should skip the cross-inbound-used .2 and pick .3, got %v", clients[0].AllowedIPs)
|
|
}
|
|
}
|
|
|
|
// Unlike WireGuard's allocation base (inferred from existing peers with a
|
|
// fallback), AmneziaWG's base always comes from the inbound's own configured
|
|
// subnet -- so this is really confirming crossInboundUsed can never change
|
|
// which subnet is used, only which addresses within it are free.
|
|
func TestDefaultAmneziaWGClientsCrossInboundUsedDoesNotChangeBase(t *testing.T) {
|
|
crossUsed := map[string]string{"192.168.99.5/32": "inbound 'unrelated' (#99)"}
|
|
clients := []model.Client{{Email: "e@awg"}}
|
|
ifaces := []any{map[string]any{"email": "e@awg"}}
|
|
if err := defaultAmneziaWGClients(amneziawgClientTestSettings, nil, clients, ifaces, crossUsed); err != nil {
|
|
t.Fatalf("defaultAmneziaWGClients: %v", err)
|
|
}
|
|
if got := clients[0].AllowedIPs[0]; got != "10.8.1.2/32" {
|
|
t.Fatalf("base subnet must stay the inbound's own 10.8.1.0/24; got %v", got)
|
|
}
|
|
}
|