device_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  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. if strings.Contains(conf, "header_protection_key=") || strings.Contains(conf, "content_padding_addition=") {
  177. t.Fatalf("empty DeviceOptions must not emit AWG 3.0 lines, got:\n%s", conf)
  178. }
  179. key, err := wireguard.GenerateWireguardPSK()
  180. if err != nil {
  181. t.Fatalf("generate header protection key: %v", err)
  182. }
  183. conf, err = buildUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: key, ContentPaddingAddition: "20-40"})
  184. if err != nil {
  185. t.Fatalf("buildUAPIConfig with AWG 3.0 options: %v", err)
  186. }
  187. if !strings.Contains(conf, "header_protection_key=") {
  188. t.Errorf("expected a header_protection_key= line, got:\n%s", conf)
  189. }
  190. if !strings.Contains(conf, "content_padding_addition=20-40\n") {
  191. t.Errorf("expected a content_padding_addition=20-40 line, got:\n%s", conf)
  192. }
  193. if _, err := buildUAPIConfig(inst, DeviceOptions{HeaderProtectionKey: "not-a-valid-base64-key"}); err == nil {
  194. t.Fatal("a malformed HeaderProtectionKey must be rejected, not silently passed through")
  195. }
  196. }
  197. // TestNewDeviceHeaderProtectionAndContentPaddingRoundTrip is the real proof
  198. // behind AmneziaWG 3.0's admin-facing HeaderProtectionKey/
  199. // ContentPaddingAddition fields: a genuine amneziawg-go client, configured
  200. // with matching header_protection_key/content_padding_addition UAPI lines
  201. // (S1-S4 all >= 12, the hard requirement amneziawg-go's own IpcSet enforces
  202. // for header protection), completes a real handshake against a Device built
  203. // via NewDevice/DeviceOptions and exchanges real application data both
  204. // directions through it. This is more than a handshake-completed check --
  205. // it also confirms actual payload bytes survive content padding on both the
  206. // send and receive sides, the specific area a third-party AmneziaWG
  207. // installer project's docs flagged a past interop concern for (see the
  208. // migration plan's own risk note); it is not a substitute for real-VPS
  209. // verification against the official client, but it is the cheapest
  210. // available local check against a regression in either engine's own padding
  211. // handling.
  212. func TestNewDeviceHeaderProtectionAndContentPaddingRoundTrip(t *testing.T) {
  213. serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
  214. if err != nil {
  215. t.Fatalf("generate server keypair: %v", err)
  216. }
  217. clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
  218. if err != nil {
  219. t.Fatalf("generate client keypair: %v", err)
  220. }
  221. headerProtectionKey, err := wireguard.GenerateWireguardPSK()
  222. if err != nil {
  223. t.Fatalf("generate header protection key: %v", err)
  224. }
  225. const listenPort = 58713 // fixed loopback test port, distinct from the handshake test above
  226. const contentPaddingAddition = "20-40"
  227. inst := amneziawg.Instance{
  228. Id: 2,
  229. InterfaceName: "awgtest2",
  230. ListenPort: listenPort,
  231. PrivateKey: serverPriv,
  232. PublicKey: serverPub,
  233. Address: []string{"10.202.0.1/24"},
  234. MTU: 1420,
  235. Obfuscation: amneziawg.Obfuscation31{
  236. Jc: 4, Jmin: 40, Jmax: 70,
  237. S1: 20, S2: 30, S3: 20, S4: 20, // all >= 12, required for header protection
  238. },
  239. Peers: []amneziawg.Peer{{
  240. Email: "[email protected]",
  241. PublicKey: clientPub,
  242. AllowedIPs: []string{"10.202.0.2/32"},
  243. }},
  244. }
  245. opts := DeviceOptions{
  246. HeaderProtectionKey: headerProtectionKey,
  247. ContentPaddingAddition: contentPaddingAddition,
  248. }
  249. dev, err := newUnconfiguredDevice(inst, opts)
  250. if err != nil {
  251. t.Fatalf("newUnconfiguredDevice: %v", err)
  252. }
  253. defer dev.Close()
  254. const wantRequest = "hello from client"
  255. const wantReply = "hello from server"
  256. serverDone := make(chan error, 1)
  257. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  258. defer conn.Close()
  259. buf := make([]byte, len(wantRequest))
  260. if _, err := io.ReadFull(conn, buf); err != nil {
  261. serverDone <- fmt.Errorf("server read: %w", err)
  262. return
  263. }
  264. if string(buf) != wantRequest {
  265. serverDone <- fmt.Errorf("server got %q, want %q", buf, wantRequest)
  266. return
  267. }
  268. if _, err := conn.Write([]byte(wantReply)); err != nil {
  269. serverDone <- fmt.Errorf("server write: %w", err)
  270. return
  271. }
  272. serverDone <- nil
  273. })
  274. // Configure (IpcSet) must come after AttachTCPForwarder -- see
  275. // newUnconfiguredDevice's doc comment.
  276. if err := dev.Configure(inst, opts); err != nil {
  277. t.Fatalf("Configure: %v", err)
  278. }
  279. clientTun, clientNet, err := netstack.CreateNetTUN(
  280. []netip.Addr{netip.MustParseAddr("10.202.0.2")},
  281. []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
  282. if err != nil {
  283. t.Fatalf("client CreateNetTUN: %v", err)
  284. }
  285. clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
  286. defer clientDev.Close()
  287. clientPrivHex, err := wireguard.KeyToHex(clientPriv)
  288. if err != nil {
  289. t.Fatalf("client key to hex: %v", err)
  290. }
  291. serverPubHex, err := wireguard.KeyToHex(serverPub)
  292. if err != nil {
  293. t.Fatalf("server key to hex: %v", err)
  294. }
  295. headerProtectionKeyHex, err := wireguard.KeyToHex(headerProtectionKey)
  296. if err != nil {
  297. t.Fatalf("header protection key to hex: %v", err)
  298. }
  299. clientConf := fmt.Sprintf(
  300. "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",
  301. clientPrivHex, headerProtectionKeyHex, contentPaddingAddition, serverPubHex, listenPort)
  302. if err := clientDev.IpcSet(clientConf); err != nil {
  303. t.Fatalf("client IpcSet: %v", err)
  304. }
  305. if err := clientDev.Up(); err != nil {
  306. t.Fatalf("client Up: %v", err)
  307. }
  308. dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  309. defer cancel()
  310. var conn net.Conn
  311. for {
  312. c, dialErr := clientNet.DialContext(dialCtx, "tcp", "10.202.9.9:9999")
  313. if dialErr == nil {
  314. conn = c
  315. break
  316. }
  317. select {
  318. case <-dialCtx.Done():
  319. t.Fatalf("client dial never succeeded: %v", dialErr)
  320. case <-time.After(100 * time.Millisecond):
  321. }
  322. }
  323. defer conn.Close()
  324. if _, err := conn.Write([]byte(wantRequest)); err != nil {
  325. t.Fatalf("client write: %v", err)
  326. }
  327. if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
  328. t.Fatalf("SetReadDeadline: %v", err)
  329. }
  330. reply := make([]byte, len(wantReply))
  331. if _, err := io.ReadFull(conn, reply); err != nil {
  332. t.Fatalf("client read reply: %v", err)
  333. }
  334. if string(reply) != wantReply {
  335. t.Fatalf("client got reply %q, want %q", reply, wantReply)
  336. }
  337. select {
  338. case err := <-serverDone:
  339. if err != nil {
  340. t.Fatalf("server side: %v", err)
  341. }
  342. case <-time.After(5 * time.Second):
  343. t.Fatal("timed out waiting for the server side to finish")
  344. }
  345. }
  346. func TestBuildUAPIConfigRandomTrailersAndDisableCookiesLines(t *testing.T) {
  347. priv, _, err := wireguard.GenerateWireguardKeypair()
  348. if err != nil {
  349. t.Fatalf("generate keypair: %v", err)
  350. }
  351. inst := amneziawg.Instance{PrivateKey: priv}
  352. // Unlike HeaderProtectionKey/ContentPaddingAddition, these two lines
  353. // must always be present -- see DeviceOptions.RandomTrailers's own doc
  354. // comment on why an absent line (instead of an explicit "false") would
  355. // break the reconfigure-in-place diff for a true->false edit.
  356. conf, err := buildUAPIConfig(inst, DeviceOptions{})
  357. if err != nil {
  358. t.Fatalf("buildUAPIConfig with empty options: %v", err)
  359. }
  360. if !strings.Contains(conf, "random_trailers=false\n") {
  361. t.Errorf("expected an explicit random_trailers=false line even when unset, got:\n%s", conf)
  362. }
  363. if !strings.Contains(conf, "disable_cookies=false\n") {
  364. t.Errorf("expected an explicit disable_cookies=false line even when unset, got:\n%s", conf)
  365. }
  366. conf, err = buildUAPIConfig(inst, DeviceOptions{RandomTrailers: true, DisableCookies: true})
  367. if err != nil {
  368. t.Fatalf("buildUAPIConfig with both enabled: %v", err)
  369. }
  370. if !strings.Contains(conf, "random_trailers=true\n") {
  371. t.Errorf("expected a random_trailers=true line, got:\n%s", conf)
  372. }
  373. if !strings.Contains(conf, "disable_cookies=true\n") {
  374. t.Errorf("expected a disable_cookies=true line, got:\n%s", conf)
  375. }
  376. }
  377. // TestNewDeviceRandomTrailersAndDisableCookiesRoundTrip is the real proof
  378. // behind AmneziaWG 3.1's two new device-wide toggles: a genuine amneziawg-go
  379. // client with matching random_trailers=true/disable_cookies=true UAPI lines
  380. // completes a real handshake against a Device built via NewDevice/
  381. // DeviceOptions and exchanges real application data both directions through
  382. // it. This specifically exercises amneziawg-go's receive.go size-matching
  383. // path for RandomTrailers (device_test.go's HeaderProtection test doesn't
  384. // enable it), which only accepts a message when
  385. // `size == expectedSize || randomTrailers && size > expectedSize` -- proof
  386. // that setting it on both ends really does interoperate, not just that
  387. // IpcSet accepts the value.
  388. func TestNewDeviceRandomTrailersAndDisableCookiesRoundTrip(t *testing.T) {
  389. serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
  390. if err != nil {
  391. t.Fatalf("generate server keypair: %v", err)
  392. }
  393. clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
  394. if err != nil {
  395. t.Fatalf("generate client keypair: %v", err)
  396. }
  397. const listenPort = 58721 // fixed loopback test port, distinct from every other test in this package
  398. inst := amneziawg.Instance{
  399. Id: 3,
  400. InterfaceName: "awgtest3",
  401. ListenPort: listenPort,
  402. PrivateKey: serverPriv,
  403. PublicKey: serverPub,
  404. Address: []string{"10.203.0.1/24"},
  405. MTU: 1420,
  406. Peers: []amneziawg.Peer{{
  407. Email: "[email protected]",
  408. PublicKey: clientPub,
  409. AllowedIPs: []string{"10.203.0.2/32"},
  410. }},
  411. }
  412. opts := DeviceOptions{RandomTrailers: true, DisableCookies: true}
  413. dev, err := newUnconfiguredDevice(inst, opts)
  414. if err != nil {
  415. t.Fatalf("newUnconfiguredDevice: %v", err)
  416. }
  417. defer dev.Close()
  418. const wantRequest = "hello from client, with a trailer"
  419. const wantReply = "hello from server, with a trailer"
  420. serverDone := make(chan error, 1)
  421. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  422. defer conn.Close()
  423. buf := make([]byte, len(wantRequest))
  424. if _, err := io.ReadFull(conn, buf); err != nil {
  425. serverDone <- fmt.Errorf("server read: %w", err)
  426. return
  427. }
  428. if string(buf) != wantRequest {
  429. serverDone <- fmt.Errorf("server got %q, want %q", buf, wantRequest)
  430. return
  431. }
  432. if _, err := conn.Write([]byte(wantReply)); err != nil {
  433. serverDone <- fmt.Errorf("server write: %w", err)
  434. return
  435. }
  436. serverDone <- nil
  437. })
  438. // Configure (IpcSet) must come after AttachTCPForwarder -- see
  439. // newUnconfiguredDevice's doc comment.
  440. if err := dev.Configure(inst, opts); err != nil {
  441. t.Fatalf("Configure: %v", err)
  442. }
  443. clientTun, clientNet, err := netstack.CreateNetTUN(
  444. []netip.Addr{netip.MustParseAddr("10.203.0.2")},
  445. []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
  446. if err != nil {
  447. t.Fatalf("client CreateNetTUN: %v", err)
  448. }
  449. clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
  450. defer clientDev.Close()
  451. clientPrivHex, err := wireguard.KeyToHex(clientPriv)
  452. if err != nil {
  453. t.Fatalf("client key to hex: %v", err)
  454. }
  455. serverPubHex, err := wireguard.KeyToHex(serverPub)
  456. if err != nil {
  457. t.Fatalf("server key to hex: %v", err)
  458. }
  459. clientConf := fmt.Sprintf(
  460. "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",
  461. clientPrivHex, serverPubHex, listenPort)
  462. if err := clientDev.IpcSet(clientConf); err != nil {
  463. t.Fatalf("client IpcSet: %v", err)
  464. }
  465. if err := clientDev.Up(); err != nil {
  466. t.Fatalf("client Up: %v", err)
  467. }
  468. dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  469. defer cancel()
  470. var conn net.Conn
  471. for {
  472. c, dialErr := clientNet.DialContext(dialCtx, "tcp", "10.203.9.9:9999")
  473. if dialErr == nil {
  474. conn = c
  475. break
  476. }
  477. select {
  478. case <-dialCtx.Done():
  479. t.Fatalf("client dial never succeeded: %v", dialErr)
  480. case <-time.After(100 * time.Millisecond):
  481. }
  482. }
  483. defer conn.Close()
  484. if _, err := conn.Write([]byte(wantRequest)); err != nil {
  485. t.Fatalf("client write: %v", err)
  486. }
  487. if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
  488. t.Fatalf("SetReadDeadline: %v", err)
  489. }
  490. reply := make([]byte, len(wantReply))
  491. if _, err := io.ReadFull(conn, reply); err != nil {
  492. t.Fatalf("client read reply: %v", err)
  493. }
  494. if string(reply) != wantReply {
  495. t.Fatalf("client got reply %q, want %q", reply, wantReply)
  496. }
  497. select {
  498. case err := <-serverDone:
  499. if err != nil {
  500. t.Fatalf("server side: %v", err)
  501. }
  502. case <-time.After(5 * time.Second):
  503. t.Fatal("timed out waiting for the server side to finish")
  504. }
  505. }