mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
3f1e52f09e
SettingService.GetDefaultJSONConfig was a byte-identical copy of GetDefaultXrayConfig with no callers anywhere in the tree. amneziawgnet.normalizeDNSServer re-implemented the exported amneziawg.NormalizeDNSServer line for line, in a file that already imports that package for EffectiveMTU three lines above it. Its two callers now use the exported one, so the bare-IP-to-host:port rule has a single definition.
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
|
|
// 127.0.0.1:64900 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
|
|
}
|