1
0

device_test.go 24 KB

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