manager.go 12 KB

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