manager_test.go 12 KB

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