manager_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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. // An inbound with no explicit MTU derives it from S4, so an S4-only edit is
  84. // structural: leave it out of the fingerprint and the netstack keeps the old MTU
  85. // while every client emitter already advertises the new one.
  86. func TestEnsureRebuildsWhenS4ChangesTheDerivedMTU(t *testing.T) {
  87. priv, pub, err := wireguard.GenerateWireguardKeypair()
  88. if err != nil {
  89. t.Fatalf("generate keypair: %v", err)
  90. }
  91. tests := []struct {
  92. name string
  93. mtu int
  94. wantRebuild bool
  95. }{
  96. {"derived MTU", 0, true},
  97. {"explicit MTU", 1420, false},
  98. }
  99. for i, tt := range tests {
  100. t.Run(tt.name, func(t *testing.T) {
  101. m := &Manager{ifaces: map[int]*managed{}}
  102. defer m.StopAll()
  103. inst := amneziawg.Instance{
  104. Id: 9 + i,
  105. InterfaceName: fmt.Sprintf("awgtest%d", 9+i),
  106. ListenPort: 58719 + i,
  107. PrivateKey: priv,
  108. PublicKey: pub,
  109. Address: []string{"10.209.0.1/24"},
  110. MTU: tt.mtu,
  111. Obfuscation: amneziawg.Obfuscation31{
  112. Jc: 4, Jmin: 40, Jmax: 70,
  113. S1: 20, S2: 30, S3: 20, S4: 5,
  114. },
  115. }
  116. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  117. t.Fatalf("Ensure (create): %v", err)
  118. }
  119. before, _, ok := m.Lookup(inst.Id)
  120. if !ok {
  121. t.Fatal("Lookup after create: not found")
  122. }
  123. edited := inst
  124. edited.Obfuscation.S4 = 27
  125. if err := m.Ensure(Desired{Instance: edited}); err != nil {
  126. t.Fatalf("Ensure (S4 changed): %v", err)
  127. }
  128. after, _, ok := m.Lookup(inst.Id)
  129. if !ok {
  130. t.Fatal("Lookup after S4 edit: not found")
  131. }
  132. if rebuilt := before != after; rebuilt != tt.wantRebuild {
  133. t.Errorf("S4 5->27 rebuilt the Device = %v, want %v (MTU %d -> %d)",
  134. rebuilt, tt.wantRebuild,
  135. amneziawg.EffectiveMTU(inst.MTU, inst.Obfuscation.S4),
  136. amneziawg.EffectiveMTU(edited.MTU, edited.Obfuscation.S4))
  137. }
  138. })
  139. }
  140. }
  141. func TestManagedUDPHandlerDoesNotWaitForManagerLock(t *testing.T) {
  142. cur := &managed{udpRelay: NewUDPRelay(SocksRelay{Addr: "invalid"}, nil)}
  143. cur.peers.Store(NewPeerIndex([]amneziawg.Peer{{
  144. Email: "peer@test",
  145. AllowedIPs: []string{"10.210.0.2/32"},
  146. }}))
  147. m := &Manager{}
  148. done := make(chan struct{})
  149. m.mu.Lock()
  150. go func() {
  151. cur.handleUDP(
  152. netip.MustParseAddrPort("10.210.0.2:1234"),
  153. netip.MustParseAddrPort("10.210.0.3:53"),
  154. []byte("query"),
  155. )
  156. close(done)
  157. }()
  158. select {
  159. case <-done:
  160. m.mu.Unlock()
  161. case <-time.After(time.Second):
  162. m.mu.Unlock()
  163. t.Fatal("UDP handler blocked on the manager lifecycle lock")
  164. }
  165. }
  166. // TestEnsureUnchangedInstanceDoesNotResetLivePeers is a regression test for a
  167. // real production bug: an unchanged Ensure call (the common case on every
  168. // 10s AmneziaWGJob reconcile tick when no admin edit happened) was calling
  169. // IpcSet unconditionally. amneziawg-go's IpcSet always includes
  170. // replace_peers=true (see buildUAPIConfig), and its own implementation of
  171. // that op is device.RemoveAllPeers() -- unconditionally, even when the new
  172. // peer list is byte-identical to the old one. That tore down every peer's
  173. // live handshake/session state on every single reconcile tick, so no real
  174. // connection could ever survive past ~10 seconds. Caught via a live test
  175. // connection that reset every ~10s with amneziawg-go's own verbose logging
  176. // enabled (AMNEZIAWGNET_DEBUG) showing "UAPI: Removing all peers" +
  177. // peer "Stopping"/"Starting" on every tick.
  178. //
  179. // Verified here by comparing the *device.Peer pointer LookupPeer returns
  180. // before and after a no-op Ensure: identical pointer proves the peer object
  181. // itself survived (no RemoveAllPeers), not just that some higher-level
  182. // abstraction looks unchanged.
  183. func TestEnsureUnchangedInstanceDoesNotResetLivePeers(t *testing.T) {
  184. priv, pub, err := wireguard.GenerateWireguardKeypair()
  185. if err != nil {
  186. t.Fatalf("generate server keypair: %v", err)
  187. }
  188. _, peerPub, err := wireguard.GenerateWireguardKeypair()
  189. if err != nil {
  190. t.Fatalf("generate peer keypair: %v", err)
  191. }
  192. m := &Manager{ifaces: map[int]*managed{}}
  193. inst := amneziawg.Instance{
  194. Id: 4,
  195. InterfaceName: "awgtest4",
  196. ListenPort: 58715,
  197. PrivateKey: priv,
  198. PublicKey: pub,
  199. Address: []string{"10.204.0.1/24"},
  200. MTU: 1420,
  201. Obfuscation: amneziawg.Obfuscation31{
  202. Jc: 4, Jmin: 40, Jmax: 70,
  203. S1: 20, S2: 30, S3: 20, S4: 20,
  204. },
  205. Peers: []amneziawg.Peer{
  206. {Email: "peer@test", PublicKey: peerPub, AllowedIPs: []string{"10.204.0.2/32"}},
  207. },
  208. }
  209. defer m.StopAll()
  210. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  211. t.Fatalf("Ensure (create): %v", err)
  212. }
  213. peerPubHex, err := wireguard.KeyToHex(peerPub)
  214. if err != nil {
  215. t.Fatalf("KeyToHex: %v", err)
  216. }
  217. var npk device.NoisePublicKey
  218. if err := npk.FromHex(peerPubHex); err != nil {
  219. t.Fatalf("NoisePublicKey.FromHex: %v", err)
  220. }
  221. dev, _, ok := m.Lookup(inst.Id)
  222. if !ok {
  223. t.Fatal("Lookup after Ensure: not found")
  224. }
  225. peerBefore := dev.LookupPeer(npk)
  226. if peerBefore == nil {
  227. t.Fatal("LookupPeer returned nil right after Ensure created the peer")
  228. }
  229. // Simulate the reconcile job firing again with byte-identical data --
  230. // this is what AmneziaWGJob does every 10 seconds regardless of whether
  231. // anything actually changed.
  232. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  233. t.Fatalf("Ensure (unchanged, second tick): %v", err)
  234. }
  235. peerAfter := dev.LookupPeer(npk)
  236. if peerAfter == nil {
  237. t.Fatal("LookupPeer returned nil after the unchanged Ensure -- peer was removed and never re-added")
  238. }
  239. if peerBefore != peerAfter {
  240. t.Error("unchanged Ensure recreated the peer object (RemoveAllPeers + re-add) -- " +
  241. "any live handshake/session on this peer would have been reset for no reason")
  242. }
  243. }
  244. // TestForwardedPortsOnlyChangeStillReconcilesPortForwards is a regression
  245. // test for the Phase 3.6 port-forwarding wiring: buildUAPIConfig never reads
  246. // ForwardedPorts (it's a panel-level concept, not a WireGuard UAPI field),
  247. // so a ForwardedPorts-only edit renders a byte-identical UAPI config and
  248. // takes ensureLocked's true no-op branch -- the exact same branch
  249. // TestEnsureUnchangedInstanceDoesNotResetLivePeers exists to guard, just for
  250. // a different subsystem. Without an explicit portForwards.Reconcile call on
  251. // that branch, a ForwardedPorts-only edit would silently never open (or
  252. // close) a listener until some unrelated change also happened to touch this
  253. // inbound. Verified end to end here: a real host-facing listener must exist
  254. // after the second Ensure call, not just an internal state flag.
  255. func TestForwardedPortsOnlyChangeStillReconcilesPortForwards(t *testing.T) {
  256. priv, pub, err := wireguard.GenerateWireguardKeypair()
  257. if err != nil {
  258. t.Fatalf("generate server keypair: %v", err)
  259. }
  260. _, peerPub, err := wireguard.GenerateWireguardKeypair()
  261. if err != nil {
  262. t.Fatalf("generate peer keypair: %v", err)
  263. }
  264. const forwardedPort = 58930
  265. m := &Manager{ifaces: map[int]*managed{}}
  266. inst := amneziawg.Instance{
  267. Id: 6,
  268. InterfaceName: "awgtest6",
  269. ListenPort: 58716,
  270. PrivateKey: priv,
  271. PublicKey: pub,
  272. Address: []string{"10.205.0.1/24"},
  273. MTU: 1420,
  274. Obfuscation: amneziawg.Obfuscation31{
  275. Jc: 4, Jmin: 40, Jmax: 70,
  276. S1: 20, S2: 30, S3: 20, S4: 20,
  277. },
  278. Peers: []amneziawg.Peer{
  279. {Email: "peer@test", PublicKey: peerPub, AllowedIPs: []string{"10.205.0.2/32"}},
  280. },
  281. }
  282. defer m.StopAll()
  283. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  284. t.Fatalf("Ensure (create, no ForwardedPorts yet): %v", err)
  285. }
  286. if _, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", forwardedPort), 200*time.Millisecond); err == nil {
  287. t.Fatal("forwarded port already accepting connections before ForwardedPorts was ever set")
  288. }
  289. // Only ForwardedPorts changes -- same keys, same AllowedIPs, same
  290. // address/MTU, so this must take ensureLocked's true no-op UAPI branch.
  291. changed := inst
  292. changed.Peers = []amneziawg.Peer{
  293. {Email: "peer@test", PublicKey: peerPub, AllowedIPs: []string{"10.205.0.2/32"}, ForwardedPorts: fmt.Sprintf("%d", forwardedPort)},
  294. }
  295. if err := m.Ensure(Desired{Instance: changed}); err != nil {
  296. t.Fatalf("Ensure (ForwardedPorts-only change): %v", err)
  297. }
  298. conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", forwardedPort), 2*time.Second)
  299. if err != nil {
  300. t.Fatalf("forwarded port not accepting connections after a ForwardedPorts-only Ensure: %v", err)
  301. }
  302. conn.Close()
  303. }
  304. // TestEnsureHeaderProtectionKeyChangeReconfiguresInPlace is a regression
  305. // test for the Phase 3.7 AWG 3.0 wiring: proves that populating
  306. // Desired.Options with a real HeaderProtectionKey/ContentPaddingAddition
  307. // takes ensureLocked's existing reconfigure-in-place branch (same *Device
  308. // survives, no rebuild) rather than silently doing nothing or forcing an
  309. // unnecessary rebuild -- buildUAPIConfig already rendered these fields
  310. // before this phase, so no manager.go changes were needed, but this proves
  311. // the whole chain (Desired -> DeviceOptions -> buildUAPIConfig -> IpcSet)
  312. // actually works together, not just in isolation.
  313. func TestEnsureHeaderProtectionKeyChangeReconfiguresInPlace(t *testing.T) {
  314. priv, pub, err := wireguard.GenerateWireguardKeypair()
  315. if err != nil {
  316. t.Fatalf("generate server keypair: %v", err)
  317. }
  318. headerProtectionKey, err := wireguard.GenerateWireguardPSK()
  319. if err != nil {
  320. t.Fatalf("generate header protection key: %v", err)
  321. }
  322. m := &Manager{ifaces: map[int]*managed{}}
  323. inst := amneziawg.Instance{
  324. Id: 7,
  325. InterfaceName: "awgtest7",
  326. ListenPort: 58717,
  327. PrivateKey: priv,
  328. PublicKey: pub,
  329. Address: []string{"10.207.0.1/24"},
  330. MTU: 1420,
  331. Obfuscation: amneziawg.Obfuscation31{
  332. Jc: 4, Jmin: 40, Jmax: 70,
  333. S1: 20, S2: 30, S3: 20, S4: 20,
  334. },
  335. }
  336. defer m.StopAll()
  337. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  338. t.Fatalf("Ensure (create, no header protection yet): %v", err)
  339. }
  340. dev1, _, ok := m.Lookup(inst.Id)
  341. if !ok {
  342. t.Fatal("Lookup after Ensure: not found")
  343. }
  344. err = m.Ensure(Desired{
  345. Instance: inst,
  346. Options: DeviceOptions{
  347. HeaderProtectionKey: headerProtectionKey,
  348. ContentPaddingAddition: "20-40",
  349. },
  350. })
  351. if err != nil {
  352. t.Fatalf("Ensure (HeaderProtectionKey-only change): %v", err)
  353. }
  354. dev2, _, ok := m.Lookup(inst.Id)
  355. if !ok {
  356. t.Fatal("Lookup after second Ensure: not found")
  357. }
  358. if dev1 != dev2 {
  359. t.Error("Ensure with a HeaderProtectionKey-only change rebuilt the Device; expected an in-place IpcSet reconfigure")
  360. }
  361. }
  362. // TestEnsureRejectsHeaderProtectionKeyWithLowS1S4 proves amneziawg-go's own
  363. // IpcSet backstop really exists independent of the save-time
  364. // ValidateHeaderProtection check in
  365. // internal/web/service/inbound_amneziawg.go -- that web-layer check can be
  366. // bypassed (a node-owned inbound, a direct DB edit), so this confirms a
  367. // malformed config still fails loudly here rather than silently applying a
  368. // broken interface.
  369. func TestEnsureRejectsHeaderProtectionKeyWithLowS1S4(t *testing.T) {
  370. priv, pub, err := wireguard.GenerateWireguardKeypair()
  371. if err != nil {
  372. t.Fatalf("generate server keypair: %v", err)
  373. }
  374. headerProtectionKey, err := wireguard.GenerateWireguardPSK()
  375. if err != nil {
  376. t.Fatalf("generate header protection key: %v", err)
  377. }
  378. m := &Manager{ifaces: map[int]*managed{}}
  379. inst := amneziawg.Instance{
  380. Id: 8,
  381. InterfaceName: "awgtest8",
  382. ListenPort: 58718,
  383. PrivateKey: priv,
  384. PublicKey: pub,
  385. Address: []string{"10.208.0.1/24"},
  386. MTU: 1420,
  387. Obfuscation: amneziawg.Obfuscation31{
  388. Jc: 4, Jmin: 40, Jmax: 70,
  389. S1: 5, S2: 5, S3: 5, S4: 5, // all below amneziawg-go's own 12-byte minimum
  390. },
  391. }
  392. defer m.StopAll()
  393. err = m.Ensure(Desired{
  394. Instance: inst,
  395. Options: DeviceOptions{HeaderProtectionKey: headerProtectionKey},
  396. })
  397. if err == nil {
  398. t.Fatal("Ensure must fail: amneziawg-go's own IpcSet rejects header protection with S1-S4 below its minimum")
  399. }
  400. }