|
@@ -0,0 +1,257 @@
|
|
|
|
|
+package amneziawgnet
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "net"
|
|
|
|
|
+ "net/netip"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+ "time"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// newDeadUDPSession builds a socks5UDPSession over real but already-closed
|
|
|
|
|
+// sockets, so pump's receive fails immediately and its teardown runs at once.
|
|
|
|
|
+func newDeadUDPSession(t *testing.T) *socks5UDPSession {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ peer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("listen udp: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = peer.Close() })
|
|
|
|
|
+ udpConn, err := net.DialUDP("udp", nil, peer.LocalAddr().(*net.UDPAddr))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("dial udp: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ ctrlLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("listen tcp: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = ctrlLn.Close() })
|
|
|
|
|
+ ctrl, err := net.Dial("tcp", ctrlLn.Addr().String())
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("dial tcp: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = udpConn.Close()
|
|
|
|
|
+ return &socks5UDPSession{ctrl: ctrl, udpConn: udpConn}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestUDPRelayPumpOnlyRetiresItsOwnSession pins the flow that survives a
|
|
|
|
|
+// duplicate associate: a losing pump must not evict the published session.
|
|
|
|
|
+func TestUDPRelayPumpOnlyRetiresItsOwnSession(t *testing.T) {
|
|
|
|
|
+ relay := NewUDPRelay(SocksRelay{Addr: "127.0.0.1:1", Password: "x"}, nil)
|
|
|
|
|
+ src := netip.MustParseAddrPort("10.8.1.5:51820")
|
|
|
|
|
+
|
|
|
|
|
+ live := newDeadUDPSession(t)
|
|
|
|
|
+ superseded := newDeadUDPSession(t)
|
|
|
|
|
+ relay.sessions[src] = live
|
|
|
|
|
+
|
|
|
|
|
+ // Returns as soon as receive fails on the closed socket, so no wait is needed.
|
|
|
|
|
+ relay.pump(src, superseded)
|
|
|
|
|
+
|
|
|
|
|
+ got, ok := relay.sessions[src]
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ t.Fatal("live session was evicted: a retiring pump deleted src's entry regardless of which session held it")
|
|
|
|
|
+ }
|
|
|
|
|
+ if got != live {
|
|
|
|
|
+ t.Fatalf("sessions[%v] = %p, want the live session %p", src, got, live)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestUDPRelayCloseDropsEverySession keeps Close's contract explicit now that
|
|
|
|
|
+// pump's teardown is conditional on still owning the key.
|
|
|
|
|
+func TestUDPRelayCloseDropsEverySession(t *testing.T) {
|
|
|
|
|
+ relay := NewUDPRelay(SocksRelay{Addr: "127.0.0.1:1", Password: "x"}, nil)
|
|
|
|
|
+ for _, s := range []string{"10.8.1.5:51820", "10.8.1.6:2000"} {
|
|
|
|
|
+ relay.sessions[netip.MustParseAddrPort(s)] = newDeadUDPSession(t)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ relay.Close()
|
|
|
|
|
+
|
|
|
|
|
+ if n := len(relay.sessions); n != 0 {
|
|
|
|
|
+ t.Fatalf("Close left %d sessions behind, want 0", n)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// tcpPair returns a connected pair of real loopback TCP conns; net.Pipe would
|
|
|
|
|
+// not do, since these tests turn on CloseWrite, which it does not implement.
|
|
|
|
|
+func tcpPair(t *testing.T) (client, server net.Conn) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("listen: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer ln.Close()
|
|
|
|
|
+
|
|
|
|
|
+ type accepted struct {
|
|
|
|
|
+ conn net.Conn
|
|
|
|
|
+ err error
|
|
|
|
|
+ }
|
|
|
|
|
+ ch := make(chan accepted, 1)
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ c, err := ln.Accept()
|
|
|
|
|
+ ch <- accepted{c, err}
|
|
|
|
|
+ }()
|
|
|
|
|
+ client, err = net.Dial("tcp", ln.Addr().String())
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("dial: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ got := <-ch
|
|
|
|
|
+ if got.err != nil {
|
|
|
|
|
+ t.Fatalf("accept: %v", got.err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = client.Close(); _ = got.conn.Close() })
|
|
|
|
|
+ return client, got.conn
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPipeBothWaysDeliversReplyAfterHalfClose is the half-close regression: a
|
|
|
|
|
+// client that shuts down its write side must still receive the full response.
|
|
|
|
|
+func TestPipeBothWaysDeliversReplyAfterHalfClose(t *testing.T) {
|
|
|
|
|
+ const request = "GET / HTTP/1.0\r\n\r\n"
|
|
|
|
|
+ const response = "the reply that arrives only after the request is complete"
|
|
|
|
|
+
|
|
|
|
|
+ client, a := tcpPair(t)
|
|
|
|
|
+ b, server := tcpPair(t)
|
|
|
|
|
+ go pipeBothWays(a, b)
|
|
|
|
|
+
|
|
|
|
|
+ if _, err := client.Write([]byte(request)); err != nil {
|
|
|
|
|
+ t.Fatalf("client write: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ // The half-close the old relay treated as "tear the whole pair down".
|
|
|
|
|
+ if err := client.(*net.TCPConn).CloseWrite(); err != nil {
|
|
|
|
|
+ t.Fatalf("client CloseWrite: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _ = server.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
|
|
|
+ gotReq, err := io.ReadAll(server)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("server read: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if string(gotReq) != request {
|
|
|
|
|
+ t.Fatalf("server got request %q, want %q", gotReq, request)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if _, err := server.Write([]byte(response)); err != nil {
|
|
|
|
|
+ t.Fatalf("server write: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := server.(*net.TCPConn).CloseWrite(); err != nil {
|
|
|
|
|
+ t.Fatalf("server CloseWrite: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _ = client.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
|
|
|
+ gotResp, err := io.ReadAll(client)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("client read: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if string(gotResp) != response {
|
|
|
|
|
+ t.Fatalf("client got response %q, want %q: the reply was cut off by the half-close", gotResp, response)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestPipeBothWaysClosesWhenBothSidesFinish keeps the teardown contract: both
|
|
|
|
|
+// directions ending must return, not hang on the idle bound.
|
|
|
|
|
+func TestPipeBothWaysClosesWhenBothSidesFinish(t *testing.T) {
|
|
|
|
|
+ client, a := tcpPair(t)
|
|
|
|
|
+ b, server := tcpPair(t)
|
|
|
|
|
+
|
|
|
|
|
+ done := make(chan struct{})
|
|
|
|
|
+ go func() { defer close(done); pipeBothWays(a, b) }()
|
|
|
|
|
+
|
|
|
|
|
+ _ = client.(*net.TCPConn).CloseWrite()
|
|
|
|
|
+ _, _ = io.ReadAll(server)
|
|
|
|
|
+ _ = server.(*net.TCPConn).CloseWrite()
|
|
|
|
|
+ _, _ = io.ReadAll(client)
|
|
|
|
|
+
|
|
|
|
|
+ select {
|
|
|
|
|
+ case <-done:
|
|
|
|
|
+ case <-time.After(10 * time.Second):
|
|
|
|
|
+ t.Fatal("pipeBothWays did not return after both directions ended")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// liveUDPSession returns a session whose udpConn is connected to the returned
|
|
|
|
|
+// peer, so a test can hand receive() one exact reply datagram.
|
|
|
|
|
+func liveUDPSession(t *testing.T) (*socks5UDPSession, *net.UDPConn) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ peer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("listen udp: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = peer.Close() })
|
|
|
|
|
+ udpConn, err := net.DialUDP("udp", nil, peer.LocalAddr().(*net.UDPAddr))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("dial udp: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = udpConn.Close() })
|
|
|
|
|
+ return &socks5UDPSession{udpConn: udpConn}, peer
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// sendReply delivers one raw datagram to sess's socket.
|
|
|
|
|
+func sendReply(t *testing.T, sess *socks5UDPSession, peer *net.UDPConn, datagram []byte) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ if _, err := peer.WriteToUDP(datagram, sess.udpConn.LocalAddr().(*net.UDPAddr)); err != nil {
|
|
|
|
|
+ t.Fatalf("write reply: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = sess.udpConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestSocks5ReceiveDecodesReplyAddressTypes covers all three ATYP forms; the
|
|
|
|
|
+// domain form used to misread its own length byte and never skip the name.
|
|
|
|
|
+func TestSocks5ReceiveDecodesReplyAddressTypes(t *testing.T) {
|
|
|
|
|
+ tests := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ addrPart []byte
|
|
|
|
|
+ wantAddr string
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"IPv4", []byte{0x01, 10, 0, 0, 7}, "10.0.0.7"},
|
|
|
|
|
+ {"IPv6", append([]byte{0x04}, netip.MustParseAddr("2001:db8::5").AsSlice()...), "2001:db8::5"},
|
|
|
|
|
+ {"domain holding a literal", append([]byte{0x03, 8}, []byte("10.0.0.9")...), "10.0.0.9"},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, tt := range tests {
|
|
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
+ sess, peer := liveUDPSession(t)
|
|
|
|
|
+ payload := []byte("the-actual-datagram-payload")
|
|
|
|
|
+ datagram := append([]byte{0x00, 0x00, 0x00}, tt.addrPart...)
|
|
|
|
|
+ datagram = append(datagram, 0x1f, 0x90) // port 8080
|
|
|
|
|
+ datagram = append(datagram, payload...)
|
|
|
|
|
+ sendReply(t, sess, peer, datagram)
|
|
|
|
|
+
|
|
|
|
|
+ buf := make([]byte, 4096)
|
|
|
|
|
+ from, got, err := sess.receive(buf)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("receive: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ want := netip.AddrPortFrom(netip.MustParseAddr(tt.wantAddr), 8080)
|
|
|
|
|
+ if from != want {
|
|
|
|
|
+ t.Errorf("source = %v, want %v", from, want)
|
|
|
|
|
+ }
|
|
|
|
|
+ if string(got) != string(payload) {
|
|
|
|
|
+ t.Errorf("payload = %q, want %q", got, payload)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestSocks5ReceiveRejectsTruncatedReplies pins that a short datagram is an
|
|
|
|
|
+// error, not a slice-bounds panic in the relay's own pump goroutine.
|
|
|
|
|
+func TestSocks5ReceiveRejectsTruncatedReplies(t *testing.T) {
|
|
|
|
|
+ tests := []struct {
|
|
|
|
|
+ name string
|
|
|
|
|
+ datagram []byte
|
|
|
|
|
+ }{
|
|
|
|
|
+ {"header only, IPv4 announced", []byte{0x00, 0x00, 0x00, 0x01}},
|
|
|
|
|
+ {"IPv4 address cut short", []byte{0x00, 0x00, 0x00, 0x01, 10, 0}},
|
|
|
|
|
+ {"IPv6 address cut short", []byte{0x00, 0x00, 0x00, 0x04, 0x20, 0x01}},
|
|
|
|
|
+ {"domain length past the end", []byte{0x00, 0x00, 0x00, 0x03, 40, 'a', 'b'}},
|
|
|
|
|
+ {"address complete but port missing", []byte{0x00, 0x00, 0x00, 0x01, 10, 0, 0, 7}},
|
|
|
|
|
+ {"unsupported address type", []byte{0x00, 0x00, 0x00, 0x09, 1, 2, 3, 4, 0, 80}},
|
|
|
|
|
+ }
|
|
|
|
|
+ for _, tt := range tests {
|
|
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
+ sess, peer := liveUDPSession(t)
|
|
|
|
|
+ sendReply(t, sess, peer, tt.datagram)
|
|
|
|
|
+ buf := make([]byte, 4096)
|
|
|
|
|
+ if _, _, err := sess.receive(buf); err == nil {
|
|
|
|
|
+ t.Fatal("receive accepted a malformed datagram instead of returning an error")
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|