| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- package amneziawgnet
- import (
- "fmt"
- "net/netip"
- "os"
- "strings"
- "sync"
- "github.com/amnezia-vpn/amneziawg-go/v3/device"
- "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
- "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
- "github.com/mhsanaei/3x-ui/v3/internal/logger"
- )
- // verboseLoggerIfEnabled returns a real amneziawg-go verbose logger (real
- // handshake/keepalive/decrypt-error diagnostics -- the device is otherwise
- // completely silent by design, see DeviceOptions' own doc comment) when the
- // AMNEZIAWGNET_DEBUG environment variable is set to any non-empty value,
- // nil otherwise (NewDevice's own default -- LogLevelSilent -- applies).
- // Deliberately opt-in and env-var-gated rather than a permanent log-level
- // setting: this device's own protocol-level logging has no per-peer
- // filtering, so enabling it on a busy real inbound would be noisy; it's
- // meant for exactly this kind of "why did this one handshake go quiet"
- // investigation on a low-traffic box.
- func verboseLoggerIfEnabled(inboundID int) *device.Logger {
- if os.Getenv("AMNEZIAWGNET_DEBUG") == "" {
- return nil
- }
- return device.NewLogger(device.LogLevelVerbose, fmt.Sprintf("(awg#%d) ", inboundID))
- }
- // Desired pairs an amneziawg.Instance (the shared, DB-backed shape) with
- // this package's embedded-only DeviceOptions -- see DeviceOptions' doc.
- type Desired struct {
- Instance amneziawg.Instance
- Options DeviceOptions
- }
- // managed is one running embedded interface: the live Device, its UDP relay
- // sessions, its open per-client port-forward listeners, 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
- udpRelay *UDPRelay
- portForwards *PortForwardSet
- peers *PeerIndex
- inst amneziawg.Instance
- structFP string
- uapiConfig string
- }
- // Manager owns the set of running embedded AmneziaWG interfaces, keyed by
- // inbound id -- the same shape as internal/mtproto.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.
- // Every Device this Manager builds gets its TCP forwarder and UDP handler
- // attached automatically (see ensureLocked), relaying into that instance's
- // own loopback SOCKS5 inbound (SOCKSPortForInbound/SocksPassword) -- a
- // caller only needs to keep calling Ensure/Reconcile with fresh Instance
- // data; it doesn't need to know relay.go exists at all.
- 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 -- this is the common case on every 10s
- // reconcile tick when no admin edit happened, and it MUST actually skip the
- // IpcSet call, not just look like it should: amneziawg-go's IpcSet always
- // includes replace_peers=true -- see buildUAPIConfig -- and its own
- // implementation of that op is device.RemoveAllPeers(), unconditionally,
- // even when the new peer list is byte-identical to the old one. A real
- // production bug, found via a live test connection that reset every ~10s:
- // calling IpcSet on every tick regardless of whether anything changed was
- // tearing down every peer's live handshake/session state on every single
- // reconcile, so no connection could ever survive past one tick); only
- // peers/obfuscation/keys/listen_port changed (reconfigure the existing
- // Device in place via IpcSet); 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).
- func (m *Manager) ensureLocked(d Desired) error {
- inst, opts := d.Instance, d.Options
- if opts.Logger == nil {
- opts.Logger = verboseLoggerIfEnabled(inst.Id)
- }
- structFP := addressFingerprint(inst)
- cur, exists := m.ifaces[inst.Id]
- // Captured before either branch below: peers/AllowedIPs can change
- // (and so can each peer's IPv6 alias) without the address/MTU
- // fingerprint changing at all, so both the reconfigure-in-place branch
- // and the rebuild branch need to diff IPv6 aliases against whatever
- // this id had before, not just on a rebuild.
- var oldInst amneziawg.Instance
- if exists {
- oldInst = cur.inst
- }
- if exists && cur.structFP == structFP {
- conf, err := buildUAPIConfig(inst, opts)
- if err != nil {
- return fmt.Errorf("amneziawgnet: %w", err)
- }
- // True no-op: the rendered UAPI config -- which already covers every
- // field IpcSet can act on (keys, listen port, obfuscation, AWG 3.0
- // options, the full peer list) -- is byte-identical to what's
- // already live. Comparing the rendered string instead of inst
- // directly means this can never drift out of sync with whatever
- // buildUAPIConfig actually reads, the way a hand-maintained field
- // list could.
- if conf == cur.uapiConfig {
- cur.peers = NewPeerIndex(inst.Peers)
- cur.inst = inst
- 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
- // branch -- without this call, that edit would silently never
- // open/close a listener until some unrelated change also
- // happened to touch this inbound. See
- // TestForwardedPortsOnlyChangeStillReconcilesPortForwards.
- cur.portForwards.Reconcile(inst)
- return nil
- }
- 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
- cur.uapiConfig = conf
- applyV6Aliases(diffV6Aliases(oldInst, inst))
- cur.portForwards.Reconcile(inst)
- return nil
- }
- if exists {
- cur.udpRelay.Close()
- cur.portForwards.Close()
- cur.dev.Close()
- delete(m.ifaces, inst.Id)
- }
- dev, err := newUnconfiguredDevice(inst, opts)
- if err != nil {
- return err
- }
- relay := socksRelayForInstance(inst)
- udpRelay := NewUDPRelay(relay, dev.Stack)
- portForwards := NewPortForwardSet(dev.Stack, inst.Id)
- inboundID := inst.Id // captured for the closures below, which outlive this call
- AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
- srcAddrPort, err := netip.ParseAddrPort(conn.RemoteAddr().String())
- if err != nil {
- conn.Close()
- return
- }
- // Re-fetched on every connection, not captured once at attach time:
- // a reconfigure-in-place (peers added/removed, no rebuild) replaces
- // cur.peers without ever re-attaching the forwarder, so a stale
- // captured index would silently miss newly-added peers.
- _, peers, ok := m.Lookup(inboundID)
- if !ok {
- conn.Close()
- return
- }
- peer, ok := peers.Lookup(srcAddrPort.Addr().Unmap())
- if !ok {
- conn.Close()
- return
- }
- relay.RelayTCP(conn, peer.Email, dest)
- })
- AttachUDPHandler(dev.Stack, func(src, dst netip.AddrPort, payload []byte) {
- _, peers, ok := m.Lookup(inboundID)
- if !ok {
- return
- }
- peer, ok := peers.Lookup(src.Addr())
- if !ok {
- return
- }
- udpRelay.Handle(src, dst, peer.Email, payload)
- })
- // Handlers are registered on dev.Stack above, BEFORE Configure's IpcSet
- // can start any peer's receive goroutine -- see newUnconfiguredDevice's
- // doc comment for why this order (not convenience) is what makes this
- // race-free.
- if err := dev.Configure(inst, opts); err != nil {
- udpRelay.Close()
- portForwards.Close()
- return err
- }
- // dev.Configure already rendered and applied this exact config
- // internally; recomputing it here (cheap, pure, guaranteed to succeed
- // since Configure just proved these inputs are valid) is simpler than
- // threading the string back out of Configure's own signature, and gives
- // the no-op check above a correct baseline to compare the next tick
- // against instead of an empty string.
- conf, _ := buildUAPIConfig(inst, opts)
- m.ifaces[inst.Id] = &managed{
- dev: dev,
- udpRelay: udpRelay,
- portForwards: portForwards,
- peers: NewPeerIndex(inst.Peers),
- inst: inst,
- structFP: structFP,
- uapiConfig: conf,
- }
- applyV6Aliases(diffV6Aliases(oldInst, inst))
- portForwards.Reconcile(inst)
- logger.Infof("amneziawgnet: started embedded interface %s for inbound %d", inst.InterfaceName, inst.Id)
- return nil
- }
- // socksRelayForInstance derives the loopback SOCKS5 relay address/password
- // for inst -- both fully determined by its id and the process-wide
- // password (SOCKSPortForInbound/SocksPassword), so no per-instance state
- // needs threading through Desired/DeviceOptions for this.
- func socksRelayForInstance(inst amneziawg.Instance) SocksRelay {
- return SocksRelay{
- Addr: fmt.Sprintf("127.0.0.1:%d", SOCKSPortForInbound(inst.Id)),
- Password: SocksPassword(),
- }
- }
- // 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/mtproto.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
- }
- applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
- cur.udpRelay.Close()
- cur.portForwards.Close()
- 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)
- }
- }
- }
- // Remove tears down inbound id's embedded interface, if any -- mirrors
- // internal/mtproto.Manager.Remove, for a caller that needs to drop a
- // single inbound outside a full Reconcile pass (e.g. the immediate-apply
- // CRUD path in internal/web/runtime/local.go).
- func (m *Manager) Remove(id int) {
- m.mu.Lock()
- defer m.mu.Unlock()
- cur, exists := m.ifaces[id]
- if !exists {
- return
- }
- applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
- cur.udpRelay.Close()
- cur.portForwards.Close()
- cur.dev.Close()
- delete(m.ifaces, id)
- logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
- }
- // 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 {
- applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
- cur.udpRelay.Close()
- cur.portForwards.Close()
- 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 --
- // the forwarder/UDP-handler closures ensureLocked attaches use this to
- // re-fetch the current peer index on every connection (see ensureLocked's
- // comment on why), and it's equally available to a test harness or any
- // other caller that wants read access to a managed interface's state.
- 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
- }
|