mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 18:07:14 +00:00
58671533bb
New internal/amneziawgnet package: builds a real amneziawg-go Device over a gVisor netstack from an existing amneziawg.Instance, with a TCP/UDP forwarder that recovers each tunnel connection's real destination and a peer-identity index keyed by AllowedIPs. This is the foundation for migrating AmneziaWG off the kernel-module+TPROXY path (see the AmneziaWG-go vs kernel-module decision) -- nothing wires into live traffic yet, that's Phase 2 (relay into Xray's own SOCKS5 inbound). Covered by three real end-to-end tests: a genuine handshake + TCP forwarder + identity resolution, the same for UDP (including a reply routed back through the tunnel), and the manager's reconfigure-in-place vs. rebuild lifecycle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
181 lines
6.0 KiB
Go
181 lines
6.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"
|
|
)
|
|
|
|
// Desired pairs an amneziawg.Instance (the shared, DB-backed shape
|
|
// internal/amneziawg's own kernel-module Manager also reconciles toward)
|
|
// with this package's own embedded-only DeviceOptions -- the AWG 3.0 fields
|
|
// that shared type doesn't carry, see DeviceOptions' doc comment.
|
|
type Desired struct {
|
|
Instance amneziawg.Instance
|
|
Options DeviceOptions
|
|
}
|
|
|
|
// managed is one running embedded interface: the live Device, the peer
|
|
// lookup index built from its current peer list, and enough of its own
|
|
// configuration to decide whether a later Ensure call can reconfigure it in
|
|
// place or needs to rebuild it from scratch.
|
|
type managed struct {
|
|
dev *Device
|
|
peers *PeerIndex
|
|
inst amneziawg.Instance
|
|
structFP string
|
|
}
|
|
|
|
// Manager owns the set of running embedded AmneziaWG interfaces, keyed by
|
|
// inbound id -- the same shape as internal/amneziawg.Manager (GetManager()
|
|
// + sync.Once, mu-guarded map, Ensure/Reconcile/StopAll/HasRunning), so a
|
|
// caller already familiar with that Manager needs to learn nothing new here.
|
|
// Unlike that Manager, this one doesn't attach any traffic handling by
|
|
// itself: Ensure/Reconcile only bring each Instance's Device up to date.
|
|
// Attaching a forwarder/UDP handler (see forwarder.go / udp.go) using the
|
|
// Device and PeerIndex returned by Lookup is left to the caller -- today a
|
|
// test harness, later the Phase 2 SOCKS5 relay wiring -- since this package
|
|
// doesn't yet know what that handler should do with a recovered connection.
|
|
type Manager struct {
|
|
mu sync.Mutex
|
|
ifaces map[int]*managed
|
|
}
|
|
|
|
var (
|
|
managerOnce sync.Once
|
|
manager *Manager
|
|
)
|
|
|
|
// GetManager returns the process-wide embedded-AmneziaWG manager singleton.
|
|
func GetManager() *Manager {
|
|
managerOnce.Do(func() {
|
|
manager = &Manager{ifaces: map[int]*managed{}}
|
|
})
|
|
return manager
|
|
}
|
|
|
|
// Ensure brings inbound d.Instance.Id's embedded interface to the state
|
|
// d describes, creating it if it doesn't exist yet. A no-op only when
|
|
// nothing has changed since the last successful Ensure/Reconcile.
|
|
func (m *Manager) Ensure(d Desired) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.ensureLocked(d)
|
|
}
|
|
|
|
// ensureLocked decides between three actions: nothing changed since the
|
|
// last apply (skip entirely); only peers/obfuscation/keys/listen_port
|
|
// changed (reconfigure the existing Device in place via IpcSet, which
|
|
// already sends replace_peers=true -- see buildUAPIConfig -- so removed
|
|
// peers are dropped correctly without a full rebuild); or the interface's
|
|
// own address(es)/MTU changed (these are fixed at netstack-construction
|
|
// time, so the only option is closing the old Device and building a fresh
|
|
// one). This is a coarser split than internal/amneziawg's own three-tier
|
|
// noop/reload/restart fingerprinting (that one also tracks host-side
|
|
// TPROXY/NDP rules this embedded path has no equivalent of) -- correct and
|
|
// sufficient for Phase 1; revisit only if reconcile frequency at real scale
|
|
// makes the address/MTU rebuild path worth avoiding too.
|
|
func (m *Manager) ensureLocked(d Desired) error {
|
|
inst, opts := d.Instance, d.Options
|
|
structFP := addressFingerprint(inst)
|
|
|
|
cur, exists := m.ifaces[inst.Id]
|
|
if exists && cur.structFP == structFP {
|
|
conf, err := buildUAPIConfig(inst, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("amneziawgnet: %w", err)
|
|
}
|
|
if err := cur.dev.IpcSet(conf); err != nil {
|
|
return fmt.Errorf("amneziawgnet: reconfigure inbound %d: %w", inst.Id, err)
|
|
}
|
|
cur.peers = NewPeerIndex(inst.Peers)
|
|
cur.inst = inst
|
|
return nil
|
|
}
|
|
|
|
if exists {
|
|
cur.dev.Close()
|
|
delete(m.ifaces, inst.Id)
|
|
}
|
|
dev, err := NewDevice(inst, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.ifaces[inst.Id] = &managed{
|
|
dev: dev,
|
|
peers: NewPeerIndex(inst.Peers),
|
|
inst: inst,
|
|
structFP: structFP,
|
|
}
|
|
logger.Infof("amneziawgnet: started embedded interface %s for inbound %d", inst.InterfaceName, inst.Id)
|
|
return nil
|
|
}
|
|
|
|
// addressFingerprint captures the two Instance fields that can't be changed
|
|
// on a running Device via IpcSet alone (they're fixed when the gVisor
|
|
// netstack is built) -- everything else (keys, listen port, obfuscation,
|
|
// AWG 3.0 options, peers) amneziawg-go's own UAPI can hot-reconfigure.
|
|
func addressFingerprint(inst amneziawg.Instance) string {
|
|
return fmt.Sprintf("%d|%s", inst.MTU, strings.Join(inst.Address, ","))
|
|
}
|
|
|
|
// Reconcile brings every desired instance's embedded interface up to date
|
|
// and stops any managed interface whose inbound is no longer desired --
|
|
// mirroring internal/amneziawg.Manager.Reconcile's per-tick contract.
|
|
func (m *Manager) Reconcile(desired []Desired) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
want := make(map[int]struct{}, len(desired))
|
|
for _, d := range desired {
|
|
want[d.Instance.Id] = struct{}{}
|
|
}
|
|
for id, cur := range m.ifaces {
|
|
if _, ok := want[id]; ok {
|
|
continue
|
|
}
|
|
cur.dev.Close()
|
|
delete(m.ifaces, id)
|
|
logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
|
|
}
|
|
for _, d := range desired {
|
|
if err := m.ensureLocked(d); err != nil {
|
|
logger.Warningf("amneziawgnet: reconcile failed for inbound %d: %v", d.Instance.Id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// StopAll tears down every managed interface. Called on panel shutdown.
|
|
func (m *Manager) StopAll() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for id, cur := range m.ifaces {
|
|
cur.dev.Close()
|
|
delete(m.ifaces, id)
|
|
}
|
|
}
|
|
|
|
// HasRunning reports whether any embedded interface is currently managed.
|
|
func (m *Manager) HasRunning() bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.ifaces) > 0
|
|
}
|
|
|
|
// Lookup returns the running Device and PeerIndex for inbound id, if any --
|
|
// for a caller that wants to attach its own forwarder/handler (a test
|
|
// harness today, the Phase 2 SOCKS5 relay wiring later) once the interface
|
|
// is up.
|
|
func (m *Manager) Lookup(id int) (dev *Device, peers *PeerIndex, ok bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
cur, exists := m.ifaces[id]
|
|
if !exists {
|
|
return nil, nil, false
|
|
}
|
|
return cur.dev, cur.peers, true
|
|
}
|