device_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. package amneziawgnet
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "net"
  7. "net/netip"
  8. "strings"
  9. "testing"
  10. "time"
  11. "github.com/amnezia-vpn/amneziawg-go/v3/device"
  12. "github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
  13. "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
  14. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  15. "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  16. )
  17. // TestNewDeviceHandshakeForwarderAndIdentity is Phase 1's real end-to-end
  18. // proof, not just a compile check: a genuine amneziawg-go client (via that
  19. // project's own tun/netstack.CreateNetTUN -- the client side doesn't need a
  20. // forwarder or peer-identity resolution, only this package's server side
  21. // does) completes a real 3-way handshake against a Device built by
  22. // NewDevice, dials a destination that was never configured anywhere on the
  23. // server, and the test verifies AttachTCPForwarder recovers that exact
  24. // destination *and* PeerIndex.Lookup resolves the connection's source back
  25. // to the right peer's Email -- Phase 1a/1b/1c working together, the same
  26. // mechanism Phase 0's throwaway spike validated, now as a real, repo-owned,
  27. // repeatable test instead of scratch code.
  28. func TestNewDeviceHandshakeForwarderAndIdentity(t *testing.T) {
  29. serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
  30. if err != nil {
  31. t.Fatalf("generate server keypair: %v", err)
  32. }
  33. clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
  34. if err != nil {
  35. t.Fatalf("generate client keypair: %v", err)
  36. }
  37. const listenPort = 58712 // fixed loopback test port, matches the validated Phase 0 spike approach
  38. const wantEmail = "[email protected]"
  39. inst := amneziawg.Instance{
  40. Id: 1,
  41. InterfaceName: "awgtest1",
  42. ListenPort: listenPort,
  43. PrivateKey: serverPriv,
  44. PublicKey: serverPub,
  45. Address: []string{"10.201.0.1/24"},
  46. MTU: 1420,
  47. Obfuscation: amneziawg.Obfuscation31{
  48. Jc: 4, Jmin: 40, Jmax: 70,
  49. S1: 20, S2: 30, S3: 20, S4: 20,
  50. },
  51. Peers: []amneziawg.Peer{{
  52. Email: wantEmail,
  53. PublicKey: clientPub,
  54. AllowedIPs: []string{"10.201.0.2/32"},
  55. }},
  56. }
  57. dev, err := newUnconfiguredDevice(inst, DeviceOptions{})
  58. if err != nil {
  59. t.Fatalf("newUnconfiguredDevice: %v", err)
  60. }
  61. defer dev.Close()
  62. idx := NewPeerIndex(inst.Peers)
  63. type recovered struct {
  64. email string
  65. ok bool
  66. dest netip.AddrPort
  67. }
  68. got := make(chan recovered, 1)
  69. // Never configured anywhere server-side: the forwarder must recover it
  70. // purely from the decapsulated packet, not from any routing table.
  71. wantDest := netip.MustParseAddrPort("10.201.9.9:9999")
  72. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  73. defer conn.Close()
  74. srcAddrPort, parseErr := netip.ParseAddrPort(conn.RemoteAddr().String())
  75. var peer amneziawg.Peer
  76. var ok bool
  77. if parseErr == nil {
  78. peer, ok = idx.Lookup(srcAddrPort.Addr().Unmap())
  79. }
  80. got <- recovered{email: peer.Email, ok: ok, dest: dest}
  81. io.Copy(io.Discard, conn)
  82. })
  83. // Configure (IpcSet) must come after AttachTCPForwarder -- see
  84. // newUnconfiguredDevice's doc comment: IpcSet is what starts the peer's
  85. // receive goroutine, which must never be able to run before the
  86. // forwarder is registered on the stack.
  87. if err := dev.Configure(inst, DeviceOptions{}); err != nil {
  88. t.Fatalf("Configure: %v", err)
  89. }
  90. clientTun, clientNet, err := netstack.CreateNetTUN(
  91. []netip.Addr{netip.MustParseAddr("10.201.0.2")},
  92. []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
  93. if err != nil {
  94. t.Fatalf("client CreateNetTUN: %v", err)
  95. }
  96. clientDev := device.NewDevice(clientTun, newListenBind(""), device.NewLogger(device.LogLevelSilent, ""))
  97. defer clientDev.Close()
  98. clientPrivHex, err := wireguard.KeyToHex(clientPriv)
  99. if err != nil {
  100. t.Fatalf("client key to hex: %v", err)
  101. }
  102. serverPubHex, err := wireguard.KeyToHex(serverPub)
  103. if err != nil {
  104. t.Fatalf("server key to hex: %v", err)
  105. }
  106. // allowed_ip=0.0.0.0/0 on the client matches a real VPN client's own
  107. // config (route everything through the tunnel) -- it's also what makes
  108. // dialing an arbitrary, never-configured destination like wantDest
  109. // actually get routed to the server peer at all: a narrower AllowedIPs
  110. // here would make the client's own Device drop the packet as
  111. // non-matching before it ever reached the wire.
  112. clientConf := fmt.Sprintf(
  113. "private_key=%s\njc=4\njmin=40\njmax=70\ns1=20\ns2=30\ns3=20\ns4=20\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
  114. clientPrivHex, serverPubHex, listenPort)
  115. if err := clientDev.IpcSet(clientConf); err != nil {
  116. t.Fatalf("client IpcSet: %v", err)
  117. }
  118. if err := clientDev.Up(); err != nil {
  119. t.Fatalf("client Up: %v", err)
  120. }
  121. // Retry the dial rather than guessing a fixed handshake delay: the
  122. // first attempts may race the handshake, later ones should succeed
  123. // once it completes.
  124. dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  125. defer cancel()
  126. var lastErr error
  127. for {
  128. conn, dialErr := clientNet.DialContext(dialCtx, "tcp", wantDest.String())
  129. if dialErr == nil {
  130. conn.Close()
  131. break
  132. }
  133. lastErr = dialErr
  134. select {
  135. case <-dialCtx.Done():
  136. t.Fatalf("client dial never succeeded: %v", lastErr)
  137. case <-time.After(100 * time.Millisecond):
  138. }
  139. }
  140. select {
  141. case r := <-got:
  142. if !r.ok {
  143. t.Fatal("forwarder: peer identity lookup failed for the recovered connection")
  144. }
  145. if r.email != wantEmail {
  146. t.Errorf("resolved peer email = %q, want %q", r.email, wantEmail)
  147. }
  148. if r.dest != wantDest {
  149. t.Errorf("recovered destination = %v, want %v", r.dest, wantDest)
  150. }
  151. case <-time.After(5 * time.Second):
  152. t.Fatal("timed out waiting for the forwarder to hand back the recovered connection")
  153. }
  154. }
  155. // TestBuildUAPIConfigHeaderProtectionAndContentPaddingLines is a cheap,
  156. // network-free companion to the real round-trip test below: confirms the 2
  157. // AWG 3.0 UAPI lines only appear when set, and that a malformed
  158. // HeaderProtectionKey surfaces a clear, wrapped error instead of silently
  159. // producing a UAPI string amneziawg-go's own IpcSet would reject uselessly.
  160. func TestBuildUAPIConfigHeaderProtectionAndContentPaddingLines(t *testing.T) {
  161. priv, _, err := wireguard.GenerateWireguardKeypair()
  162. if err != nil {
  163. t.Fatalf("generate keypair: %v", err)
  164. }
  165. inst := amneziawg.Instance{
  166. PrivateKey: priv,
  167. Obfuscation: amneziawg.Obfuscation31{
  168. S1: 20, S2: 20, S3: 20, S4: 20,
  169. },
  170. }
  171. conf, err := buildUAPIConfig(inst, DeviceOptions{})
  172. if err != nil {
  173. t.Fatalf("buildUAPIConfig with empty options: %v", err)
  174. }
  175. // header_protection_key is the exception: an omitted line reads as
  176. // "unchanged", so clearing the key has to be sent as the all-zero one.
  177. if !strings.Contains(conf, "header_protection_key="+strings.Repeat("0", 64)+"\n") {
  178. t.Fatalf("an unset key must be emitted as the all-zero key, got:\n%s", conf)
  179. }
  180. if strings.Contains(conf, "content_padding_addition=") {
  181. t.Fatalf("empty DeviceOptions must not emit AWG 3.0 lines, got:\n%s", conf)
  182. }
  183. key, err := wireguard.GenerateWireguardPSK()
  184. if err != nil {
  185. t.Fatalf("generate header protection key: %v", err)
  186. }
  187. conf, err = buildUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: key, ContentPaddingAddition: "20-40"})
  188. if err != nil {
  189. t.Fatalf("buildUAPIConfig with AWG 3.0 options: %v", err)
  190. }
  191. if !strings.Contains(conf, "header_protection_key=") {
  192. t.Errorf("expected a header_protection_key= line, got:\n%s", conf)
  193. }
  194. if !strings.Contains(conf, "content_padding_addition=20-40\n") {
  195. t.Errorf("expected a content_padding_addition=20-40 line, got:\n%s", conf)
  196. }
  197. if _, err := buildUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: "not-a-valid-base64-key"}); err == nil {
  198. t.Fatal("a malformed HeaderProtectionKey must be rejected, not silently passed through")
  199. }
  200. }
  201. // TestNewDeviceHeaderProtectionAndContentPaddingRoundTrip is the real proof
  202. // behind AmneziaWG 3.0's admin-facing HeaderProtectionKey/
  203. // ContentPaddingAddition fields: a genuine amneziawg-go client, configured
  204. // with matching header_protection_key/content_padding_addition UAPI lines
  205. // (S1-S4 all >= 12, the hard requirement amneziawg-go's own IpcSet enforces
  206. // for header protection), completes a real handshake against a Device built
  207. // via NewDevice/DeviceOptions and exchanges real application data both
  208. // directions through it. This is more than a handshake-completed check --
  209. // it also confirms actual payload bytes survive content padding on both the
  210. // send and receive sides, the specific area a third-party AmneziaWG
  211. // installer project's docs flagged a past interop concern for (see the
  212. // migration plan's own risk note); it is not a substitute for real-VPS
  213. // verification against the official client, but it is the cheapest
  214. // available local check against a regression in either engine's own padding
  215. // handling.
  216. func TestNewDeviceHeaderProtectionAndContentPaddingRoundTrip(t *testing.T) {
  217. serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
  218. if err != nil {
  219. t.Fatalf("generate server keypair: %v", err)
  220. }
  221. clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
  222. if err != nil {
  223. t.Fatalf("generate client keypair: %v", err)
  224. }
  225. headerProtectionKey, err := wireguard.GenerateWireguardPSK()
  226. if err != nil {
  227. t.Fatalf("generate header protection key: %v", err)
  228. }
  229. const listenPort = 58713 // fixed loopback test port, distinct from the handshake test above
  230. const contentPaddingAddition = "20-40"
  231. inst := amneziawg.Instance{
  232. Id: 2,
  233. InterfaceName: "awgtest2",
  234. ListenPort: listenPort,
  235. PrivateKey: serverPriv,
  236. PublicKey: serverPub,
  237. Address: []string{"10.202.0.1/24"},
  238. MTU: 1420,
  239. Obfuscation: amneziawg.Obfuscation31{
  240. Jc: 4, Jmin: 40, Jmax: 70,
  241. S1: 20, S2: 30, S3: 20, S4: 20, // all >= 12, required for header protection
  242. },
  243. Peers: []amneziawg.Peer{{
  244. Email: "[email protected]",
  245. PublicKey: clientPub,
  246. AllowedIPs: []string{"10.202.0.2/32"},
  247. }},
  248. }
  249. opts := DeviceOptions{
  250. HeaderProtectionKey: headerProtectionKey,
  251. ContentPaddingAddition: contentPaddingAddition,
  252. }
  253. dev, err := newUnconfiguredDevice(inst, opts)
  254. if err != nil {
  255. t.Fatalf("newUnconfiguredDevice: %v", err)
  256. }
  257. defer dev.Close()
  258. const wantRequest = "hello from client"
  259. const wantReply = "hello from server"
  260. serverDone := make(chan error, 1)
  261. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  262. defer conn.Close()
  263. buf := make([]byte, len(wantRequest))
  264. if _, err := io.ReadFull(conn, buf); err != nil {
  265. serverDone <- fmt.Errorf("server read: %w", err)
  266. return
  267. }
  268. if string(buf) != wantRequest {
  269. serverDone <- fmt.Errorf("server got %q, want %q", buf, wantRequest)
  270. return
  271. }
  272. if _, err := conn.Write([]byte(wantReply)); err != nil {
  273. serverDone <- fmt.Errorf("server write: %w", err)
  274. return
  275. }
  276. serverDone <- nil
  277. })
  278. // Configure (IpcSet) must come after AttachTCPForwarder -- see
  279. // newUnconfiguredDevice's doc comment.
  280. if err := dev.Configure(inst, opts); err != nil {
  281. t.Fatalf("Configure: %v", err)
  282. }
  283. clientTun, clientNet, err := netstack.CreateNetTUN(
  284. []netip.Addr{netip.MustParseAddr("10.202.0.2")},
  285. []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
  286. if err != nil {
  287. t.Fatalf("client CreateNetTUN: %v", err)
  288. }
  289. clientDev := device.NewDevice(clientTun, newListenBind(""), device.NewLogger(device.LogLevelSilent, ""))
  290. defer clientDev.Close()
  291. clientPrivHex, err := wireguard.KeyToHex(clientPriv)
  292. if err != nil {
  293. t.Fatalf("client key to hex: %v", err)
  294. }
  295. serverPubHex, err := wireguard.KeyToHex(serverPub)
  296. if err != nil {
  297. t.Fatalf("server key to hex: %v", err)
  298. }
  299. headerProtectionKeyHex, err := wireguard.KeyToHex(headerProtectionKey)
  300. if err != nil {
  301. t.Fatalf("header protection key to hex: %v", err)
  302. }
  303. clientConf := fmt.Sprintf(
  304. "private_key=%s\njc=4\njmin=40\njmax=70\ns1=20\ns2=30\ns3=20\ns4=20\nheader_protection_key=%s\ncontent_padding_addition=%s\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
  305. clientPrivHex, headerProtectionKeyHex, contentPaddingAddition, serverPubHex, listenPort)
  306. if err := clientDev.IpcSet(clientConf); err != nil {
  307. t.Fatalf("client IpcSet: %v", err)
  308. }
  309. if err := clientDev.Up(); err != nil {
  310. t.Fatalf("client Up: %v", err)
  311. }
  312. dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  313. defer cancel()
  314. var conn net.Conn
  315. for {
  316. c, dialErr := clientNet.DialContext(dialCtx, "tcp", "10.202.9.9:9999")
  317. if dialErr == nil {
  318. conn = c
  319. break
  320. }
  321. select {
  322. case <-dialCtx.Done():
  323. t.Fatalf("client dial never succeeded: %v", dialErr)
  324. case <-time.After(100 * time.Millisecond):
  325. }
  326. }
  327. defer conn.Close()
  328. if _, err := conn.Write([]byte(wantRequest)); err != nil {
  329. t.Fatalf("client write: %v", err)
  330. }
  331. if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
  332. t.Fatalf("SetReadDeadline: %v", err)
  333. }
  334. reply := make([]byte, len(wantReply))
  335. if _, err := io.ReadFull(conn, reply); err != nil {
  336. t.Fatalf("client read reply: %v", err)
  337. }
  338. if string(reply) != wantReply {
  339. t.Fatalf("client got reply %q, want %q", reply, wantReply)
  340. }
  341. select {
  342. case err := <-serverDone:
  343. if err != nil {
  344. t.Fatalf("server side: %v", err)
  345. }
  346. case <-time.After(5 * time.Second):
  347. t.Fatal("timed out waiting for the server side to finish")
  348. }
  349. }
  350. func TestBuildUAPIConfigRandomTrailersAndDisableCookiesLines(t *testing.T) {
  351. priv, _, err := wireguard.GenerateWireguardKeypair()
  352. if err != nil {
  353. t.Fatalf("generate keypair: %v", err)
  354. }
  355. inst := amneziawg.Instance{PrivateKey: priv}
  356. // Unlike HeaderProtectionKey/ContentPaddingAddition, these two lines
  357. // must always be present -- see DeviceOptions.RandomTrailers's own doc
  358. // comment on why an absent line (instead of an explicit "false") would
  359. // break the reconfigure-in-place diff for a true->false edit.
  360. conf, err := buildUAPIConfig(inst, DeviceOptions{})
  361. if err != nil {
  362. t.Fatalf("buildUAPIConfig with empty options: %v", err)
  363. }
  364. if !strings.Contains(conf, "random_trailers=false\n") {
  365. t.Errorf("expected an explicit random_trailers=false line even when unset, got:\n%s", conf)
  366. }
  367. if !strings.Contains(conf, "disable_cookies=false\n") {
  368. t.Errorf("expected an explicit disable_cookies=false line even when unset, got:\n%s", conf)
  369. }
  370. conf, err = buildUAPIConfig(inst, DeviceOptions{RandomTrailers: true, DisableCookies: true})
  371. if err != nil {
  372. t.Fatalf("buildUAPIConfig with both enabled: %v", err)
  373. }
  374. if !strings.Contains(conf, "random_trailers=true\n") {
  375. t.Errorf("expected a random_trailers=true line, got:\n%s", conf)
  376. }
  377. if !strings.Contains(conf, "disable_cookies=true\n") {
  378. t.Errorf("expected a disable_cookies=true line, got:\n%s", conf)
  379. }
  380. }
  381. // TestNewDeviceRandomTrailersAndDisableCookiesRoundTrip is the real proof
  382. // behind AmneziaWG 3.1's two new device-wide toggles: a genuine amneziawg-go
  383. // client with matching random_trailers=true/disable_cookies=true UAPI lines
  384. // completes a real handshake against a Device built via NewDevice/
  385. // DeviceOptions and exchanges real application data both directions through
  386. // it. This specifically exercises amneziawg-go's receive.go size-matching
  387. // path for RandomTrailers (device_test.go's HeaderProtection test doesn't
  388. // enable it), which only accepts a message when
  389. // `size == expectedSize || randomTrailers && size > expectedSize` -- proof
  390. // that setting it on both ends really does interoperate, not just that
  391. // IpcSet accepts the value.
  392. func TestNewDeviceRandomTrailersAndDisableCookiesRoundTrip(t *testing.T) {
  393. serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
  394. if err != nil {
  395. t.Fatalf("generate server keypair: %v", err)
  396. }
  397. clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
  398. if err != nil {
  399. t.Fatalf("generate client keypair: %v", err)
  400. }
  401. const listenPort = 58721 // fixed loopback test port, distinct from every other test in this package
  402. inst := amneziawg.Instance{
  403. Id: 3,
  404. InterfaceName: "awgtest3",
  405. ListenPort: listenPort,
  406. PrivateKey: serverPriv,
  407. PublicKey: serverPub,
  408. Address: []string{"10.203.0.1/24"},
  409. MTU: 1420,
  410. Peers: []amneziawg.Peer{{
  411. Email: "[email protected]",
  412. PublicKey: clientPub,
  413. AllowedIPs: []string{"10.203.0.2/32"},
  414. }},
  415. }
  416. opts := DeviceOptions{RandomTrailers: true, DisableCookies: true}
  417. dev, err := newUnconfiguredDevice(inst, opts)
  418. if err != nil {
  419. t.Fatalf("newUnconfiguredDevice: %v", err)
  420. }
  421. defer dev.Close()
  422. const wantRequest = "hello from client, with a trailer"
  423. const wantReply = "hello from server, with a trailer"
  424. serverDone := make(chan error, 1)
  425. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  426. defer conn.Close()
  427. buf := make([]byte, len(wantRequest))
  428. if _, err := io.ReadFull(conn, buf); err != nil {
  429. serverDone <- fmt.Errorf("server read: %w", err)
  430. return
  431. }
  432. if string(buf) != wantRequest {
  433. serverDone <- fmt.Errorf("server got %q, want %q", buf, wantRequest)
  434. return
  435. }
  436. if _, err := conn.Write([]byte(wantReply)); err != nil {
  437. serverDone <- fmt.Errorf("server write: %w", err)
  438. return
  439. }
  440. serverDone <- nil
  441. })
  442. // Configure (IpcSet) must come after AttachTCPForwarder -- see
  443. // newUnconfiguredDevice's doc comment.
  444. if err := dev.Configure(inst, opts); err != nil {
  445. t.Fatalf("Configure: %v", err)
  446. }
  447. clientTun, clientNet, err := netstack.CreateNetTUN(
  448. []netip.Addr{netip.MustParseAddr("10.203.0.2")},
  449. []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
  450. if err != nil {
  451. t.Fatalf("client CreateNetTUN: %v", err)
  452. }
  453. clientDev := device.NewDevice(clientTun, newListenBind(""), device.NewLogger(device.LogLevelSilent, ""))
  454. defer clientDev.Close()
  455. clientPrivHex, err := wireguard.KeyToHex(clientPriv)
  456. if err != nil {
  457. t.Fatalf("client key to hex: %v", err)
  458. }
  459. serverPubHex, err := wireguard.KeyToHex(serverPub)
  460. if err != nil {
  461. t.Fatalf("server key to hex: %v", err)
  462. }
  463. clientConf := fmt.Sprintf(
  464. "private_key=%s\nrandom_trailers=true\ndisable_cookies=true\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
  465. clientPrivHex, serverPubHex, listenPort)
  466. if err := clientDev.IpcSet(clientConf); err != nil {
  467. t.Fatalf("client IpcSet: %v", err)
  468. }
  469. if err := clientDev.Up(); err != nil {
  470. t.Fatalf("client Up: %v", err)
  471. }
  472. dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  473. defer cancel()
  474. var conn net.Conn
  475. for {
  476. c, dialErr := clientNet.DialContext(dialCtx, "tcp", "10.203.9.9:9999")
  477. if dialErr == nil {
  478. conn = c
  479. break
  480. }
  481. select {
  482. case <-dialCtx.Done():
  483. t.Fatalf("client dial never succeeded: %v", dialErr)
  484. case <-time.After(100 * time.Millisecond):
  485. }
  486. }
  487. defer conn.Close()
  488. if _, err := conn.Write([]byte(wantRequest)); err != nil {
  489. t.Fatalf("client write: %v", err)
  490. }
  491. if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
  492. t.Fatalf("SetReadDeadline: %v", err)
  493. }
  494. reply := make([]byte, len(wantReply))
  495. if _, err := io.ReadFull(conn, reply); err != nil {
  496. t.Fatalf("client read reply: %v", err)
  497. }
  498. if string(reply) != wantReply {
  499. t.Fatalf("client got reply %q, want %q", reply, wantReply)
  500. }
  501. select {
  502. case err := <-serverDone:
  503. if err != nil {
  504. t.Fatalf("server side: %v", err)
  505. }
  506. case <-time.After(5 * time.Second):
  507. t.Fatal("timed out waiting for the server side to finish")
  508. }
  509. }
  510. // TestValidatedObfuscationAlwaysApplies pins the contract ValidateObfuscation
  511. // exists for: whatever it accepts, amneziawg-go's own IpcSet must accept too.
  512. func TestValidatedObfuscationAlwaysApplies(t *testing.T) {
  513. priv, pub, err := wireguard.GenerateWireguardKeypair()
  514. if err != nil {
  515. t.Fatalf("server keypair: %v", err)
  516. }
  517. _, peerPub, err := wireguard.GenerateWireguardKeypair()
  518. if err != nil {
  519. t.Fatalf("peer keypair: %v", err)
  520. }
  521. base := amneziawg.Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}
  522. cases := []struct {
  523. name string
  524. mut func(*amneziawg.Obfuscation31)
  525. }{
  526. {"generated defaults", func(o *amneziawg.Obfuscation31) { *o = amneziawg.GenerateObfuscation31() }},
  527. {"S1 over uint16", func(o *amneziawg.Obfuscation31) { o.S1 = 70000 }},
  528. {"S2 over uint16", func(o *amneziawg.Obfuscation31) { o.S2 = 70000 }},
  529. {"negative Jc", func(o *amneziawg.Obfuscation31) { o.Jc = -1 }},
  530. {"negative Jmin and Jmax", func(o *amneziawg.Obfuscation31) { o.Jmin, o.Jmax = -5, -1 }},
  531. {"Jc over uint32", func(o *amneziawg.Obfuscation31) { o.Jc = 5000000000 }},
  532. {"I1 unknown tag", func(o *amneziawg.Obfuscation31) { o.I1 = "<rand 100>" }},
  533. {"I1 missing close", func(o *amneziawg.Obfuscation31) { o.I1 = "<r 100" }},
  534. {"I1 empty tag", func(o *amneziawg.Obfuscation31) { o.I1 = "<>" }},
  535. // The specs validateObfChain deliberately accepts must really apply.
  536. {"I1 chained tags", func(o *amneziawg.Obfuscation31) { o.I1 = "<b ff00><r 10>" }},
  537. {"I1 valueless tag", func(o *amneziawg.Obfuscation31) { o.I1 = "<t><rc 5>" }},
  538. {"I1 no tags at all", func(o *amneziawg.Obfuscation31) { o.I1 = "plain text" }},
  539. {"H ranges overlap", func(o *amneziawg.Obfuscation31) { o.H1, o.H2 = "100-200", "150-300" }},
  540. {"H1 equals the blank H3 default", func(o *amneziawg.Obfuscation31) { o.H1 = "3" }},
  541. {"H1-H4 = WireGuard's 1-4", func(o *amneziawg.Obfuscation31) { o.H1, o.H2, o.H3, o.H4 = "1", "2", "3", "4" }},
  542. // Separate cases: 1552+56 == 1608, so both maxima together trip the S1/S2 size rule.
  543. {"S1 and S3 at the 1700-byte bound", func(o *amneziawg.Obfuscation31) { o.S1, o.S3 = 1552, 1636 }},
  544. {"S2 at the 1700-byte bound", func(o *amneziawg.Obfuscation31) { o.S2 = 1608 }},
  545. {"Amnezia Premium set", func(o *amneziawg.Obfuscation31) {
  546. o.S1, o.S2, o.S3, o.S4 = 284, 659, 1045, 12
  547. o.H1, o.H2, o.H3, o.H4 = "1", "2", "3", "4"
  548. o.HeaderProtectionKey = "A2lG0Jm3m8u1WJt0qg3d7V6Qx8cFvH9pL1nR4sT6yZ0="
  549. }},
  550. }
  551. for i, tc := range cases {
  552. t.Run(tc.name, func(t *testing.T) {
  553. o := base
  554. tc.mut(&o)
  555. if err := amneziawg.ValidateObfuscation(o); err != nil {
  556. return // rejected before saving, which is the whole point
  557. }
  558. inst := amneziawg.Instance{
  559. Id: 88, InterfaceName: "awgcontract", ListenPort: 58900 + i,
  560. PrivateKey: priv, PublicKey: pub,
  561. Address: []string{"10.198.0.1/24"}, MTU: 1420,
  562. Obfuscation: o,
  563. Peers: []amneziawg.Peer{{
  564. Email: "[email protected]", PublicKey: peerPub,
  565. AllowedIPs: []string{"10.198.0.2/32"},
  566. }},
  567. }
  568. opts := DeviceOptions{
  569. HeaderProtectionKey: o.HeaderProtectionKey,
  570. ContentPaddingAddition: o.ContentPaddingAddition,
  571. RekeyAfterTime: o.RekeyAfterTime,
  572. RekeyTimeout: o.RekeyTimeout,
  573. RejectAfterTime: o.RejectAfterTime,
  574. KeepaliveTimeout: o.KeepaliveTimeout,
  575. MaxHandshakeAttempts: o.MaxHandshakeAttempts,
  576. RandomTrailers: o.RandomTrailers,
  577. DisableCookies: o.DisableCookies,
  578. }
  579. dev, err := newUnconfiguredDevice(inst, opts)
  580. if err != nil {
  581. t.Fatalf("newUnconfiguredDevice: %v", err)
  582. }
  583. defer dev.Close()
  584. conf, err := buildUAPIConfig(inst, opts)
  585. if err != nil {
  586. t.Fatalf("buildUAPIConfig: %v", err)
  587. }
  588. if err := dev.IpcSet(conf); err != nil {
  589. t.Fatalf("ValidateObfuscation accepted this config but amneziawg-go rejected it: %v", err)
  590. }
  591. })
  592. }
  593. }
  594. // Clearing HeaderProtectionKey on a running inbound must actually reach the
  595. // device: amneziawg-go treats an absent UAPI line as "keep the current value",
  596. // so an omitted key leaves header protection permanently on. Worse, the stale
  597. // key keeps the S1-S4 minimum alive, so lowering them then fails IpcSet with
  598. // -22 on every reconcile after the peers were already replaced.
  599. func TestBuildUAPIConfigClearedHeaderProtectionKeyIsSentAsZero(t *testing.T) {
  600. priv, _, err := wireguard.GenerateWireguardKeypair()
  601. if err != nil {
  602. t.Fatalf("generate keypair: %v", err)
  603. }
  604. inst := amneziawg.Instance{
  605. PrivateKey: priv,
  606. Obfuscation: amneziawg.Obfuscation31{S1: 20, S2: 20, S3: 20, S4: 20},
  607. }
  608. key, err := wireguard.GenerateWireguardPSK()
  609. if err != nil {
  610. t.Fatalf("generate header protection key: %v", err)
  611. }
  612. withKey, err := buildUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: key})
  613. if err != nil {
  614. t.Fatalf("buildUAPIConfig with a key: %v", err)
  615. }
  616. cleared, err := buildUAPIConfig(inst, DeviceOptions{})
  617. if err != nil {
  618. t.Fatalf("buildUAPIConfig with the key cleared: %v", err)
  619. }
  620. if withKey == cleared {
  621. t.Fatal("clearing the key produced an identical UAPI config, so the device would never see the change")
  622. }
  623. zero := "header_protection_key=" + strings.Repeat("0", 64) + "\n"
  624. if !strings.Contains(cleared, zero) {
  625. t.Fatalf("cleared config must carry the all-zero key, got:\n%s", cleared)
  626. }
  627. if strings.Contains(withKey, zero) {
  628. t.Fatalf("a configured key must not be emitted as zero, got:\n%s", withKey)
  629. }
  630. }