manager_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. package amneziawgnet
  2. import (
  3. "fmt"
  4. "net"
  5. "net/netip"
  6. "testing"
  7. "time"
  8. "github.com/amnezia-vpn/amneziawg-go/v3/device"
  9. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  10. "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  11. )
  12. // TestManagerLifecycle exercises Ensure/Reconcile's reconfigure-in-place vs.
  13. // rebuild split (see ensureLocked's doc comment) and Reconcile's stop path,
  14. // using a throwaway Manager rather than the process-wide singleton so this
  15. // test doesn't interact with any other test's state.
  16. func TestManagerLifecycle(t *testing.T) {
  17. priv, pub, err := wireguard.GenerateWireguardKeypair()
  18. if err != nil {
  19. t.Fatalf("generate keypair: %v", err)
  20. }
  21. m := &Manager{ifaces: map[int]*managed{}}
  22. inst := amneziawg.Instance{
  23. Id: 3,
  24. InterfaceName: "awgtest3",
  25. ListenPort: 58714,
  26. PrivateKey: priv,
  27. PublicKey: pub,
  28. Address: []string{"10.203.0.1/24"},
  29. MTU: 1420,
  30. Obfuscation: amneziawg.Obfuscation31{
  31. Jc: 4, Jmin: 40, Jmax: 70,
  32. S1: 20, S2: 30, S3: 20, S4: 20,
  33. },
  34. }
  35. defer m.StopAll()
  36. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  37. t.Fatalf("Ensure (create): %v", err)
  38. }
  39. if !m.HasRunning() {
  40. t.Fatal("HasRunning() = false after Ensure created an interface")
  41. }
  42. dev1, _, ok := m.Lookup(inst.Id)
  43. if !ok {
  44. t.Fatal("Lookup after Ensure: not found")
  45. }
  46. // Same Instance again: same address fingerprint, so this should
  47. // reconfigure the existing Device via IpcSet rather than rebuild it --
  48. // verify by checking the *Device pointer survived unchanged.
  49. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  50. t.Fatalf("Ensure (unchanged): %v", err)
  51. }
  52. dev2, _, ok := m.Lookup(inst.Id)
  53. if !ok {
  54. t.Fatal("Lookup after second Ensure: not found")
  55. }
  56. if dev1 != dev2 {
  57. t.Error("Ensure with an unchanged Instance rebuilt the Device; expected an in-place reconfigure")
  58. }
  59. // Changing the interface address is structural (fixed at netstack
  60. // construction time) and must force a rebuild -- verify by checking the
  61. // *Device pointer changed.
  62. changed := inst
  63. changed.Address = []string{"10.203.1.1/24"}
  64. if err := m.Ensure(Desired{Instance: changed}); err != nil {
  65. t.Fatalf("Ensure (address changed): %v", err)
  66. }
  67. dev3, _, ok := m.Lookup(inst.Id)
  68. if !ok {
  69. t.Fatal("Lookup after address-changing Ensure: not found")
  70. }
  71. if dev3 == dev2 {
  72. t.Error("Ensure with a changed address reconfigured in place; expected a rebuild")
  73. }
  74. // Reconcile with nothing desired stops every managed interface.
  75. m.Reconcile(nil)
  76. if m.HasRunning() {
  77. t.Error("HasRunning() = true after Reconcile([]) should have stopped everything")
  78. }
  79. if _, _, ok := m.Lookup(inst.Id); ok {
  80. t.Error("Lookup succeeded after Reconcile([]) removed the interface")
  81. }
  82. }
  83. func TestManagedUDPHandlerDoesNotWaitForManagerLock(t *testing.T) {
  84. cur := &managed{udpRelay: NewUDPRelay(SocksRelay{Addr: "invalid"}, nil)}
  85. cur.peers.Store(NewPeerIndex([]amneziawg.Peer{{
  86. Email: "peer@test",
  87. AllowedIPs: []string{"10.210.0.2/32"},
  88. }}))
  89. m := &Manager{}
  90. done := make(chan struct{})
  91. m.mu.Lock()
  92. go func() {
  93. cur.handleUDP(
  94. netip.MustParseAddrPort("10.210.0.2:1234"),
  95. netip.MustParseAddrPort("10.210.0.3:53"),
  96. []byte("query"),
  97. )
  98. close(done)
  99. }()
  100. select {
  101. case <-done:
  102. m.mu.Unlock()
  103. case <-time.After(time.Second):
  104. m.mu.Unlock()
  105. t.Fatal("UDP handler blocked on the manager lifecycle lock")
  106. }
  107. }
  108. // TestEnsureUnchangedInstanceDoesNotResetLivePeers is a regression test for a
  109. // real production bug: an unchanged Ensure call (the common case on every
  110. // 10s AmneziaWGJob reconcile tick when no admin edit happened) was calling
  111. // IpcSet unconditionally. amneziawg-go's IpcSet always includes
  112. // replace_peers=true (see buildUAPIConfig), and its own implementation of
  113. // that op is device.RemoveAllPeers() -- unconditionally, even when the new
  114. // peer list is byte-identical to the old one. That tore down every peer's
  115. // live handshake/session state on every single reconcile tick, so no real
  116. // connection could ever survive past ~10 seconds. Caught via a live test
  117. // connection that reset every ~10s with amneziawg-go's own verbose logging
  118. // enabled (AMNEZIAWGNET_DEBUG) showing "UAPI: Removing all peers" +
  119. // peer "Stopping"/"Starting" on every tick.
  120. //
  121. // Verified here by comparing the *device.Peer pointer LookupPeer returns
  122. // before and after a no-op Ensure: identical pointer proves the peer object
  123. // itself survived (no RemoveAllPeers), not just that some higher-level
  124. // abstraction looks unchanged.
  125. func TestEnsureUnchangedInstanceDoesNotResetLivePeers(t *testing.T) {
  126. priv, pub, err := wireguard.GenerateWireguardKeypair()
  127. if err != nil {
  128. t.Fatalf("generate server keypair: %v", err)
  129. }
  130. _, peerPub, err := wireguard.GenerateWireguardKeypair()
  131. if err != nil {
  132. t.Fatalf("generate peer keypair: %v", err)
  133. }
  134. m := &Manager{ifaces: map[int]*managed{}}
  135. inst := amneziawg.Instance{
  136. Id: 4,
  137. InterfaceName: "awgtest4",
  138. ListenPort: 58715,
  139. PrivateKey: priv,
  140. PublicKey: pub,
  141. Address: []string{"10.204.0.1/24"},
  142. MTU: 1420,
  143. Obfuscation: amneziawg.Obfuscation31{
  144. Jc: 4, Jmin: 40, Jmax: 70,
  145. S1: 20, S2: 30, S3: 20, S4: 20,
  146. },
  147. Peers: []amneziawg.Peer{
  148. {Email: "peer@test", PublicKey: peerPub, AllowedIPs: []string{"10.204.0.2/32"}},
  149. },
  150. }
  151. defer m.StopAll()
  152. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  153. t.Fatalf("Ensure (create): %v", err)
  154. }
  155. peerPubHex, err := wireguard.KeyToHex(peerPub)
  156. if err != nil {
  157. t.Fatalf("KeyToHex: %v", err)
  158. }
  159. var npk device.NoisePublicKey
  160. if err := npk.FromHex(peerPubHex); err != nil {
  161. t.Fatalf("NoisePublicKey.FromHex: %v", err)
  162. }
  163. dev, _, ok := m.Lookup(inst.Id)
  164. if !ok {
  165. t.Fatal("Lookup after Ensure: not found")
  166. }
  167. peerBefore := dev.LookupPeer(npk)
  168. if peerBefore == nil {
  169. t.Fatal("LookupPeer returned nil right after Ensure created the peer")
  170. }
  171. // Simulate the reconcile job firing again with byte-identical data --
  172. // this is what AmneziaWGJob does every 10 seconds regardless of whether
  173. // anything actually changed.
  174. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  175. t.Fatalf("Ensure (unchanged, second tick): %v", err)
  176. }
  177. peerAfter := dev.LookupPeer(npk)
  178. if peerAfter == nil {
  179. t.Fatal("LookupPeer returned nil after the unchanged Ensure -- peer was removed and never re-added")
  180. }
  181. if peerBefore != peerAfter {
  182. t.Error("unchanged Ensure recreated the peer object (RemoveAllPeers + re-add) -- " +
  183. "any live handshake/session on this peer would have been reset for no reason")
  184. }
  185. }
  186. // TestForwardedPortsOnlyChangeStillReconcilesPortForwards is a regression
  187. // test for the Phase 3.6 port-forwarding wiring: buildUAPIConfig never reads
  188. // ForwardedPorts (it's a panel-level concept, not a WireGuard UAPI field),
  189. // so a ForwardedPorts-only edit renders a byte-identical UAPI config and
  190. // takes ensureLocked's true no-op branch -- the exact same branch
  191. // TestEnsureUnchangedInstanceDoesNotResetLivePeers exists to guard, just for
  192. // a different subsystem. Without an explicit portForwards.Reconcile call on
  193. // that branch, a ForwardedPorts-only edit would silently never open (or
  194. // close) a listener until some unrelated change also happened to touch this
  195. // inbound. Verified end to end here: a real host-facing listener must exist
  196. // after the second Ensure call, not just an internal state flag.
  197. func TestForwardedPortsOnlyChangeStillReconcilesPortForwards(t *testing.T) {
  198. priv, pub, err := wireguard.GenerateWireguardKeypair()
  199. if err != nil {
  200. t.Fatalf("generate server keypair: %v", err)
  201. }
  202. _, peerPub, err := wireguard.GenerateWireguardKeypair()
  203. if err != nil {
  204. t.Fatalf("generate peer keypair: %v", err)
  205. }
  206. const forwardedPort = 58930
  207. m := &Manager{ifaces: map[int]*managed{}}
  208. inst := amneziawg.Instance{
  209. Id: 6,
  210. InterfaceName: "awgtest6",
  211. ListenPort: 58716,
  212. PrivateKey: priv,
  213. PublicKey: pub,
  214. Address: []string{"10.205.0.1/24"},
  215. MTU: 1420,
  216. Obfuscation: amneziawg.Obfuscation31{
  217. Jc: 4, Jmin: 40, Jmax: 70,
  218. S1: 20, S2: 30, S3: 20, S4: 20,
  219. },
  220. Peers: []amneziawg.Peer{
  221. {Email: "peer@test", PublicKey: peerPub, AllowedIPs: []string{"10.205.0.2/32"}},
  222. },
  223. }
  224. defer m.StopAll()
  225. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  226. t.Fatalf("Ensure (create, no ForwardedPorts yet): %v", err)
  227. }
  228. if _, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", forwardedPort), 200*time.Millisecond); err == nil {
  229. t.Fatal("forwarded port already accepting connections before ForwardedPorts was ever set")
  230. }
  231. // Only ForwardedPorts changes -- same keys, same AllowedIPs, same
  232. // address/MTU, so this must take ensureLocked's true no-op UAPI branch.
  233. changed := inst
  234. changed.Peers = []amneziawg.Peer{
  235. {Email: "peer@test", PublicKey: peerPub, AllowedIPs: []string{"10.205.0.2/32"}, ForwardedPorts: fmt.Sprintf("%d", forwardedPort)},
  236. }
  237. if err := m.Ensure(Desired{Instance: changed}); err != nil {
  238. t.Fatalf("Ensure (ForwardedPorts-only change): %v", err)
  239. }
  240. conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", forwardedPort), 2*time.Second)
  241. if err != nil {
  242. t.Fatalf("forwarded port not accepting connections after a ForwardedPorts-only Ensure: %v", err)
  243. }
  244. conn.Close()
  245. }
  246. // TestEnsureHeaderProtectionKeyChangeReconfiguresInPlace is a regression
  247. // test for the Phase 3.7 AWG 3.0 wiring: proves that populating
  248. // Desired.Options with a real HeaderProtectionKey/ContentPaddingAddition
  249. // takes ensureLocked's existing reconfigure-in-place branch (same *Device
  250. // survives, no rebuild) rather than silently doing nothing or forcing an
  251. // unnecessary rebuild -- buildUAPIConfig already rendered these fields
  252. // before this phase, so no manager.go changes were needed, but this proves
  253. // the whole chain (Desired -> DeviceOptions -> buildUAPIConfig -> IpcSet)
  254. // actually works together, not just in isolation.
  255. func TestEnsureHeaderProtectionKeyChangeReconfiguresInPlace(t *testing.T) {
  256. priv, pub, err := wireguard.GenerateWireguardKeypair()
  257. if err != nil {
  258. t.Fatalf("generate server keypair: %v", err)
  259. }
  260. headerProtectionKey, err := wireguard.GenerateWireguardPSK()
  261. if err != nil {
  262. t.Fatalf("generate header protection key: %v", err)
  263. }
  264. m := &Manager{ifaces: map[int]*managed{}}
  265. inst := amneziawg.Instance{
  266. Id: 7,
  267. InterfaceName: "awgtest7",
  268. ListenPort: 58717,
  269. PrivateKey: priv,
  270. PublicKey: pub,
  271. Address: []string{"10.207.0.1/24"},
  272. MTU: 1420,
  273. Obfuscation: amneziawg.Obfuscation31{
  274. Jc: 4, Jmin: 40, Jmax: 70,
  275. S1: 20, S2: 30, S3: 20, S4: 20,
  276. },
  277. }
  278. defer m.StopAll()
  279. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  280. t.Fatalf("Ensure (create, no header protection yet): %v", err)
  281. }
  282. dev1, _, ok := m.Lookup(inst.Id)
  283. if !ok {
  284. t.Fatal("Lookup after Ensure: not found")
  285. }
  286. err = m.Ensure(Desired{
  287. Instance: inst,
  288. Options: DeviceOptions{
  289. HeaderProtectionKey: headerProtectionKey,
  290. ContentPaddingAddition: "20-40",
  291. },
  292. })
  293. if err != nil {
  294. t.Fatalf("Ensure (HeaderProtectionKey-only change): %v", err)
  295. }
  296. dev2, _, ok := m.Lookup(inst.Id)
  297. if !ok {
  298. t.Fatal("Lookup after second Ensure: not found")
  299. }
  300. if dev1 != dev2 {
  301. t.Error("Ensure with a HeaderProtectionKey-only change rebuilt the Device; expected an in-place IpcSet reconfigure")
  302. }
  303. }
  304. // TestEnsureRejectsHeaderProtectionKeyWithLowS1S4 proves amneziawg-go's own
  305. // IpcSet backstop really exists independent of the save-time
  306. // ValidateHeaderProtection check in
  307. // internal/web/service/inbound_amneziawg.go -- that web-layer check can be
  308. // bypassed (a node-owned inbound, a direct DB edit), so this confirms a
  309. // malformed config still fails loudly here rather than silently applying a
  310. // broken interface.
  311. func TestEnsureRejectsHeaderProtectionKeyWithLowS1S4(t *testing.T) {
  312. priv, pub, err := wireguard.GenerateWireguardKeypair()
  313. if err != nil {
  314. t.Fatalf("generate server keypair: %v", err)
  315. }
  316. headerProtectionKey, err := wireguard.GenerateWireguardPSK()
  317. if err != nil {
  318. t.Fatalf("generate header protection key: %v", err)
  319. }
  320. m := &Manager{ifaces: map[int]*managed{}}
  321. inst := amneziawg.Instance{
  322. Id: 8,
  323. InterfaceName: "awgtest8",
  324. ListenPort: 58718,
  325. PrivateKey: priv,
  326. PublicKey: pub,
  327. Address: []string{"10.208.0.1/24"},
  328. MTU: 1420,
  329. Obfuscation: amneziawg.Obfuscation31{
  330. Jc: 4, Jmin: 40, Jmax: 70,
  331. S1: 5, S2: 5, S3: 5, S4: 5, // all below amneziawg-go's own 12-byte minimum
  332. },
  333. }
  334. defer m.StopAll()
  335. err = m.Ensure(Desired{
  336. Instance: inst,
  337. Options: DeviceOptions{HeaderProtectionKey: headerProtectionKey},
  338. })
  339. if err == nil {
  340. t.Fatal("Ensure must fail: amneziawg-go's own IpcSet rejects header protection with S1-S4 below its minimum")
  341. }
  342. }