manager.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. package amneziawgnet
  2. import (
  3. "fmt"
  4. "net/netip"
  5. "os"
  6. "strings"
  7. "sync"
  8. "sync/atomic"
  9. "github.com/amnezia-vpn/amneziawg-go/v3/device"
  10. "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
  11. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  12. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  13. )
  14. // verboseLoggerIfEnabled returns a real amneziawg-go verbose logger (real
  15. // handshake/keepalive/decrypt-error diagnostics -- the device is otherwise
  16. // completely silent by design, see DeviceOptions' own doc comment) when the
  17. // AMNEZIAWGNET_DEBUG environment variable is set to any non-empty value,
  18. // nil otherwise (NewDevice's own default -- LogLevelSilent -- applies).
  19. // Deliberately opt-in and env-var-gated rather than a permanent log-level
  20. // setting: this device's own protocol-level logging has no per-peer
  21. // filtering, so enabling it on a busy real inbound would be noisy; it's
  22. // meant for exactly this kind of "why did this one handshake go quiet"
  23. // investigation on a low-traffic box.
  24. func verboseLoggerIfEnabled(inboundID int) *device.Logger {
  25. if os.Getenv("AMNEZIAWGNET_DEBUG") == "" {
  26. return nil
  27. }
  28. return device.NewLogger(device.LogLevelVerbose, fmt.Sprintf("(awg#%d) ", inboundID))
  29. }
  30. // Desired pairs an amneziawg.Instance (the shared, DB-backed shape) with
  31. // this package's embedded-only DeviceOptions -- see DeviceOptions' doc.
  32. type Desired struct {
  33. Instance amneziawg.Instance
  34. Options DeviceOptions
  35. }
  36. // managed is one running embedded interface: the live Device, its UDP relay
  37. // sessions, its open per-client port-forward listeners, the peer lookup
  38. // index built from its current peer list, and enough of its own
  39. // configuration to decide whether a later Ensure call can reconfigure it in
  40. // place or needs to rebuild it from scratch.
  41. type managed struct {
  42. dev *Device
  43. udpRelay *UDPRelay
  44. portForwards *PortForwardSet
  45. peers atomic.Pointer[PeerIndex]
  46. inst amneziawg.Instance
  47. structFP string
  48. uapiConfig string
  49. }
  50. func (m *managed) lookupPeer(addr netip.Addr) (amneziawg.Peer, bool) {
  51. peers := m.peers.Load()
  52. if peers == nil {
  53. return amneziawg.Peer{}, false
  54. }
  55. return peers.Lookup(addr)
  56. }
  57. func (m *managed) handleUDP(src, dst netip.AddrPort, payload []byte) {
  58. peer, ok := m.lookupPeer(src.Addr())
  59. if !ok {
  60. return
  61. }
  62. m.udpRelay.Handle(src, dst, peer.Email, payload)
  63. }
  64. func (m *managed) close() {
  65. m.portForwards.Close()
  66. // Stop packet delivery before closing the relay so an in-flight handler
  67. // cannot publish a new session after the relay has already been swept.
  68. m.dev.Close()
  69. m.udpRelay.Close()
  70. }
  71. // Manager owns the set of running embedded AmneziaWG interfaces, keyed by
  72. // inbound id -- the same shape as internal/mtproto.Manager (GetManager()
  73. // + sync.Once, mu-guarded map, Ensure/Reconcile/StopAll/HasRunning), so a
  74. // caller already familiar with that Manager needs to learn nothing new here.
  75. // Every Device this Manager builds gets its TCP forwarder and UDP handler
  76. // attached automatically (see ensureLocked), relaying into that instance's
  77. // own loopback SOCKS5 inbound (SOCKSPortForInbound/SocksPassword) -- a
  78. // caller only needs to keep calling Ensure/Reconcile with fresh Instance
  79. // data; it doesn't need to know relay.go exists at all.
  80. type Manager struct {
  81. mu sync.Mutex
  82. ifaces map[int]*managed
  83. }
  84. var (
  85. managerOnce sync.Once
  86. manager *Manager
  87. )
  88. // GetManager returns the process-wide embedded-AmneziaWG manager singleton.
  89. func GetManager() *Manager {
  90. managerOnce.Do(func() {
  91. manager = &Manager{ifaces: map[int]*managed{}}
  92. })
  93. return manager
  94. }
  95. // Ensure brings inbound d.Instance.Id's embedded interface to the state
  96. // d describes, creating it if it doesn't exist yet. A no-op only when
  97. // nothing has changed since the last successful Ensure/Reconcile.
  98. func (m *Manager) Ensure(d Desired) error {
  99. m.mu.Lock()
  100. defer m.mu.Unlock()
  101. return m.ensureLocked(d)
  102. }
  103. // ensureLocked decides between three actions: nothing changed since the
  104. // last apply (skip entirely -- this is the common case on every 10s
  105. // reconcile tick when no admin edit happened, and it MUST actually skip the
  106. // IpcSet call, not just look like it should: amneziawg-go's IpcSet always
  107. // includes replace_peers=true -- see buildUAPIConfig -- and its own
  108. // implementation of that op is device.RemoveAllPeers(), unconditionally,
  109. // even when the new peer list is byte-identical to the old one. A real
  110. // production bug, found via a live test connection that reset every ~10s:
  111. // calling IpcSet on every tick regardless of whether anything changed was
  112. // tearing down every peer's live handshake/session state on every single
  113. // reconcile, so no connection could ever survive past one tick); only
  114. // peers/obfuscation/keys/listen_port changed (reconfigure the existing
  115. // Device in place via IpcSet); or the interface's own address(es)/MTU
  116. // changed (these are fixed at netstack-construction time, so the only
  117. // option is closing the old Device and building a fresh one).
  118. func (m *Manager) ensureLocked(d Desired) error {
  119. inst, opts := d.Instance, d.Options
  120. if opts.Logger == nil {
  121. opts.Logger = verboseLoggerIfEnabled(inst.Id)
  122. }
  123. structFP := addressFingerprint(inst)
  124. cur, exists := m.ifaces[inst.Id]
  125. // Captured before either branch below: peers/AllowedIPs can change
  126. // (and so can each peer's IPv6 alias) without the address/MTU
  127. // fingerprint changing at all, so both the reconfigure-in-place branch
  128. // and the rebuild branch need to diff IPv6 aliases against whatever
  129. // this id had before, not just on a rebuild.
  130. var oldInst amneziawg.Instance
  131. if exists {
  132. oldInst = cur.inst
  133. }
  134. if exists && cur.structFP == structFP {
  135. conf, err := buildUAPIConfig(inst, opts)
  136. if err != nil {
  137. return fmt.Errorf("amneziawgnet: %w", err)
  138. }
  139. // True no-op: the rendered UAPI config -- which already covers every
  140. // field IpcSet can act on (keys, listen port, obfuscation, AWG 3.0
  141. // options, the full peer list) -- is byte-identical to what's
  142. // already live. Comparing the rendered string instead of inst
  143. // directly means this can never drift out of sync with whatever
  144. // buildUAPIConfig actually reads, the way a hand-maintained field
  145. // list could.
  146. if conf == cur.uapiConfig {
  147. cur.peers.Store(NewPeerIndex(inst.Peers))
  148. cur.inst = inst
  149. applyV6Aliases(diffV6Aliases(oldInst, inst))
  150. // buildUAPIConfig never reads ForwardedPorts (it's a panel-level
  151. // concept, not a WireGuard UAPI field), so a ForwardedPorts-only
  152. // edit renders byte-identical here and takes this exact no-op
  153. // branch -- without this call, that edit would silently never
  154. // open/close a listener until some unrelated change also
  155. // happened to touch this inbound. See
  156. // TestForwardedPortsOnlyChangeStillReconcilesPortForwards.
  157. cur.portForwards.Reconcile(inst)
  158. return nil
  159. }
  160. if err := cur.dev.IpcSet(conf); err != nil {
  161. return fmt.Errorf("amneziawgnet: reconfigure inbound %d: %w", inst.Id, err)
  162. }
  163. cur.peers.Store(NewPeerIndex(inst.Peers))
  164. cur.inst = inst
  165. cur.uapiConfig = conf
  166. applyV6Aliases(diffV6Aliases(oldInst, inst))
  167. cur.portForwards.Reconcile(inst)
  168. return nil
  169. }
  170. if exists {
  171. cur.close()
  172. delete(m.ifaces, inst.Id)
  173. }
  174. dev, err := newUnconfiguredDevice(inst, opts)
  175. if err != nil {
  176. return err
  177. }
  178. relay := socksRelayForInstance(inst)
  179. udpRelay := NewUDPRelay(relay, dev.Stack)
  180. portForwards := NewPortForwardSet(dev.Stack, inst.Id)
  181. next := &managed{
  182. dev: dev,
  183. udpRelay: udpRelay,
  184. portForwards: portForwards,
  185. inst: inst,
  186. structFP: structFP,
  187. }
  188. next.peers.Store(NewPeerIndex(inst.Peers))
  189. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  190. srcAddrPort, err := netip.ParseAddrPort(conn.RemoteAddr().String())
  191. if err != nil {
  192. conn.Close()
  193. return
  194. }
  195. // Reload for every connection: in-place reconfiguration swaps the peer
  196. // index without reattaching handlers and may hold the lifecycle lock.
  197. peer, ok := next.lookupPeer(srcAddrPort.Addr().Unmap())
  198. if !ok {
  199. conn.Close()
  200. return
  201. }
  202. relay.RelayTCP(conn, peer.Email, dest)
  203. })
  204. AttachUDPHandler(dev.Stack, next.handleUDP)
  205. // Handlers are registered on dev.Stack above, BEFORE Configure's IpcSet
  206. // can start any peer's receive goroutine -- see newUnconfiguredDevice's
  207. // doc comment for why this order (not convenience) is what makes this
  208. // race-free.
  209. if err := dev.Configure(inst, opts); err != nil {
  210. udpRelay.Close()
  211. portForwards.Close()
  212. return err
  213. }
  214. // dev.Configure already rendered and applied this exact config
  215. // internally; recomputing it here (cheap, pure, guaranteed to succeed
  216. // since Configure just proved these inputs are valid) is simpler than
  217. // threading the string back out of Configure's own signature, and gives
  218. // the no-op check above a correct baseline to compare the next tick
  219. // against instead of an empty string.
  220. conf, _ := buildUAPIConfig(inst, opts)
  221. next.uapiConfig = conf
  222. m.ifaces[inst.Id] = next
  223. applyV6Aliases(diffV6Aliases(oldInst, inst))
  224. portForwards.Reconcile(inst)
  225. logger.Infof("amneziawgnet: started embedded interface %s for inbound %d", inst.InterfaceName, inst.Id)
  226. return nil
  227. }
  228. // socksRelayForInstance derives the loopback SOCKS5 relay address/password
  229. // for inst -- both fully determined by its id and the process-wide
  230. // password (SOCKSPortForInbound/SocksPassword), so no per-instance state
  231. // needs threading through Desired/DeviceOptions for this.
  232. func socksRelayForInstance(inst amneziawg.Instance) SocksRelay {
  233. return SocksRelay{
  234. Addr: fmt.Sprintf("127.0.0.1:%d", SOCKSPortForInbound(inst.Id)),
  235. Password: SocksPassword(),
  236. }
  237. }
  238. // addressFingerprint captures the two Instance fields that can't be changed
  239. // on a running Device via IpcSet alone (they're fixed when the gVisor
  240. // netstack is built) -- everything else (keys, listen port, obfuscation,
  241. // AWG 3.0 options, peers) amneziawg-go's own UAPI can hot-reconfigure.
  242. func addressFingerprint(inst amneziawg.Instance) string {
  243. return fmt.Sprintf("%d|%s", inst.MTU, strings.Join(inst.Address, ","))
  244. }
  245. // Reconcile brings every desired instance's embedded interface up to date
  246. // and stops any managed interface whose inbound is no longer desired --
  247. // mirroring internal/mtproto.Manager.Reconcile's per-tick contract.
  248. func (m *Manager) Reconcile(desired []Desired) {
  249. m.mu.Lock()
  250. defer m.mu.Unlock()
  251. want := make(map[int]struct{}, len(desired))
  252. for _, d := range desired {
  253. want[d.Instance.Id] = struct{}{}
  254. }
  255. for id, cur := range m.ifaces {
  256. if _, ok := want[id]; ok {
  257. continue
  258. }
  259. applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
  260. cur.close()
  261. delete(m.ifaces, id)
  262. logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
  263. }
  264. for _, d := range desired {
  265. if err := m.ensureLocked(d); err != nil {
  266. logger.Warningf("amneziawgnet: reconcile failed for inbound %d: %v", d.Instance.Id, err)
  267. }
  268. }
  269. }
  270. // Remove tears down inbound id's embedded interface, if any -- mirrors
  271. // internal/mtproto.Manager.Remove, for a caller that needs to drop a
  272. // single inbound outside a full Reconcile pass (e.g. the immediate-apply
  273. // CRUD path in internal/web/runtime/local.go).
  274. func (m *Manager) Remove(id int) {
  275. m.mu.Lock()
  276. defer m.mu.Unlock()
  277. cur, exists := m.ifaces[id]
  278. if !exists {
  279. return
  280. }
  281. applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
  282. cur.close()
  283. delete(m.ifaces, id)
  284. logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
  285. }
  286. // StopAll tears down every managed interface. Called on panel shutdown.
  287. func (m *Manager) StopAll() {
  288. m.mu.Lock()
  289. defer m.mu.Unlock()
  290. for id, cur := range m.ifaces {
  291. applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
  292. cur.close()
  293. delete(m.ifaces, id)
  294. }
  295. }
  296. // HasRunning reports whether any embedded interface is currently managed.
  297. func (m *Manager) HasRunning() bool {
  298. m.mu.Lock()
  299. defer m.mu.Unlock()
  300. return len(m.ifaces) > 0
  301. }
  302. // Lookup returns the running device and current peer snapshot for diagnostics,
  303. // tests, and other callers outside the packet-delivery path.
  304. func (m *Manager) Lookup(id int) (dev *Device, peers *PeerIndex, ok bool) {
  305. m.mu.Lock()
  306. defer m.mu.Unlock()
  307. cur, exists := m.ifaces[id]
  308. if !exists {
  309. return nil, nil, false
  310. }
  311. return cur.dev, cur.peers.Load(), true
  312. }