mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(amneziawg): only delete IPv6 aliases this panel added itself
Teardown and the per-peer diff built the list of IPv6 aliases to delete from configuration alone, while adding an alias treated "already exists" as success. An address the host already owned, ::1 on lo or its own public /128, was therefore deleted when an inbound or peer that named it went away. The manager records the aliases whose add actually created them and deletes only those; an address that was already there is left in place.
This commit is contained in:
@@ -116,6 +116,9 @@ func (m *managed) close() {
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
ifaces map[int]*managed
|
||||
// v6Owned: IPv6 aliases this Manager put on the host, the only ones it
|
||||
// may delete again (see applyV6Aliases).
|
||||
v6Owned map[v6Alias]struct{}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -189,7 +192,7 @@ func (m *Manager) ensureLocked(d Desired) error {
|
||||
if conf == cur.uapiConfig {
|
||||
cur.peers.Store(NewPeerIndex(inst.Peers))
|
||||
cur.inst = inst
|
||||
applyV6Aliases(diffV6Aliases(oldInst, inst))
|
||||
m.applyV6Aliases(diffV6Aliases(oldInst, inst))
|
||||
// buildUAPIConfig never reads ForwardedPorts (it's a panel-level
|
||||
// concept, not a WireGuard UAPI field), so a ForwardedPorts-only
|
||||
// edit renders byte-identical here and takes this exact no-op
|
||||
@@ -206,7 +209,7 @@ func (m *Manager) ensureLocked(d Desired) error {
|
||||
oldPeers := cur.peers.Swap(NewPeerIndex(inst.Peers))
|
||||
cur.inst = inst
|
||||
cur.uapiConfig = conf
|
||||
applyV6Aliases(diffV6Aliases(oldInst, inst))
|
||||
m.applyV6Aliases(diffV6Aliases(oldInst, inst))
|
||||
cur.portForwards.Reconcile(inst)
|
||||
cur.dropReassignedFlows(oldPeers)
|
||||
return nil
|
||||
@@ -262,7 +265,7 @@ func (m *Manager) ensureLocked(d Desired) error {
|
||||
conf, _ := buildUAPIConfig(inst, opts)
|
||||
next.uapiConfig = conf
|
||||
m.ifaces[inst.Id] = next
|
||||
applyV6Aliases(diffV6Aliases(oldInst, inst))
|
||||
m.applyV6Aliases(diffV6Aliases(oldInst, inst))
|
||||
portForwards.Reconcile(inst)
|
||||
logger.Infof("amneziawgnet: started embedded interface %s for inbound %d", inst.InterfaceName, inst.Id)
|
||||
return nil
|
||||
@@ -302,7 +305,7 @@ func (m *Manager) Reconcile(desired []Desired) {
|
||||
if _, ok := want[id]; ok {
|
||||
continue
|
||||
}
|
||||
applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
|
||||
m.applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
|
||||
cur.close()
|
||||
delete(m.ifaces, id)
|
||||
logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
|
||||
@@ -325,7 +328,7 @@ func (m *Manager) Remove(id int) {
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
|
||||
m.applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
|
||||
cur.close()
|
||||
delete(m.ifaces, id)
|
||||
logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
|
||||
@@ -336,7 +339,7 @@ func (m *Manager) StopAll() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for id, cur := range m.ifaces {
|
||||
applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
|
||||
m.applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
|
||||
cur.close()
|
||||
delete(m.ifaces, id)
|
||||
}
|
||||
|
||||
@@ -117,16 +117,30 @@ const ipCommandTimeout = 3 * time.Second
|
||||
// aliased at once is harmless). Never surfaces an error — an alias failing
|
||||
// only narrows that one peer's own outbound-source-identity feature, never
|
||||
// a reason to fail the tunnel or its SOCKS5 relay.
|
||||
func applyV6Aliases(add, remove []v6Alias) {
|
||||
for _, a := range add {
|
||||
addV6Alias(a)
|
||||
func (m *Manager) applyV6Aliases(add, remove []v6Alias) {
|
||||
if m.v6Owned == nil {
|
||||
m.v6Owned = map[v6Alias]struct{}{}
|
||||
}
|
||||
for _, a := range add {
|
||||
if addV6Alias(a) {
|
||||
m.v6Owned[a] = struct{}{}
|
||||
}
|
||||
}
|
||||
// A peer may name an address the host already owns (::1 on lo, its own
|
||||
// public /128): only what this Manager itself added is ever deleted.
|
||||
for _, a := range remove {
|
||||
if _, ours := m.v6Owned[a]; !ours {
|
||||
logger.Debugf("amneziawgnet: leaving IPv6 address %s on %s, not aliased by this process", a.Addr, a.Iface)
|
||||
continue
|
||||
}
|
||||
delete(m.v6Owned, a)
|
||||
removeV6Alias(a)
|
||||
}
|
||||
}
|
||||
|
||||
func addV6Alias(a v6Alias) {
|
||||
// addV6Alias reports whether the alias is on the host because of this call;
|
||||
// an address that was already there is the host's own, not ours to remove.
|
||||
func addV6Alias(a v6Alias) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), ipCommandTimeout)
|
||||
defer cancel()
|
||||
// nodad: this address is a specific peer's own admin-assigned identity,
|
||||
@@ -136,16 +150,15 @@ func addV6Alias(a v6Alias) {
|
||||
stderr, err := runIP(ctx, "-6", "addr", "add", a.Addr+"/128", "dev", a.Iface, "nodad")
|
||||
if err == nil {
|
||||
logger.Infof("amneziawgnet: aliased IPv6 address %s onto %s", a.Addr, a.Iface)
|
||||
return
|
||||
return true
|
||||
}
|
||||
if strings.Contains(stderr, "File exists") {
|
||||
// Already the desired end state -- most commonly hit once, harmlessly,
|
||||
// right after an ungraceful panel restart (the OS-level alias from
|
||||
// before the crash outlives the process; the in-memory managed map
|
||||
// doesn't).
|
||||
return
|
||||
// Already there: the host's own address, or an alias an ungraceful
|
||||
// restart left behind -- indistinguishable, so neither is ours.
|
||||
return false
|
||||
}
|
||||
logger.Warningf("amneziawgnet: alias IPv6 address %s onto %s: %v (%s)", a.Addr, a.Iface, err, strings.TrimSpace(stderr))
|
||||
return false
|
||||
}
|
||||
|
||||
func removeV6Alias(a v6Alias) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
||||
)
|
||||
|
||||
func peerWithIPs(email string, ips ...string) amneziawg.Peer {
|
||||
@@ -264,14 +265,71 @@ func TestApplyV6AliasesAddsBeforeRemoves(t *testing.T) {
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
applyV6Aliases(
|
||||
m := &Manager{ifaces: map[int]*managed{}}
|
||||
m.applyV6Aliases([]v6Alias{{Addr: "fd86::2", Iface: "eth0"}}, nil)
|
||||
m.applyV6Aliases(
|
||||
[]v6Alias{{Addr: "fd86::99", Iface: "eth0"}},
|
||||
[]v6Alias{{Addr: "fd86::2", Iface: "eth0"}},
|
||||
)
|
||||
if len(*calls) != 2 {
|
||||
t.Fatalf("expected exactly 2 calls, got %d", len(*calls))
|
||||
if len(*calls) != 3 {
|
||||
t.Fatalf("expected exactly 3 calls, got %d", len(*calls))
|
||||
}
|
||||
if order[0] != "add" || order[1] != "del" {
|
||||
t.Fatalf("expected add before del, got order=%v", order)
|
||||
if order[1] != "add" || order[2] != "del" {
|
||||
t.Fatalf("expected add before del, got order=%v", order[1:])
|
||||
}
|
||||
}
|
||||
|
||||
// An address the host already held (::1 on lo, its own /128) answers the add
|
||||
// with EEXIST; tearing the inbound down must leave it in place.
|
||||
func TestManagerRemoveLeavesHostOwnedAddressInPlace(t *testing.T) {
|
||||
calls := withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
|
||||
if args[2] == "add" && args[3] == "::1/128" {
|
||||
return "RTNETLINK answers: File exists", errors.New("exit status 2")
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
priv, pub, err := wireguard.GenerateWireguardKeypair()
|
||||
if err != nil {
|
||||
t.Fatalf("generate keypair: %v", err)
|
||||
}
|
||||
_, foreignPub, err := wireguard.GenerateWireguardKeypair()
|
||||
if err != nil {
|
||||
t.Fatalf("generate peer keypair: %v", err)
|
||||
}
|
||||
_, hostOwnPub, err := wireguard.GenerateWireguardKeypair()
|
||||
if err != nil {
|
||||
t.Fatalf("generate peer keypair: %v", err)
|
||||
}
|
||||
m := &Manager{ifaces: map[int]*managed{}}
|
||||
defer m.StopAll()
|
||||
inst := amneziawg.Instance{
|
||||
Id: 7,
|
||||
InterfaceName: "awgtest7",
|
||||
ListenPort: 58717,
|
||||
PrivateKey: priv,
|
||||
PublicKey: pub,
|
||||
Address: []string{"10.207.0.1/24", "fd86:ea04:1115::1/64"},
|
||||
MTU: 1420,
|
||||
Obfuscation: amneziawg.Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20},
|
||||
IPv6Enabled: true,
|
||||
IPv6ExternalInterface: "lo",
|
||||
Peers: []amneziawg.Peer{
|
||||
{Email: "foreign", PublicKey: foreignPub, AllowedIPs: []string{"10.207.0.2/32", "2001:db8:beef::1/128"}},
|
||||
{Email: "host-own", PublicKey: hostOwnPub, AllowedIPs: []string{"10.207.0.3/32", "::1/128"}},
|
||||
},
|
||||
}
|
||||
if err := m.Ensure(Desired{Instance: inst}); err != nil {
|
||||
t.Fatalf("Ensure: %v", err)
|
||||
}
|
||||
m.Remove(inst.Id)
|
||||
|
||||
var deleted []string
|
||||
for _, c := range *calls {
|
||||
if c[2] == "del" {
|
||||
deleted = append(deleted, c[3])
|
||||
}
|
||||
}
|
||||
if len(deleted) != 1 || deleted[0] != "2001:db8:beef::1/128" {
|
||||
t.Fatalf("deleted %v, want only the alias this process added (2001:db8:beef::1/128)", deleted)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user