mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-20 12:55:00 +03:00
75f3702dd3
The panel's SOCKS5 egress for AmneziaWG outbounds bound the fixed 127.0.0.1:64900, and every generated socks bridge dialed that constant. 64900 sits inside Windows' dynamic port range, where the OS can reserve whole blocks (this host excludes 64885-64984), so on the Windows builds release.yml ships the listener could stay down and every AmneziaWG outbound with it. Listen now tries 64900 first and falls back to any free loopback port; bridges, the outbound probe and the port-conflict check use EgressPort(), the port actually held. Bridges are generated apart from the listener, so BuildSocksBridge records the port it wrote and the AmneziaWG job requests an Xray restart while the listener holds a different one. Where 64900 is free nothing changes. The job's restart request is two lines of wiring no test reaches; the staleness it acts on is pinned by TestBridgesStaleUntilRegeneratedForTheBoundPort.
179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
)
|
|
|
|
// OutboundDesired pairs an instance with inbound-path DeviceOptions; AWG
|
|
// parameters must be identical on both ends of a tunnel.
|
|
type OutboundDesired struct {
|
|
Instance amneziawg.OutboundInstance
|
|
Options DeviceOptions
|
|
}
|
|
|
|
// managedOutbound is one running interface plus its rendered UAPI config
|
|
// (no-op/reconfigure decision) and an address/MTU fingerprint.
|
|
type managedOutbound struct {
|
|
dev *Device
|
|
uapiConfig string
|
|
structFP string
|
|
}
|
|
|
|
// OutboundManager owns the running AmneziaWG client interfaces keyed by tag,
|
|
// keeping the egress registry and listener current; callers just Reconcile.
|
|
type OutboundManager struct {
|
|
mu sync.Mutex
|
|
iface map[string]*managedOutbound
|
|
}
|
|
|
|
var (
|
|
outboundManagerOnce sync.Once
|
|
outboundManager *OutboundManager
|
|
)
|
|
|
|
// GetOutboundManager returns the process-wide outbound manager singleton.
|
|
func GetOutboundManager() *OutboundManager {
|
|
outboundManagerOnce.Do(func() {
|
|
outboundManager = &OutboundManager{iface: map[string]*managedOutbound{}}
|
|
})
|
|
return outboundManager
|
|
}
|
|
|
|
// outboundFingerprint captures what IpcSet can't change on a running Device,
|
|
// fixed when the netstack is built: address, and the S4-derived effective MTU.
|
|
func outboundFingerprint(inst amneziawg.OutboundInstance) string {
|
|
return fmt.Sprintf("%d|%s",
|
|
amneziawg.EffectiveMTU(inst.MTU, inst.Obfuscation.S4),
|
|
strings.Join(inst.Address, ","))
|
|
}
|
|
|
|
// Reconcile converges devices to desired and stops removed tags; per-tick
|
|
// contract of Manager.Reconcile -- errors log, never abort the batch.
|
|
func (m *OutboundManager) Reconcile(desired []OutboundDesired) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Empty desired converges to "no tunnels": close egress listener so
|
|
// the egress port stays free on installs without AWG outbounds.
|
|
if len(desired) == 0 {
|
|
for tag, cur := range m.iface {
|
|
cur.dev.Close()
|
|
GetEgressServer().DeleteStack(tag)
|
|
delete(m.iface, tag)
|
|
logger.Infof("amneziawgnet: stopped embedded outbound %q", tag)
|
|
}
|
|
GetEgressServer().Close()
|
|
return
|
|
}
|
|
|
|
if err := GetEgressServer().Listen(); err != nil {
|
|
logger.Warningf("amneziawgnet: egress listener unavailable: %v", err)
|
|
}
|
|
|
|
want := make(map[string]struct{}, len(desired))
|
|
for _, d := range desired {
|
|
want[d.Instance.Tag] = struct{}{}
|
|
}
|
|
for tag, cur := range m.iface {
|
|
if _, ok := want[tag]; ok {
|
|
continue
|
|
}
|
|
cur.dev.Close()
|
|
GetEgressServer().DeleteStack(tag)
|
|
delete(m.iface, tag)
|
|
logger.Infof("amneziawgnet: stopped embedded outbound %q", tag)
|
|
}
|
|
|
|
for _, d := range desired {
|
|
if err := m.ensureLocked(d); err != nil {
|
|
logger.Warningf("amneziawgnet: reconcile failed for outbound %q: %v", d.Instance.Tag, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ensureLocked picks no-op / reconfigure-in-place / rebuild for one desired
|
|
// outbound (address/MTU are fixed at netstack build time).
|
|
func (m *OutboundManager) ensureLocked(d OutboundDesired) error {
|
|
inst, opts := d.Instance, d.Options
|
|
if opts.Logger == nil {
|
|
opts.Logger = verboseLoggerIfEnabled(0)
|
|
}
|
|
|
|
fp := outboundFingerprint(inst)
|
|
conf, err := buildClientUAPIConfig(inst, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("render UAPI config: %w", err)
|
|
}
|
|
|
|
cur, exists := m.iface[inst.Tag]
|
|
if exists && cur.structFP == fp {
|
|
if conf == cur.uapiConfig {
|
|
GetEgressServer().SetStack(inst.Tag, cur.dev, inst.DNS)
|
|
return nil
|
|
}
|
|
if err := cur.dev.IpcSet(conf); err != nil {
|
|
return fmt.Errorf("reconfigure outbound %q: %w", inst.Tag, err)
|
|
}
|
|
cur.uapiConfig = conf
|
|
GetEgressServer().SetStack(inst.Tag, cur.dev, inst.DNS)
|
|
return nil
|
|
}
|
|
|
|
if exists {
|
|
cur.dev.Close()
|
|
// A failed rebuild must not leave stackFor handing out a closed device.
|
|
GetEgressServer().DeleteStack(inst.Tag)
|
|
delete(m.iface, inst.Tag)
|
|
}
|
|
dev, err := newUnconfiguredClientDevice(inst, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dev.ConfigureClient(inst, opts); err != nil {
|
|
return err
|
|
}
|
|
m.iface[inst.Tag] = &managedOutbound{dev: dev, uapiConfig: conf, structFP: fp}
|
|
GetEgressServer().SetStack(inst.Tag, dev, inst.DNS)
|
|
logger.Infof("amneziawgnet: started embedded outbound %s (%d peers)", inst.Tag, len(inst.Peers))
|
|
return nil
|
|
}
|
|
|
|
// Remove tears down one outbound's device by tag.
|
|
func (m *OutboundManager) Remove(tag string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
cur, exists := m.iface[tag]
|
|
if !exists {
|
|
return
|
|
}
|
|
cur.dev.Close()
|
|
GetEgressServer().DeleteStack(tag)
|
|
delete(m.iface, tag)
|
|
logger.Infof("amneziawgnet: stopped embedded outbound %q", tag)
|
|
}
|
|
|
|
// StopAll tears down every managed outbound device and the egress listener;
|
|
// m.mu stays held across Close so a cron tick cannot re-bind mid-teardown.
|
|
func (m *OutboundManager) StopAll() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for tag, cur := range m.iface {
|
|
cur.dev.Close()
|
|
GetEgressServer().DeleteStack(tag)
|
|
delete(m.iface, tag)
|
|
}
|
|
GetEgressServer().Close()
|
|
}
|
|
|
|
// HasRunning reports whether any outbound device is currently managed.
|
|
func (m *OutboundManager) HasRunning() bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.iface) > 0
|
|
}
|