manager.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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)/effective
  116. // MTU changed -- S4 counts, the default MTU derives from it (these are fixed
  117. // at netstack-construction time, so the only option is closing the old
  118. // Device and building a fresh one).
  119. func (m *Manager) ensureLocked(d Desired) error {
  120. inst, opts := d.Instance, d.Options
  121. if opts.Logger == nil {
  122. opts.Logger = verboseLoggerIfEnabled(inst.Id)
  123. }
  124. structFP := addressFingerprint(inst)
  125. cur, exists := m.ifaces[inst.Id]
  126. // Captured before either branch below: peers/AllowedIPs can change
  127. // (and so can each peer's IPv6 alias) without the address/MTU
  128. // fingerprint changing at all, so both the reconfigure-in-place branch
  129. // and the rebuild branch need to diff IPv6 aliases against whatever
  130. // this id had before, not just on a rebuild.
  131. var oldInst amneziawg.Instance
  132. if exists {
  133. oldInst = cur.inst
  134. }
  135. if exists && cur.structFP == structFP {
  136. conf, err := buildUAPIConfig(inst, opts)
  137. if err != nil {
  138. return fmt.Errorf("amneziawgnet: %w", err)
  139. }
  140. // True no-op: the rendered UAPI config -- which already covers every
  141. // field IpcSet can act on (keys, listen port, obfuscation, AWG 3.0
  142. // options, the full peer list) -- is byte-identical to what's
  143. // already live. Comparing the rendered string instead of inst
  144. // directly means this can never drift out of sync with whatever
  145. // buildUAPIConfig actually reads, the way a hand-maintained field
  146. // list could.
  147. if conf == cur.uapiConfig {
  148. cur.peers.Store(NewPeerIndex(inst.Peers))
  149. cur.inst = inst
  150. applyV6Aliases(diffV6Aliases(oldInst, inst))
  151. // buildUAPIConfig never reads ForwardedPorts (it's a panel-level
  152. // concept, not a WireGuard UAPI field), so a ForwardedPorts-only
  153. // edit renders byte-identical here and takes this exact no-op
  154. // branch -- without this call, that edit would silently never
  155. // open/close a listener until some unrelated change also
  156. // happened to touch this inbound. See
  157. // TestForwardedPortsOnlyChangeStillReconcilesPortForwards.
  158. cur.portForwards.Reconcile(inst)
  159. return nil
  160. }
  161. if err := cur.dev.IpcSet(conf); err != nil {
  162. return fmt.Errorf("amneziawgnet: reconfigure inbound %d: %w", inst.Id, err)
  163. }
  164. cur.peers.Store(NewPeerIndex(inst.Peers))
  165. cur.inst = inst
  166. cur.uapiConfig = conf
  167. applyV6Aliases(diffV6Aliases(oldInst, inst))
  168. cur.portForwards.Reconcile(inst)
  169. return nil
  170. }
  171. if exists {
  172. cur.close()
  173. delete(m.ifaces, inst.Id)
  174. }
  175. dev, err := newUnconfiguredDevice(inst, opts)
  176. if err != nil {
  177. return err
  178. }
  179. relay := socksRelayForInstance(inst)
  180. udpRelay := NewUDPRelay(relay, dev.Stack)
  181. portForwards := NewPortForwardSet(dev.Stack, inst.Id)
  182. next := &managed{
  183. dev: dev,
  184. udpRelay: udpRelay,
  185. portForwards: portForwards,
  186. inst: inst,
  187. structFP: structFP,
  188. }
  189. next.peers.Store(NewPeerIndex(inst.Peers))
  190. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  191. srcAddrPort, err := netip.ParseAddrPort(conn.RemoteAddr().String())
  192. if err != nil {
  193. conn.Close()
  194. return
  195. }
  196. // Reload for every connection: in-place reconfiguration swaps the peer
  197. // index without reattaching handlers and may hold the lifecycle lock.
  198. peer, ok := next.lookupPeer(srcAddrPort.Addr().Unmap())
  199. if !ok {
  200. conn.Close()
  201. return
  202. }
  203. relay.RelayTCP(conn, peer.Email, dest)
  204. })
  205. AttachUDPHandler(dev.Stack, next.handleUDP)
  206. // Handlers are registered on dev.Stack above, BEFORE Configure's IpcSet
  207. // can start any peer's receive goroutine -- see newUnconfiguredDevice's
  208. // doc comment for why this order (not convenience) is what makes this
  209. // race-free.
  210. if err := dev.Configure(inst, opts); err != nil {
  211. udpRelay.Close()
  212. portForwards.Close()
  213. return err
  214. }
  215. // dev.Configure already rendered and applied this exact config
  216. // internally; recomputing it here (cheap, pure, guaranteed to succeed
  217. // since Configure just proved these inputs are valid) is simpler than
  218. // threading the string back out of Configure's own signature, and gives
  219. // the no-op check above a correct baseline to compare the next tick
  220. // against instead of an empty string.
  221. conf, _ := buildUAPIConfig(inst, opts)
  222. next.uapiConfig = conf
  223. m.ifaces[inst.Id] = next
  224. applyV6Aliases(diffV6Aliases(oldInst, inst))
  225. portForwards.Reconcile(inst)
  226. logger.Infof("amneziawgnet: started embedded interface %s for inbound %d", inst.InterfaceName, inst.Id)
  227. return nil
  228. }
  229. // socksRelayForInstance derives the loopback SOCKS5 relay address/password
  230. // for inst -- both fully determined by its id and the process-wide
  231. // password (SOCKSPortForInbound/SocksPassword), so no per-instance state
  232. // needs threading through Desired/DeviceOptions for this.
  233. func socksRelayForInstance(inst amneziawg.Instance) SocksRelay {
  234. return SocksRelay{
  235. Addr: fmt.Sprintf("127.0.0.1:%d", SOCKSPortForInbound(inst.Id)),
  236. Password: SocksPassword(),
  237. }
  238. }
  239. // addressFingerprint captures what IpcSet can't change on a running Device,
  240. // fixed when the netstack is built: address, and the S4-derived effective MTU.
  241. func addressFingerprint(inst amneziawg.Instance) string {
  242. return fmt.Sprintf("%d|%s",
  243. amneziawg.EffectiveMTU(inst.MTU, inst.Obfuscation.S4),
  244. strings.Join(inst.Address, ","))
  245. }
  246. // Reconcile brings every desired instance's embedded interface up to date
  247. // and stops any managed interface whose inbound is no longer desired --
  248. // mirroring internal/mtproto.Manager.Reconcile's per-tick contract.
  249. func (m *Manager) Reconcile(desired []Desired) {
  250. m.mu.Lock()
  251. defer m.mu.Unlock()
  252. want := make(map[int]struct{}, len(desired))
  253. for _, d := range desired {
  254. want[d.Instance.Id] = struct{}{}
  255. }
  256. for id, cur := range m.ifaces {
  257. if _, ok := want[id]; ok {
  258. continue
  259. }
  260. applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
  261. cur.close()
  262. delete(m.ifaces, id)
  263. logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
  264. }
  265. for _, d := range desired {
  266. if err := m.ensureLocked(d); err != nil {
  267. logger.Warningf("amneziawgnet: reconcile failed for inbound %d: %v", d.Instance.Id, err)
  268. }
  269. }
  270. }
  271. // Remove tears down inbound id's embedded interface, if any -- mirrors
  272. // internal/mtproto.Manager.Remove, for a caller that needs to drop a
  273. // single inbound outside a full Reconcile pass (e.g. the immediate-apply
  274. // CRUD path in internal/web/runtime/local.go).
  275. func (m *Manager) Remove(id int) {
  276. m.mu.Lock()
  277. defer m.mu.Unlock()
  278. cur, exists := m.ifaces[id]
  279. if !exists {
  280. return
  281. }
  282. applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
  283. cur.close()
  284. delete(m.ifaces, id)
  285. logger.Infof("amneziawgnet: stopped embedded interface for removed inbound %d", id)
  286. }
  287. // StopAll tears down every managed interface. Called on panel shutdown.
  288. func (m *Manager) StopAll() {
  289. m.mu.Lock()
  290. defer m.mu.Unlock()
  291. for id, cur := range m.ifaces {
  292. applyV6Aliases(diffV6Aliases(cur.inst, amneziawg.Instance{}))
  293. cur.close()
  294. delete(m.ifaces, id)
  295. }
  296. }
  297. // HasRunning reports whether any embedded interface is currently managed.
  298. func (m *Manager) HasRunning() bool {
  299. m.mu.Lock()
  300. defer m.mu.Unlock()
  301. return len(m.ifaces) > 0
  302. }
  303. // Lookup returns the running device and current peer snapshot for diagnostics,
  304. // tests, and other callers outside the packet-delivery path.
  305. func (m *Manager) Lookup(id int) (dev *Device, peers *PeerIndex, ok bool) {
  306. m.mu.Lock()
  307. defer m.mu.Unlock()
  308. cur, exists := m.ifaces[id]
  309. if !exists {
  310. return nil, nil, false
  311. }
  312. return cur.dev, cur.peers.Load(), true
  313. }