From cc54025bbde705dd445b5686ee5a26d54a1f4600 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 8 May 2024 16:35:24 -0400 Subject: [PATCH 01/54] Start PacketDialer --- transport/socks5/packet_dialer.go | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 transport/socks5/packet_dialer.go diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go new file mode 100644 index 00000000..81e6d477 --- /dev/null +++ b/transport/socks5/packet_dialer.go @@ -0,0 +1,51 @@ +// Copyright 2024 Jigsaw Operations LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package socks5 + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/Jigsaw-Code/outline-sdk/transport" +) + +type PacketDialer struct { + se transport.StreamEndpoint + pe transport.PacketEndpoint + cred *credentials +} + +var _ transport.PacketDialer = (*PacketDialer)(nil) + +// NewPacketDialer creates a [transport.PacketDialer] that routes connections to a SOCKS5 +// proxy listening at the given endpoint. +func NewPacketDialer(streamEndpoint transport.StreamEndpoint, packetEndpoint transport.PacketEndpoint) (transport.PacketDialer, error) { + if streamEndpoint == nil || packetEndpoint == nil { + return nil, errors.New("must specify both endpoints") + } + return &PacketDialer{se: streamEndpoint, pe: packetEndpoint, cred: nil}, nil +} + +// DialPacket creates a packet [net.Conn] via SOCKS5. +func (pd *PacketDialer) DialPacket(ctx context.Context, remoteAddr string) (net.Conn, error) { + sc, err := pd.se.ConnectStream(ctx) + if err != nil { + return nil, fmt.Errorf("failed to connect to stream endpoint: %w", err) + } + + return nil, errors.ErrUnsupported +} From a285b132136e3dfd093ed8c48e7e2bc40146c000 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 8 May 2024 19:02:56 -0400 Subject: [PATCH 02/54] Add stuff --- transport/socks5/packet_dialer.go | 70 +++++++++++--- transport/socks5/socks5.go | 7 ++ transport/socks5/stream_dialer.go | 129 +++++++++++++++---------- transport/socks5/stream_dialer_test.go | 12 +-- x/config/socks5.go | 2 +- 5 files changed, 146 insertions(+), 74 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index 81e6d477..a45604a7 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -19,33 +19,71 @@ import ( "errors" "fmt" "net" + "time" "github.com/Jigsaw-Code/outline-sdk/transport" ) -type PacketDialer struct { - se transport.StreamEndpoint - pe transport.PacketEndpoint - cred *credentials +type packetConn struct { + dstAddr net.Addr + pc net.Conn + sc transport.StreamConn } -var _ transport.PacketDialer = (*PacketDialer)(nil) +var _ net.Conn = (*packetConn)(nil) -// NewPacketDialer creates a [transport.PacketDialer] that routes connections to a SOCKS5 -// proxy listening at the given endpoint. -func NewPacketDialer(streamEndpoint transport.StreamEndpoint, packetEndpoint transport.PacketEndpoint) (transport.PacketDialer, error) { - if streamEndpoint == nil || packetEndpoint == nil { - return nil, errors.New("must specify both endpoints") - } - return &PacketDialer{se: streamEndpoint, pe: packetEndpoint, cred: nil}, nil +func (c *packetConn) LocalAddr() net.Addr { + // TODO: Is this right? + return c.pc.LocalAddr() +} + +func (c *packetConn) RemoteAddr() net.Addr { + return c.dstAddr +} + +func (c *packetConn) SetDeadline(t time.Time) error { + return c.pc.SetDeadline(t) +} + +func (c *packetConn) SetReadDeadline(t time.Time) error { + return c.pc.SetReadDeadline(t) +} + +func (c *packetConn) SetWriteDeadline(t time.Time) error { + return c.pc.SetWriteDeadline(t) +} + +func (c *packetConn) Read(b []byte) (int, error) { + // TODO: read header + return c.pc.Read(b) +} + +func (c *packetConn) Write(b []byte) (int, error) { + // TODO: write header + return c.pc.Write(b) +} + +func (c *packetConn) Close() error { + return errors.Join(c.sc.Close(), c.pc.Close()) } // DialPacket creates a packet [net.Conn] via SOCKS5. -func (pd *PacketDialer) DialPacket(ctx context.Context, remoteAddr string) (net.Conn, error) { - sc, err := pd.se.ConnectStream(ctx) +func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, error) { + netDstAddr, err := transport.MakeNetAddr("udp", dstAddr) + if err != nil { + return nil, fmt.Errorf("failed to parse address: %w", err) + } + + sc, bindAddr, err := d.request(ctx, CmdUDPAssociate, dstAddr) + if err != nil { + return nil, err + } + + pc, err := d.pd.DialPacket(ctx, bindAddr) if err != nil { - return nil, fmt.Errorf("failed to connect to stream endpoint: %w", err) + sc.Close() + return nil, fmt.Errorf("failed to connect to packet endpoint: %w", err) } - return nil, errors.ErrUnsupported + return &packetConn{netDstAddr, pc, sc}, nil } diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 59e2bf06..6b4f6404 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -37,6 +37,13 @@ const ( ErrAddressTypeNotSupported = ReplyCode(0x08) ) +// SOCKS5 commands, from https://datatracker.ietf.org/doc/html/rfc1928#section-4. +const ( + CmdConnect = byte(1) + CmdBind = byte(2) + CmdUDPAssociate = byte(1) +) + // SOCKS5 authentication methods, as specified in https://datatracker.ietf.org/doc/html/rfc1928#section-3 const ( authMethodNoAuth = 0x00 diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index b1839a7c..250963ba 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -16,9 +16,13 @@ package socks5 import ( "context" + "encoding/binary" "errors" "fmt" "io" + "net" + "net/netip" + "strconv" "github.com/Jigsaw-Code/outline-sdk/transport" ) @@ -30,23 +34,24 @@ type credentials struct { password []byte } -// NewStreamDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 +// NewDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 // proxy listening at the given [transport.StreamEndpoint]. -func NewStreamDialer(endpoint transport.StreamEndpoint) (*StreamDialer, error) { - if endpoint == nil { +func NewDialer(streamEndpoint transport.StreamEndpoint) (*Dialer, error) { + if streamEndpoint == nil { return nil, errors.New("argument endpoint must not be nil") } - return &StreamDialer{proxyEndpoint: endpoint, cred: nil}, nil + return &Dialer{se: streamEndpoint, cred: nil}, nil } -type StreamDialer struct { - proxyEndpoint transport.StreamEndpoint - cred *credentials +type Dialer struct { + se transport.StreamEndpoint + pd transport.PacketDialer + cred *credentials } -var _ transport.StreamDialer = (*StreamDialer)(nil) +var _ transport.StreamDialer = (*Dialer)(nil) -func (c *StreamDialer) SetCredentials(username, password []byte) error { +func (d *Dialer) SetCredentials(username, password []byte) error { if len(username) > 255 { return errors.New("username exceeds 255 bytes") } @@ -61,19 +66,18 @@ func (c *StreamDialer) SetCredentials(username, password []byte) error { return errors.New("password must be at least 1 byte") } - c.cred = &credentials{username: username, password: password} + d.cred = &credentials{username: username, password: password} return nil } -// DialStream implements [transport.StreamDialer].DialStream using SOCKS5. -// It will send the auth method, auth credentials (if auth is chosen), and -// the connect requests in one packet, to avoid an additional roundtrip. -// The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which -// you can check against the error constants in this package using [errors.Is]. -func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) { - proxyConn, err := c.proxyEndpoint.ConnectStream(ctx) +func (d *Dialer) EnablePacket(packetDialer transport.PacketDialer) { + d.pd = packetDialer +} + +func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transport.StreamConn, string, error) { + proxyConn, err := d.se.ConnectStream(ctx) if err != nil { - return nil, fmt.Errorf("could not connect to SOCKS5 proxy: %w", err) + return nil, "", fmt.Errorf("could not connect to SOCKS5 proxy: %w", err) } dialSuccess := false defer func() { @@ -92,7 +96,7 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans var buffer [(1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256]byte var b []byte - if c.cred == nil { + if d.cred == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) // +----+----------+----------+ // |VER | NMETHODS | METHODS | @@ -112,32 +116,32 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ b = append(b, 1) - b = append(b, byte(len(c.cred.username))) - b = append(b, c.cred.username...) - b = append(b, byte(len(c.cred.password))) - b = append(b, c.cred.password...) + b = append(b, byte(len(d.cred.username))) + b = append(b, d.cred.username...) + b = append(b, byte(len(d.cred.password))) + b = append(b, d.cred.password...) } - // Connect request: - // VER = 5, CMD = 1 (connect), RSV = 0, DST.ADDR, DST.PORT + // CMD Request: + // VER = 5, CMD = cmd, RSV = 0, DST.ADDR, DST.PORT // +----+-----+-------+------+----------+----------+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - b = append(b, 5, 1, 0) + b = append(b, 5, cmd, 0) // TODO: Probably more memory efficient if remoteAddr is added to the buffer directly. - b, err = appendSOCKS5Address(b, remoteAddr) + b, err = appendSOCKS5Address(b, dstAddr) if err != nil { - return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) + return nil, "", fmt.Errorf("failed to create SOCKS5 address: %w", err) } - // We merge the method and connect requests and only perform one write + // We merge the method and CMD requests and only perform one write // because we send a single authentication method, so there's no point // in waiting for the response. This eliminates a roundtrip. _, err = proxyConn.Write(b) if err != nil { - return nil, fmt.Errorf("failed to write combined SOCKS5 request: %w", err) + return nil, "", fmt.Errorf("failed to write combined SOCKS5 request: %w", err) } // Reading the response: @@ -150,10 +154,10 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // buffer[0]: VER, buffer[1]: METHOD // Reuse buffer for better performance. if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { - return nil, fmt.Errorf("failed to read method server response: %w", err) + return nil, "", fmt.Errorf("failed to read method server response: %w", err) } if buffer[0] != 5 { - return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) + return nil, "", fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } switch buffer[1] { @@ -170,16 +174,16 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // VER = 1 means the server should be expecting username/password authentication. // buffer[2]: VER, buffer[3]: STATUS if _, err = io.ReadFull(proxyConn, buffer[2:4]); err != nil { - return nil, fmt.Errorf("failed to read authentication version and status: %w", err) + return nil, "", fmt.Errorf("failed to read authentication version and status: %w", err) } if buffer[2] != 1 { - return nil, fmt.Errorf("invalid authentication version %v. Expected 1", buffer[2]) + return nil, "", fmt.Errorf("invalid authentication version %v. Expected 1", buffer[2]) } if buffer[3] != 0 { - return nil, fmt.Errorf("authentication failed: %v", buffer[3]) + return nil, "", fmt.Errorf("authentication failed: %v", buffer[3]) } default: - return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1]) + return nil, "", fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1]) } // 3. Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT). @@ -194,44 +198,67 @@ func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (trans // buffer[2]: RSV // buffer[3]: ATYP if _, err = io.ReadFull(proxyConn, buffer[:4]); err != nil { - return nil, fmt.Errorf("failed to read connect server response: %w", err) + return nil, "", fmt.Errorf("failed to read connect server response: %w", err) } if buffer[0] != 5 { - return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) + return nil, "", fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } // if REP is not 0, it means the server returned an error. if buffer[1] != 0 { - return nil, ReplyCode(buffer[1]) + return nil, "", ReplyCode(buffer[1]) } - // 4. Read address and length + // 4. Read BND.ADDR. + host := "" var bndAddrLen int switch buffer[3] { case addrTypeIPv4: - bndAddrLen = 4 + if _, err := io.ReadFull(proxyConn, buffer[:4]); err != nil { + return nil, "", fmt.Errorf("failed to read bound IPv4 address: %w", err) + } + host = netip.AddrFrom4([4]byte(buffer[:4])).String() case addrTypeIPv6: - bndAddrLen = 16 + if _, err := io.ReadFull(proxyConn, buffer[:16]); err != nil { + return nil, "", fmt.Errorf("failed to read bound IPv6 address: %w", err) + } + host = netip.AddrFrom16([16]byte(buffer[:16])).String() case addrTypeDomainName: // buffer[8]: length of the domain name _, err := io.ReadFull(proxyConn, buffer[:1]) if err != nil { - return nil, fmt.Errorf("failed to read address length in connect response: %w", err) + return nil, "", fmt.Errorf("failed to read address length in connect response: %w", err) } bndAddrLen = int(buffer[0]) + if _, err := io.ReadFull(proxyConn, buffer[:bndAddrLen]); err != nil { + return nil, "", fmt.Errorf("failed to read bound domain address: %w", err) + } + host = string(buffer[:bndAddrLen]) default: - return nil, fmt.Errorf("invalid address type %v", buffer[3]) - } - // 5. Reads the bound address and port, but we currently ignore them. - // TODO(fortuna): Should we expose the remote bound address as the net.Conn.LocalAddr()? - if _, err := io.ReadFull(proxyConn, buffer[:bndAddrLen]); err != nil { - return nil, fmt.Errorf("failed to read bound address: %w", err) + return nil, "", fmt.Errorf("invalid address type %v", buffer[3]) } - // We read but ignore the remote bound port number: BND.PORT + + // Read BND.PORT if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { - return nil, fmt.Errorf("failed to read bound port: %w", err) + return nil, "", fmt.Errorf("failed to read bound port: %w", err) } + port := binary.BigEndian.Uint16(buffer[:2]) + bindAddr := net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)) + dialSuccess = true + return proxyConn, bindAddr, nil +} + +// DialStream implements [transport.StreamDialer].DialStream using SOCKS5. +// It will send the auth method, auth credentials (if auth is chosen), and +// the connect requests in one packet, to avoid an additional roundtrip. +// The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which +// you can check against the error constants in this package using [errors.Is]. +func (d *Dialer) DialStream(ctx context.Context, dstAddr string) (transport.StreamConn, error) { + proxyConn, _, err := d.request(ctx, CmdConnect, dstAddr) + if err != nil { + return nil, err + } return proxyConn, nil } diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 6352bd3b..78c0ccb4 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -32,13 +32,13 @@ import ( ) func TestSOCKS5Dialer_NewStreamDialerNil(t *testing.T) { - dialer, err := NewStreamDialer(nil) + dialer, err := NewDialer(nil) require.Nil(t, dialer) require.Error(t, err) } func TestSOCKS5Dialer_BadConnection(t *testing.T) { - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"}) + dialer, err := NewDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"}) require.NotNil(t, dialer) require.NoError(t, err) _, err = dialer.DialStream(context.Background(), "example.com:443") @@ -50,7 +50,7 @@ func TestSOCKS5Dialer_BadAddress(t *testing.T) { require.NoError(t, err, "Failed to create TCP listener: %v", err) defer listener.Close() - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) + dialer, err := NewDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) require.NotNil(t, dialer) require.NoError(t, err) @@ -97,7 +97,7 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req // Client go func() { defer running.Done() - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) + dialer, err := NewDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) require.NoError(tb, err) serverConn, err := dialer.DialStream(context.Background(), destAddr) if replyCode != 0 { @@ -188,7 +188,7 @@ func TestConnectWithoutAuth(t *testing.T) { address := listener.Addr().String() // Create a SOCKS5 client - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}) + dialer, err := NewDialer(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) @@ -221,7 +221,7 @@ func TestConnectWithAuth(t *testing.T) { // wait for server to start time.Sleep(10 * time.Millisecond) - dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: address}) + dialer, err := NewDialer(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) err = dialer.SetCredentials([]byte("testusername"), []byte("testpassword")) diff --git a/x/config/socks5.go b/x/config/socks5.go index 9820a148..7ad5f354 100644 --- a/x/config/socks5.go +++ b/x/config/socks5.go @@ -27,7 +27,7 @@ func wrapStreamDialerWithSOCKS5(innerSD func() (transport.StreamDialer, error), return nil, err } endpoint := transport.StreamDialerEndpoint{Dialer: sd, Address: configURL.Host} - dialer, err := socks5.NewStreamDialer(&endpoint) + dialer, err := socks5.NewDialer(&endpoint) if err != nil { return nil, err } From 927a81dbfdc1c720fff14729713156b410632b8d Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 2 Jul 2024 14:38:03 -0700 Subject: [PATCH 03/54] fix: associate command typo --- transport/socks5/socks5.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 6b4f6404..c65921ed 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -41,7 +41,7 @@ const ( const ( CmdConnect = byte(1) CmdBind = byte(2) - CmdUDPAssociate = byte(1) + CmdUDPAssociate = byte(3) ) // SOCKS5 authentication methods, as specified in https://datatracker.ietf.org/doc/html/rfc1928#section-3 From 52bbcbe901911986099241d215974b6810db588f Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 2 Jul 2024 14:38:46 -0700 Subject: [PATCH 04/54] feat: impelment read/write encapsulation --- transport/socks5/packet_dialer.go | 103 ++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index a45604a7..08697445 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "net" + "strconv" "time" "github.com/Jigsaw-Code/outline-sdk/transport" @@ -55,12 +56,99 @@ func (c *packetConn) SetWriteDeadline(t time.Time) error { func (c *packetConn) Read(b []byte) (int, error) { // TODO: read header - return c.pc.Read(b) + buffer := make([]byte, 65536) // Maximum size for UDP packet + n, err := c.pc.Read(buffer) + if err != nil { + return 0, err + } + fmt.Printf("Read buffer is %#X \n", buffer) + fmt.Printf("Read buffer length is %d \n", n) + + // Minimum size of header is 10 bytes + if n < 10 { + return 0, fmt.Errorf("invalid SOCKS5 UDP packet: too short") + } + + // Start parsing the header + rsv := buffer[:2] + if rsv[0] != 0x00 || rsv[1] != 0x00 { + return 0, fmt.Errorf("invalid reserved bytes: expected 0x0000, got %#x%#x", rsv[0], rsv[1]) + } + + frag := buffer[2] + if frag != 0 { + return 0, fmt.Errorf("fragmentation is not supported") + } + + atyp := buffer[3] + addrLen := 0 + switch atyp { + case addrTypeIPv4: + addrLen = net.IPv4len + case addrTypeIPv6: + addrLen = net.IPv6len + case addrTypeDomainName: + // Domain name's first byte is the length of the name + addrLen = int(buffer[4]) + 1 // +1 for the length byte itself + default: + return 0, fmt.Errorf("unknown address type %#x", atyp) + } + + // Calculate the start position of the actual data + headerLength := 4 + addrLen + 2 // RSV (2) + FRAG (1) + ATYP (1) + ADDR (variable) + PORT (2) + if n < headerLength { + return 0, fmt.Errorf("invalid SOCKS5 UDP packet: header too short") + } + + // Copy the payload into the provided buffer + payloadLength := n - headerLength + if payloadLength > len(b) { + // maybe raise an error to indicate that the provided buffer is too small? + payloadLength = len(b) + } + copy(b, buffer[headerLength:n]) + + return payloadLength, nil } func (c *packetConn) Write(b []byte) (int, error) { // TODO: write header - return c.pc.Write(b) + // Encapsulate the payload in a SOCKS5 UDP packet + header := []byte{ + 0x00, 0x00, // Reserved + 0x00, // Fragment number + // To be appended below: ATYP, IPv4, IPv6, Domain name + // To be appended below: IP and port (destination address) + } + destHost, destPortStr, _ := net.SplitHostPort(c.dstAddr.String()) + destPort, _ := strconv.Atoi(destPortStr) + // check if address is IPv4, IPv6 or domain name + if ipv4 := net.ParseIP(destHost).To4(); ipv4 != nil { + header = append(header, addrTypeIPv4) + header = append(header, ipv4...) + } else if ipv6 := net.ParseIP(destHost).To16(); ipv6 != nil { + header = append(header, addrTypeIPv6) + header = append(header, ipv6...) + } else { + // TODO: resolve domain name to IP? + _, err := net.LookupHost(destHost) + if err != nil { + return 0, fmt.Errorf("failed to resolve host: %w", err) + } + header = append(header, addrTypeDomainName) + header = append(header, []byte(destHost)...) + } + + header = append(header, byte(destPort>>8), byte(destPort)) + + fmt.Printf("Write header is %#X \n", header) + + // Combine the header and the payload + fullPacket := append(header, b...) + + fmt.Printf("fullPacket is %#X \n", fullPacket) + + return c.pc.Write(fullPacket) } func (c *packetConn) Close() error { @@ -73,13 +161,18 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro if err != nil { return nil, fmt.Errorf("failed to parse address: %w", err) } - - sc, bindAddr, err := d.request(ctx, CmdUDPAssociate, dstAddr) + // TODO: how to provide the bind address? + sc, bindAddr, err := d.request(ctx, CmdUDPAssociate, "[::]:12800") + fmt.Println("Bound address is:", bindAddr) if err != nil { return nil, err } - pc, err := d.pd.DialPacket(ctx, bindAddr) + host, port, err := net.SplitHostPort(bindAddr) + if err != nil { + return nil, fmt.Errorf("failed to parse bound address: %w", err) + } + pc, err := d.pd.DialPacket(ctx, net.JoinHostPort(host, port)) if err != nil { sc.Close() return nil, fmt.Errorf("failed to connect to packet endpoint: %w", err) From 2fe6b06425ad82cfc10baa54c8aecba61afcec6d Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 2 Jul 2024 14:39:22 -0700 Subject: [PATCH 05/54] some debugging lines added --- transport/socks5/stream_dialer.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 250963ba..783b7930 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -50,6 +50,7 @@ type Dialer struct { } var _ transport.StreamDialer = (*Dialer)(nil) +var _ transport.PacketDialer = (*Dialer)(nil) func (d *Dialer) SetCredentials(username, password []byte) error { if len(username) > 255 { @@ -213,6 +214,7 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo // 4. Read BND.ADDR. host := "" var bndAddrLen int + fmt.Printf("bind address type is %v \n", buffer[3]) switch buffer[3] { case addrTypeIPv4: if _, err := io.ReadFull(proxyConn, buffer[:4]); err != nil { @@ -225,7 +227,7 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo } host = netip.AddrFrom16([16]byte(buffer[:16])).String() case addrTypeDomainName: - // buffer[8]: length of the domain name + // read address length _, err := io.ReadFull(proxyConn, buffer[:1]) if err != nil { return nil, "", fmt.Errorf("failed to read address length in connect response: %w", err) @@ -244,7 +246,9 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo return nil, "", fmt.Errorf("failed to read bound port: %w", err) } port := binary.BigEndian.Uint16(buffer[:2]) - bindAddr := net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10)) + portStr := strconv.FormatUint(uint64(port), 10) + fmt.Printf("bind address is %v:%v \n", host, portStr) + bindAddr := net.JoinHostPort(host, portStr) dialSuccess = true return proxyConn, bindAddr, nil From 1ebea67884c8b2a759bba18a7b4d204a31d6b6f6 Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 2 Jul 2024 14:39:55 -0700 Subject: [PATCH 06/54] test: packet dialer tests --- transport/socks5/packet_dialer_test.go | 266 +++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 transport/socks5/packet_dialer_test.go diff --git a/transport/socks5/packet_dialer_test.go b/transport/socks5/packet_dialer_test.go new file mode 100644 index 00000000..7b3bba79 --- /dev/null +++ b/transport/socks5/packet_dialer_test.go @@ -0,0 +1,266 @@ +package socks5 + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "log" + "net" + "os" + "strconv" + "testing" + "time" + + "github.com/Jigsaw-Code/outline-sdk/transport" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/things-go/go-socks5" + "github.com/things-go/go-socks5/statute" +) + +func TestSOCKS5_Associate(t *testing.T) { + locIP := net.ParseIP("127.0.0.1") + + // Create a local listener + // This creates a UDP server that responded to "ping" + // message with "pong" response + serverAddr := &net.UDPAddr{IP: locIP, Port: 12399} + server, err := net.ListenUDP("udp", serverAddr) + require.NoError(t, err) + defer server.Close() + + go func() { + buf := make([]byte, 2048) + for { + n, remote, err := server.ReadFrom(buf) + if err != nil { + return + } + require.Equal(t, []byte("ping"), buf[:n]) + + server.WriteTo([]byte("pong"), remote) //nolint: errcheck + } + }() + + // Create a socks server to proxy "ping" message + cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{ + "testusername": "testpassword", + }} + proxySrv := socks5.NewServer( + socks5.WithAuthMethods([]socks5.Authenticator{cator}), + socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), + ) + // Start listening + proxyServerAddress := "127.0.0.1:12355" + go func() { + err := proxySrv.ListenAndServe("tcp", proxyServerAddress) + require.NoError(t, err) + }() + time.Sleep(10 * time.Millisecond) + + // // Get a local conn + // Connect, auth and connec to local + dialer, err := NewDialer(&transport.TCPEndpoint{Address: proxyServerAddress}) + require.NotNil(t, dialer) + require.NoError(t, err) + err = dialer.SetCredentials([]byte("testusername"), []byte("testpassword")) + require.NoError(t, err) + dialer.EnablePacket(transport.PacketListenerDialer{Listener: &transport.UDPListener{Address: ":12800"}}) + //dialer.EnablePacket(&transport.UDPDialer{}) + //net.ListenPacket() + //net.ListenUDP() + conn, err := dialer.DialPacket(context.Background(), serverAddr.String()) + require.NoError(t, err) + defer conn.Close() + fmt.Printf("local address is: %v\n", conn.LocalAddr()) + + // Send "ping" message + _, err = conn.Write([]byte("ping")) + require.NoError(t, err) + // wait time for response + conn.SetDeadline(time.Now().Add(time.Second)) + response := make([]byte, 1024) + _, err = conn.Read(response) + fmt.Printf("response: %s\n", response) + //conn.SetDeadline(time.Time{}) + require.NoError(t, err) + require.Equal(t, []byte("pong"), response) + time.Sleep(time.Second * 1) +} + +// Test UDP without Outline dialer +func TestSOCKS5_Associate_2(t *testing.T) { + locIP := net.ParseIP("127.0.0.1") + // Create a local listener + echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12400} + echoServer, err := net.ListenUDP("udp", echoServerAddr) + require.NoError(t, err) + defer echoServer.Close() + + go func() { + buf := make([]byte, 2048) + for { + n, remote, err := echoServer.ReadFrom(buf) + if err != nil { + return + } + require.Equal(t, []byte("ping"), buf[:n]) + + echoServer.WriteTo([]byte("pong"), remote) //nolint: errcheck + } + }() + + clientAddr := &net.UDPAddr{IP: locIP, Port: 12599} + client, err := net.ListenUDP("udp", clientAddr) + require.NoError(t, err) + defer client.Close() + + // Create a socks server + // Create a socks server to proxy "ping" message + cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{ + "foo": "bar", + }} + proxySrv := socks5.NewServer( + socks5.WithAuthMethods([]socks5.Authenticator{cator}), + socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), + ) + // Start listening + go func() { + err := proxySrv.ListenAndServe("tcp", "127.0.0.1:12350") + require.NoError(t, err) + }() + time.Sleep(10 * time.Millisecond) + + // Get a local conn + conn, err := net.Dial("tcp", "127.0.0.1:12350") + require.NoError(t, err) + + // Connect, auth and connec to local + req := bytes.NewBuffer( + []byte{ + statute.VersionSocks5, 2, statute.MethodNoAuth, statute.MethodUserPassAuth, + statute.UserPassAuthVersion, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r', + }) + reqHead := statute.Request{ + Version: statute.VersionSocks5, + Command: statute.CommandAssociate, + Reserved: 0, + DstAddr: statute.AddrSpec{ + FQDN: "", + IP: clientAddr.IP, + Port: clientAddr.Port, + AddrType: statute.ATYPIPv4, + }, + } + req.Write(reqHead.Bytes()) + // Send all the bytes + conn.Write(req.Bytes()) //nolint: errcheck + + // Verify response + expected := []byte{ + statute.VersionSocks5, statute.MethodUserPassAuth, // use user password auth + statute.UserPassAuthVersion, statute.AuthSuccess, // response auth success + } + + out := make([]byte, len(expected)) + conn.SetDeadline(time.Now().Add(time.Second)) //nolint: errcheck + _, err = io.ReadFull(conn, out) + conn.SetDeadline(time.Time{}) //nolint: errcheck + require.NoError(t, err) + require.Equal(t, expected, out) + + rspHead, err := statute.ParseReply(conn) + require.NoError(t, err) + require.Equal(t, statute.VersionSocks5, rspHead.Version) + require.Equal(t, statute.RepSuccess, rspHead.Response) + + ipByte := []byte(echoServerAddr.IP.To4()) + portByte := make([]byte, 2) + binary.BigEndian.PutUint16(portByte, uint16(echoServerAddr.Port)) + + msgBytes := []byte{0, 0, 0, statute.ATYPIPv4} + msgBytes = append(msgBytes, ipByte...) + msgBytes = append(msgBytes, portByte...) + msgBytes = append(msgBytes, []byte("ping")...) + client.WriteTo(msgBytes, &net.UDPAddr{IP: locIP, Port: rspHead.BndAddr.Port}) //nolint: errcheck + // t.Logf("proxy bind listen port: %d", rspHead.BndAddr.Port) + response := make([]byte, 1024) + n, _, err := client.ReadFrom(response) + require.NoError(t, err) + assert.Equal(t, []byte("pong"), response[n-4:n]) + time.Sleep(time.Second * 1) +} + +// test request +func TestSOCKS5_Associate_Request(t *testing.T) { + locIP := net.ParseIP("127.0.0.1") + // Create a local listener + echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12399} + echoServer, err := net.ListenUDP("udp", echoServerAddr) + require.NoError(t, err) + defer echoServer.Close() + + go func() { + buf := make([]byte, 2048) + for { + n, remote, err := echoServer.ReadFrom(buf) + if err != nil { + return + } + require.Equal(t, []byte("ping"), buf[:n]) + + echoServer.WriteTo([]byte("pong"), remote) //nolint: errcheck + } + }() + + clientAddr := &net.UDPAddr{IP: locIP, Port: 12499} + client, err := net.ListenUDP("udp", clientAddr) + require.NoError(t, err) + defer client.Close() + + // Create a socks server + // Create a socks server to proxy "ping" message + cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{ + "foo": "bar", + }} + proxySrv := socks5.NewServer( + socks5.WithAuthMethods([]socks5.Authenticator{cator}), + socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), + ) + // Start listening + go func() { + err := proxySrv.ListenAndServe("tcp", "127.0.0.1:12355") + require.NoError(t, err) + }() + time.Sleep(10 * time.Millisecond) + + // Connect, auth and connec to local + dialer, err := NewDialer(&transport.TCPEndpoint{Address: "127.0.0.1:12355"}) + require.NotNil(t, dialer) + require.NoError(t, err) + err = dialer.SetCredentials([]byte("foo"), []byte("bar")) + require.NoError(t, err) + sc, bindAddr, err := dialer.request(context.Background(), CmdUDPAssociate, clientAddr.String()) + require.NoError(t, err) + defer sc.Close() + + ipByte := []byte(echoServerAddr.IP.To4()) + portByte := make([]byte, 2) + binary.BigEndian.PutUint16(portByte, uint16(echoServerAddr.Port)) + + msgBytes := []byte{0, 0, 0, statute.ATYPIPv4} + msgBytes = append(msgBytes, ipByte...) + msgBytes = append(msgBytes, portByte...) + msgBytes = append(msgBytes, []byte("ping")...) + _, port, _ := net.SplitHostPort(bindAddr) + portInt, _ := strconv.Atoi(port) + client.WriteTo(msgBytes, &net.UDPAddr{IP: locIP, Port: portInt}) //nolint: errcheck + // t.Logf("proxy bind listen port: %d", rspHead.BndAddr.Port) + response := make([]byte, 1024) + n, _, err := client.ReadFrom(response) + require.NoError(t, err) + assert.Equal(t, []byte("pong"), response[n-4:n]) + time.Sleep(time.Second * 1) +} From 5bb95cc5bf9d48319987384912c1b3c102c643e7 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 7 Jul 2024 22:23:18 -0700 Subject: [PATCH 07/54] tests: with local udp echoserver and socks5 proxy --- transport/socks5/packet_dialer_test.go | 227 ++++--------------------- 1 file changed, 34 insertions(+), 193 deletions(-) diff --git a/transport/socks5/packet_dialer_test.go b/transport/socks5/packet_dialer_test.go index 7b3bba79..30ab17d8 100644 --- a/transport/socks5/packet_dialer_test.go +++ b/transport/socks5/packet_dialer_test.go @@ -3,13 +3,7 @@ package socks5 import ( "bytes" "context" - "encoding/binary" - "fmt" - "io" - "log" "net" - "os" - "strconv" "testing" "time" @@ -17,32 +11,18 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/things-go/go-socks5" - "github.com/things-go/go-socks5/statute" ) -func TestSOCKS5_Associate(t *testing.T) { - locIP := net.ParseIP("127.0.0.1") +func TestSOCKS5Associate(t *testing.T) { // Create a local listener // This creates a UDP server that responded to "ping" // message with "pong" response - serverAddr := &net.UDPAddr{IP: locIP, Port: 12399} - server, err := net.ListenUDP("udp", serverAddr) - require.NoError(t, err) - defer server.Close() - - go func() { - buf := make([]byte, 2048) - for { - n, remote, err := server.ReadFrom(buf) - if err != nil { - return - } - require.Equal(t, []byte("ping"), buf[:n]) - - server.WriteTo([]byte("pong"), remote) //nolint: errcheck - } - }() + locIP := net.ParseIP("127.0.0.1") + // Create a local listener + echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12199} + echoServer := setupUDPEchoServer(t, echoServerAddr) + defer echoServer.Close() // Create a socks server to proxy "ping" message cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{ @@ -50,7 +30,7 @@ func TestSOCKS5_Associate(t *testing.T) { }} proxySrv := socks5.NewServer( socks5.WithAuthMethods([]socks5.Authenticator{cator}), - socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), + //socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), ) // Start listening proxyServerAddress := "127.0.0.1:12355" @@ -60,207 +40,68 @@ func TestSOCKS5_Associate(t *testing.T) { }() time.Sleep(10 * time.Millisecond) - // // Get a local conn - // Connect, auth and connec to local + // Connect, auth and connec to local server dialer, err := NewDialer(&transport.TCPEndpoint{Address: proxyServerAddress}) require.NotNil(t, dialer) require.NoError(t, err) err = dialer.SetCredentials([]byte("testusername"), []byte("testpassword")) require.NoError(t, err) - dialer.EnablePacket(transport.PacketListenerDialer{Listener: &transport.UDPListener{Address: ":12800"}}) - //dialer.EnablePacket(&transport.UDPDialer{}) - //net.ListenPacket() - //net.ListenUDP() - conn, err := dialer.DialPacket(context.Background(), serverAddr.String()) + dialer.EnablePacket(&transport.UDPDialer{}) + conn, err := dialer.DialPacket(context.Background(), echoServerAddr.String()) require.NoError(t, err) defer conn.Close() - fmt.Printf("local address is: %v\n", conn.LocalAddr()) + //fmt.Printf("local address is: %v\n", conn.LocalAddr()) // Send "ping" message _, err = conn.Write([]byte("ping")) require.NoError(t, err) - // wait time for response + // max wait time for response conn.SetDeadline(time.Now().Add(time.Second)) response := make([]byte, 1024) - _, err = conn.Read(response) - fmt.Printf("response: %s\n", response) + n, err := conn.Read(response) //conn.SetDeadline(time.Time{}) require.NoError(t, err) - require.Equal(t, []byte("pong"), response) - time.Sleep(time.Second * 1) + require.Equal(t, []byte("pong"), response[:n]) } -// Test UDP without Outline dialer -func TestSOCKS5_Associate_2(t *testing.T) { +func TestUDPLoopBack(t *testing.T) { + // Create a local listener locIP := net.ParseIP("127.0.0.1") // Create a local listener - echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12400} - echoServer, err := net.ListenUDP("udp", echoServerAddr) - require.NoError(t, err) + echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12199} + echoServer := setupUDPEchoServer(t, echoServerAddr) defer echoServer.Close() - go func() { - buf := make([]byte, 2048) - for { - n, remote, err := echoServer.ReadFrom(buf) - if err != nil { - return - } - require.Equal(t, []byte("ping"), buf[:n]) - - echoServer.WriteTo([]byte("pong"), remote) //nolint: errcheck - } - }() - - clientAddr := &net.UDPAddr{IP: locIP, Port: 12599} - client, err := net.ListenUDP("udp", clientAddr) - require.NoError(t, err) - defer client.Close() - - // Create a socks server - // Create a socks server to proxy "ping" message - cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{ - "foo": "bar", - }} - proxySrv := socks5.NewServer( - socks5.WithAuthMethods([]socks5.Authenticator{cator}), - socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), - ) - // Start listening - go func() { - err := proxySrv.ListenAndServe("tcp", "127.0.0.1:12350") - require.NoError(t, err) - }() - time.Sleep(10 * time.Millisecond) - - // Get a local conn - conn, err := net.Dial("tcp", "127.0.0.1:12350") - require.NoError(t, err) - - // Connect, auth and connec to local - req := bytes.NewBuffer( - []byte{ - statute.VersionSocks5, 2, statute.MethodNoAuth, statute.MethodUserPassAuth, - statute.UserPassAuthVersion, 3, 'f', 'o', 'o', 3, 'b', 'a', 'r', - }) - reqHead := statute.Request{ - Version: statute.VersionSocks5, - Command: statute.CommandAssociate, - Reserved: 0, - DstAddr: statute.AddrSpec{ - FQDN: "", - IP: clientAddr.IP, - Port: clientAddr.Port, - AddrType: statute.ATYPIPv4, - }, - } - req.Write(reqHead.Bytes()) - // Send all the bytes - conn.Write(req.Bytes()) //nolint: errcheck - - // Verify response - expected := []byte{ - statute.VersionSocks5, statute.MethodUserPassAuth, // use user password auth - statute.UserPassAuthVersion, statute.AuthSuccess, // response auth success - } - - out := make([]byte, len(expected)) - conn.SetDeadline(time.Now().Add(time.Second)) //nolint: errcheck - _, err = io.ReadFull(conn, out) - conn.SetDeadline(time.Time{}) //nolint: errcheck + packDialer := transport.UDPDialer{} + conn, err := packDialer.DialPacket(context.Background(), echoServerAddr.String()) require.NoError(t, err) - require.Equal(t, expected, out) - - rspHead, err := statute.ParseReply(conn) - require.NoError(t, err) - require.Equal(t, statute.VersionSocks5, rspHead.Version) - require.Equal(t, statute.RepSuccess, rspHead.Response) - - ipByte := []byte(echoServerAddr.IP.To4()) - portByte := make([]byte, 2) - binary.BigEndian.PutUint16(portByte, uint16(echoServerAddr.Port)) - - msgBytes := []byte{0, 0, 0, statute.ATYPIPv4} - msgBytes = append(msgBytes, ipByte...) - msgBytes = append(msgBytes, portByte...) - msgBytes = append(msgBytes, []byte("ping")...) - client.WriteTo(msgBytes, &net.UDPAddr{IP: locIP, Port: rspHead.BndAddr.Port}) //nolint: errcheck - // t.Logf("proxy bind listen port: %d", rspHead.BndAddr.Port) + conn.Write([]byte("ping")) response := make([]byte, 1024) - n, _, err := client.ReadFrom(response) + n, err := conn.Read(response) require.NoError(t, err) - assert.Equal(t, []byte("pong"), response[n-4:n]) - time.Sleep(time.Second * 1) + assert.Equal(t, []byte("pong"), response[:n]) } -// test request -func TestSOCKS5_Associate_Request(t *testing.T) { - locIP := net.ParseIP("127.0.0.1") - // Create a local listener - echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12399} - echoServer, err := net.ListenUDP("udp", echoServerAddr) +func setupUDPEchoServer(t *testing.T, serverAddr *net.UDPAddr) *net.UDPConn { + server, err := net.ListenUDP("udp", serverAddr) require.NoError(t, err) - defer echoServer.Close() - go func() { buf := make([]byte, 2048) for { - n, remote, err := echoServer.ReadFrom(buf) + n, remote, err := server.ReadFrom(buf) if err != nil { + //log.Printf("Error reading: %v", err) return } - require.Equal(t, []byte("ping"), buf[:n]) - - echoServer.WriteTo([]byte("pong"), remote) //nolint: errcheck + if bytes.Equal(buf[:n], []byte("ping")) { + server.WriteTo([]byte("pong"), remote) + } } }() - clientAddr := &net.UDPAddr{IP: locIP, Port: 12499} - client, err := net.ListenUDP("udp", clientAddr) - require.NoError(t, err) - defer client.Close() - - // Create a socks server - // Create a socks server to proxy "ping" message - cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{ - "foo": "bar", - }} - proxySrv := socks5.NewServer( - socks5.WithAuthMethods([]socks5.Authenticator{cator}), - socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), - ) - // Start listening - go func() { - err := proxySrv.ListenAndServe("tcp", "127.0.0.1:12355") - require.NoError(t, err) - }() - time.Sleep(10 * time.Millisecond) - - // Connect, auth and connec to local - dialer, err := NewDialer(&transport.TCPEndpoint{Address: "127.0.0.1:12355"}) - require.NotNil(t, dialer) - require.NoError(t, err) - err = dialer.SetCredentials([]byte("foo"), []byte("bar")) - require.NoError(t, err) - sc, bindAddr, err := dialer.request(context.Background(), CmdUDPAssociate, clientAddr.String()) - require.NoError(t, err) - defer sc.Close() - - ipByte := []byte(echoServerAddr.IP.To4()) - portByte := make([]byte, 2) - binary.BigEndian.PutUint16(portByte, uint16(echoServerAddr.Port)) + t.Cleanup(func() { + server.Close() + }) - msgBytes := []byte{0, 0, 0, statute.ATYPIPv4} - msgBytes = append(msgBytes, ipByte...) - msgBytes = append(msgBytes, portByte...) - msgBytes = append(msgBytes, []byte("ping")...) - _, port, _ := net.SplitHostPort(bindAddr) - portInt, _ := strconv.Atoi(port) - client.WriteTo(msgBytes, &net.UDPAddr{IP: locIP, Port: portInt}) //nolint: errcheck - // t.Logf("proxy bind listen port: %d", rspHead.BndAddr.Port) - response := make([]byte, 1024) - n, _, err := client.ReadFrom(response) - require.NoError(t, err) - assert.Equal(t, []byte("pong"), response[n-4:n]) - time.Sleep(time.Second * 1) + return server } From 95096b5a7ebb38cc61657b6459d508a6ac00b2cc Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 7 Jul 2024 22:23:54 -0700 Subject: [PATCH 08/54] refactor: clean up --- transport/socks5/packet_dialer.go | 48 +++++++++++-------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index 08697445..994f86c3 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -19,7 +19,6 @@ import ( "errors" "fmt" "net" - "strconv" "time" "github.com/Jigsaw-Code/outline-sdk/transport" @@ -61,9 +60,6 @@ func (c *packetConn) Read(b []byte) (int, error) { if err != nil { return 0, err } - fmt.Printf("Read buffer is %#X \n", buffer) - fmt.Printf("Read buffer length is %d \n", n) - // Minimum size of header is 10 bytes if n < 10 { return 0, fmt.Errorf("invalid SOCKS5 UDP packet: too short") @@ -112,7 +108,6 @@ func (c *packetConn) Read(b []byte) (int, error) { } func (c *packetConn) Write(b []byte) (int, error) { - // TODO: write header // Encapsulate the payload in a SOCKS5 UDP packet header := []byte{ 0x00, 0x00, // Reserved @@ -120,34 +115,12 @@ func (c *packetConn) Write(b []byte) (int, error) { // To be appended below: ATYP, IPv4, IPv6, Domain name // To be appended below: IP and port (destination address) } - destHost, destPortStr, _ := net.SplitHostPort(c.dstAddr.String()) - destPort, _ := strconv.Atoi(destPortStr) - // check if address is IPv4, IPv6 or domain name - if ipv4 := net.ParseIP(destHost).To4(); ipv4 != nil { - header = append(header, addrTypeIPv4) - header = append(header, ipv4...) - } else if ipv6 := net.ParseIP(destHost).To16(); ipv6 != nil { - header = append(header, addrTypeIPv6) - header = append(header, ipv6...) - } else { - // TODO: resolve domain name to IP? - _, err := net.LookupHost(destHost) - if err != nil { - return 0, fmt.Errorf("failed to resolve host: %w", err) - } - header = append(header, addrTypeDomainName) - header = append(header, []byte(destHost)...) + header, err := appendSOCKS5Address(header, c.dstAddr.String()) + if err != nil { + return 0, fmt.Errorf("failed to append SOCKS5 address: %w", err) } - - header = append(header, byte(destPort>>8), byte(destPort)) - - fmt.Printf("Write header is %#X \n", header) - // Combine the header and the payload fullPacket := append(header, b...) - - fmt.Printf("fullPacket is %#X \n", fullPacket) - return c.pc.Write(fullPacket) } @@ -162,8 +135,8 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro return nil, fmt.Errorf("failed to parse address: %w", err) } // TODO: how to provide the bind address? - sc, bindAddr, err := d.request(ctx, CmdUDPAssociate, "[::]:12800") - fmt.Println("Bound address is:", bindAddr) + sc, bindAddr, err := d.request(ctx, CmdUDPAssociate, "0.0.0.0:0") + //fmt.Println("Bound address is:", bindAddr) if err != nil { return nil, err } @@ -172,6 +145,17 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro if err != nil { return nil, fmt.Errorf("failed to parse bound address: %w", err) } + fmt.Printf("bound host is %v, bound port is %v \n", host, port) + + if host == "::" { + schost, scPort, err := net.SplitHostPort(sc.RemoteAddr().String()) + if err != nil { + return nil, fmt.Errorf("failed to parse tcp address: %w", err) + } + fmt.Printf("tcp host is %v, tcp port is %v \n", schost, scPort) + host = schost + } + pc, err := d.pd.DialPacket(ctx, net.JoinHostPort(host, port)) if err != nil { sc.Close() From 28396077f2dde31b4cb82d08b54480017a6a6221 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 7 Jul 2024 22:25:04 -0700 Subject: [PATCH 09/54] ading wrapPacketDialerWithSOCKS5 --- x/config/socks5.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/x/config/socks5.go b/x/config/socks5.go index 7ad5f354..55ab4be7 100644 --- a/x/config/socks5.go +++ b/x/config/socks5.go @@ -42,3 +42,31 @@ func wrapStreamDialerWithSOCKS5(innerSD func() (transport.StreamDialer, error), } return dialer, nil } + +func wrapPacketDialerWithSOCKS5(associateSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.PacketDialer, error) { + sd, err := associateSD() + if err != nil { + return nil, err + } + endpoint := transport.StreamDialerEndpoint{Dialer: sd, Address: configURL.Host} + dialer, err := socks5.NewDialer(&endpoint) + if err != nil { + return nil, err + } + userInfo := configURL.User + if userInfo != nil { + username := userInfo.Username() + password, _ := userInfo.Password() + err := dialer.SetCredentials([]byte(username), []byte(password)) + if err != nil { + return nil, err + } + } + + pd, err := innerPD() + if err != nil { + return nil, err + } + dialer.EnablePacket(pd) + return dialer, nil +} From 4f51b366fe756b03014ee859112aa0599c1da506 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 7 Jul 2024 22:25:45 -0700 Subject: [PATCH 10/54] registering socks5 packet dialer type --- x/config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/x/config/config.go b/x/config/config.go index c5d9d8ec..708ce814 100644 --- a/x/config/config.go +++ b/x/config/config.go @@ -57,6 +57,7 @@ func NewDefaultConfigToDialer() *ConfigToDialer { p.RegisterPacketDialerType("override", wrapPacketDialerWithOverride) p.RegisterStreamDialerType("socks5", wrapStreamDialerWithSOCKS5) + p.RegisterPacketDialerType("socks5", wrapPacketDialerWithSOCKS5) p.RegisterStreamDialerType("split", wrapStreamDialerWithSplit) From f75974ee5acdc7e768abe693139a0e301b1401ec Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 8 Jul 2024 09:51:17 -0700 Subject: [PATCH 11/54] update go mod/sum --- x/go.mod | 2 +- x/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x/go.mod b/x/go.mod index 394c95e4..e5a91d9a 100644 --- a/x/go.mod +++ b/x/go.mod @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x go 1.20 require ( - github.com/Jigsaw-Code/outline-sdk v0.0.15 + github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b github.com/stretchr/testify v1.8.4 github.com/vishvananda/netlink v1.1.0 diff --git a/x/go.sum b/x/go.sum index ef550a64..206e33b8 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,5 +1,7 @@ github.com/Jigsaw-Code/outline-sdk v0.0.15 h1:2OfYum4vllfIgoDa/X9drA2I57knXFPREv4kMZkjTuI= github.com/Jigsaw-Code/outline-sdk v0.0.15/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 h1:1t6cnyP4liW9hjmnVrDn6Bc5m2qlAcjPurf9UEye4Hg= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8= From 6026ef4a9304f6413b916e64267ca52060796e7e Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 8 Jul 2024 14:19:30 -0700 Subject: [PATCH 12/54] removed logging and some improvements --- transport/socks5/packet_dialer.go | 10 +++++----- transport/socks5/packet_dialer_test.go | 1 - transport/socks5/stream_dialer.go | 2 -- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index 994f86c3..f4befb45 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -134,25 +134,25 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro if err != nil { return nil, fmt.Errorf("failed to parse address: %w", err) } - // TODO: how to provide the bind address? sc, bindAddr, err := d.request(ctx, CmdUDPAssociate, "0.0.0.0:0") //fmt.Println("Bound address is:", bindAddr) if err != nil { return nil, err } + // Wait for the bind to be ready + //time.Sleep(1 * time.Millisecond) + host, port, err := net.SplitHostPort(bindAddr) if err != nil { return nil, fmt.Errorf("failed to parse bound address: %w", err) } - fmt.Printf("bound host is %v, bound port is %v \n", host, port) - if host == "::" { - schost, scPort, err := net.SplitHostPort(sc.RemoteAddr().String()) + if ipAddr := net.ParseIP(host); ipAddr != nil && ipAddr.IsUnspecified() { + schost, _, err := net.SplitHostPort(sc.RemoteAddr().String()) if err != nil { return nil, fmt.Errorf("failed to parse tcp address: %w", err) } - fmt.Printf("tcp host is %v, tcp port is %v \n", schost, scPort) host = schost } diff --git a/transport/socks5/packet_dialer_test.go b/transport/socks5/packet_dialer_test.go index 30ab17d8..c219fb59 100644 --- a/transport/socks5/packet_dialer_test.go +++ b/transport/socks5/packet_dialer_test.go @@ -50,7 +50,6 @@ func TestSOCKS5Associate(t *testing.T) { conn, err := dialer.DialPacket(context.Background(), echoServerAddr.String()) require.NoError(t, err) defer conn.Close() - //fmt.Printf("local address is: %v\n", conn.LocalAddr()) // Send "ping" message _, err = conn.Write([]byte("ping")) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 783b7930..d0adaaa8 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -214,7 +214,6 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo // 4. Read BND.ADDR. host := "" var bndAddrLen int - fmt.Printf("bind address type is %v \n", buffer[3]) switch buffer[3] { case addrTypeIPv4: if _, err := io.ReadFull(proxyConn, buffer[:4]); err != nil { @@ -247,7 +246,6 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo } port := binary.BigEndian.Uint16(buffer[:2]) portStr := strconv.FormatUint(uint64(port), 10) - fmt.Printf("bind address is %v:%v \n", host, portStr) bindAddr := net.JoinHostPort(host, portStr) dialSuccess = true From e3f0a561fb9a03d1d3b6904bae830cb67e245cc5 Mon Sep 17 00:00:00 2001 From: amir gh Date: Wed, 10 Jul 2024 15:40:17 -0700 Subject: [PATCH 13/54] code review improvements --- transport/socks5/packet_dialer.go | 80 +++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index f4befb45..d9829eb7 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -16,8 +16,10 @@ package socks5 import ( "context" + "encoding/binary" "errors" "fmt" + "io" "net" "time" @@ -33,7 +35,6 @@ type packetConn struct { var _ net.Conn = (*packetConn)(nil) func (c *packetConn) LocalAddr() net.Addr { - // TODO: Is this right? return c.pc.LocalAddr() } @@ -54,42 +55,51 @@ func (c *packetConn) SetWriteDeadline(t time.Time) error { } func (c *packetConn) Read(b []byte) (int, error) { - // TODO: read header buffer := make([]byte, 65536) // Maximum size for UDP packet n, err := c.pc.Read(buffer) if err != nil { return 0, err } // Minimum size of header is 10 bytes + // 2 bytes for reserved, 1 byte for fragment, 1 byte for address type, 4 byte for ipv4, 2 bytes for port if n < 10 { return 0, fmt.Errorf("invalid SOCKS5 UDP packet: too short") } + pkt := buffer[:n] + // Start parsing the header - rsv := buffer[:2] + rsv := pkt[:2] if rsv[0] != 0x00 || rsv[1] != 0x00 { return 0, fmt.Errorf("invalid reserved bytes: expected 0x0000, got %#x%#x", rsv[0], rsv[1]) } - frag := buffer[2] + frag := pkt[2] if frag != 0 { - return 0, fmt.Errorf("fragmentation is not supported") + return 0, errors.New("fragmentation is not supported") } - atyp := buffer[3] + atyp := pkt[3] addrLen := 0 switch atyp { case addrTypeIPv4: - addrLen = net.IPv4len + addrLen = 4 case addrTypeIPv6: - addrLen = net.IPv6len + addrLen = 16 case addrTypeDomainName: // Domain name's first byte is the length of the name - addrLen = int(buffer[4]) + 1 // +1 for the length byte itself + addrLen = int(pkt[4]) + 1 // +1 for the length byte itself default: return 0, fmt.Errorf("unknown address type %#x", atyp) } + pkt = pkt[4:] // Skip the header + addr := pkt[:addrLen] + + pkt = pkt[addrLen:] // Skip the address + port := binary.BigEndian.Uint16(pkt[:2]) + fmt.Printf("Received packet from %d:%d\n", addr, port) + // Calculate the start position of the actual data headerLength := 4 + addrLen + 2 // RSV (2) + FRAG (1) + ATYP (1) + ADDR (variable) + PORT (2) if n < headerLength { @@ -99,8 +109,7 @@ func (c *packetConn) Read(b []byte) (int, error) { // Copy the payload into the provided buffer payloadLength := n - headerLength if payloadLength > len(b) { - // maybe raise an error to indicate that the provided buffer is too small? - payloadLength = len(b) + return 0, io.ErrShortBuffer } copy(b, buffer[headerLength:n]) @@ -109,12 +118,14 @@ func (c *packetConn) Read(b []byte) (int, error) { func (c *packetConn) Write(b []byte) (int, error) { // Encapsulate the payload in a SOCKS5 UDP packet - header := []byte{ + // this is the minimum preallocated header size (10 bytes) + header := make([]byte, 10) + header = append(header[:0], 0x00, 0x00, // Reserved 0x00, // Fragment number - // To be appended below: ATYP, IPv4, IPv6, Domain name - // To be appended below: IP and port (destination address) - } + // To be appended below: + // ATYP, IPv4, IPv6, Domain Name, Port + ) header, err := appendSOCKS5Address(header, c.dstAddr.String()) if err != nil { return 0, fmt.Errorf("failed to append SOCKS5 address: %w", err) @@ -164,3 +175,42 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro return &packetConn{netDstAddr, pc, sc}, nil } + +// func readAddress(conn *net.Conn) (string, error) { +// // Read the address type +// fmt.Println("Reading address type") +// addrType := make([]byte, 1) +// addrLen := 0 +// n, err := io.ReadFull(*conn, addrType) +// //n, err := *conn.Read(addrType) +// if err != nil { +// return "", fmt.Errorf("failed to read address type: %w", err) +// } +// fmt.Printf("Read address type %d bytes\n", n) +// // Read the address type +// switch addrType[0] { +// case addrTypeIPv4: +// addrLen = 4 +// case addrTypeIPv6: +// addrLen = 16 +// case addrTypeDomainName: +// // Domain name's first byte is the length of the name +// domainAddrLen := make([]byte, 1) +// _, err := io.ReadFull(*conn, domainAddrLen) +// //_, err := reader.Read(domainAddrLen) +// if err != nil { +// return "", fmt.Errorf("failed to read domain address length: %w", err) +// } +// addrLen = int(domainAddrLen[0]) +// default: +// return "", fmt.Errorf("unknown address type %#x", addrType[0]) +// } +// fmt.Printf("Address length is: %d\n", addrLen) +// addr := make([]byte, addrLen) +// _, err = io.ReadFull(*conn, addr) +// //_, err = reader.Read(addr) +// if err != nil { +// return "", fmt.Errorf("failed to read address: %w", err) +// } +// return string(addr), nil +// } From 3ffc6639f2f98c02ad63cf6cfe589eb7c6d88e48 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 15 Jul 2024 08:46:00 -0700 Subject: [PATCH 14/54] add spec reference to udp dialer --- transport/socks5/packet_dialer.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index d9829eb7..3c50afd0 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -117,8 +117,9 @@ func (c *packetConn) Read(b []byte) (int, error) { } func (c *packetConn) Write(b []byte) (int, error) { - // Encapsulate the payload in a SOCKS5 UDP packet - // this is the minimum preallocated header size (10 bytes) + // Encapsulate the payload in a SOCKS5 UDP packet as specified in + // https://datatracker.ietf.org/doc/html/rfc1928#section-7 + // The minimum preallocated header size (10 bytes) header := make([]byte, 10) header = append(header[:0], 0x00, 0x00, // Reserved From 587bb40ecf8ccd5bd916ceaeea841d10fd8f6f53 Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 16 Jul 2024 11:15:21 -0600 Subject: [PATCH 15/54] feat: use slicepool + readAdress refactor --- transport/socks5/packet_dialer.go | 116 +++++++++++++++--------------- 1 file changed, 57 insertions(+), 59 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index 3c50afd0..d1bcb114 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -15,17 +15,26 @@ package socks5 import ( + "bytes" "context" "encoding/binary" "errors" "fmt" "io" "net" + "strconv" "time" + "github.com/Jigsaw-Code/outline-sdk/internal/slicepool" "github.com/Jigsaw-Code/outline-sdk/transport" ) +// clientUDPBufferSize is the maximum supported UDP packet size in bytes. +const clientUDPBufferSize = 16 * 1024 + +// udpPool stores the byte slices used for storing packets. +var udpPool = slicepool.MakePool(clientUDPBufferSize) + type packetConn struct { dstAddr net.Addr pc net.Conn @@ -55,7 +64,9 @@ func (c *packetConn) SetWriteDeadline(t time.Time) error { } func (c *packetConn) Read(b []byte) (int, error) { - buffer := make([]byte, 65536) // Maximum size for UDP packet + lazySlice := udpPool.LazySlice() + buffer := lazySlice.Acquire() + defer lazySlice.Release() n, err := c.pc.Read(buffer) if err != nil { return 0, err @@ -79,27 +90,12 @@ func (c *packetConn) Read(b []byte) (int, error) { return 0, errors.New("fragmentation is not supported") } - atyp := pkt[3] - addrLen := 0 - switch atyp { - case addrTypeIPv4: - addrLen = 4 - case addrTypeIPv6: - addrLen = 16 - case addrTypeDomainName: - // Domain name's first byte is the length of the name - addrLen = int(pkt[4]) + 1 // +1 for the length byte itself - default: - return 0, fmt.Errorf("unknown address type %#x", atyp) + // Do something with address? + _, addrLen, err := readAddress(bytes.NewReader(pkt[3:])) + if err != nil { + return 0, fmt.Errorf("failed to read address: %w", err) } - pkt = pkt[4:] // Skip the header - addr := pkt[:addrLen] - - pkt = pkt[addrLen:] // Skip the address - port := binary.BigEndian.Uint16(pkt[:2]) - fmt.Printf("Received packet from %d:%d\n", addr, port) - // Calculate the start position of the actual data headerLength := 4 + addrLen + 2 // RSV (2) + FRAG (1) + ATYP (1) + ADDR (variable) + PORT (2) if n < headerLength { @@ -153,7 +149,7 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro } // Wait for the bind to be ready - //time.Sleep(1 * time.Millisecond) + // time.Sleep(10 * time.Millisecond) host, port, err := net.SplitHostPort(bindAddr) if err != nil { @@ -177,41 +173,43 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro return &packetConn{netDstAddr, pc, sc}, nil } -// func readAddress(conn *net.Conn) (string, error) { -// // Read the address type -// fmt.Println("Reading address type") -// addrType := make([]byte, 1) -// addrLen := 0 -// n, err := io.ReadFull(*conn, addrType) -// //n, err := *conn.Read(addrType) -// if err != nil { -// return "", fmt.Errorf("failed to read address type: %w", err) -// } -// fmt.Printf("Read address type %d bytes\n", n) -// // Read the address type -// switch addrType[0] { -// case addrTypeIPv4: -// addrLen = 4 -// case addrTypeIPv6: -// addrLen = 16 -// case addrTypeDomainName: -// // Domain name's first byte is the length of the name -// domainAddrLen := make([]byte, 1) -// _, err := io.ReadFull(*conn, domainAddrLen) -// //_, err := reader.Read(domainAddrLen) -// if err != nil { -// return "", fmt.Errorf("failed to read domain address length: %w", err) -// } -// addrLen = int(domainAddrLen[0]) -// default: -// return "", fmt.Errorf("unknown address type %#x", addrType[0]) -// } -// fmt.Printf("Address length is: %d\n", addrLen) -// addr := make([]byte, addrLen) -// _, err = io.ReadFull(*conn, addr) -// //_, err = reader.Read(addr) -// if err != nil { -// return "", fmt.Errorf("failed to read address: %w", err) -// } -// return string(addr), nil -// } +func readAddress(reader io.Reader) (string, int, error) { + // Read the address type + addrType := make([]byte, 1) + addrLen := 0 + _, err := io.ReadFull(reader, addrType) + if err != nil { + return "", 0, fmt.Errorf("failed to read address type: %w", err) + } + // Read the address type + switch addrType[0] { + case addrTypeIPv4: + addrLen = 4 + case addrTypeIPv6: + addrLen = 16 + case addrTypeDomainName: + // Domain name's first byte is the length of the name + domainAddrLen := make([]byte, 1) + _, err := io.ReadFull(reader, domainAddrLen) + if err != nil { + return "", 0, fmt.Errorf("failed to read domain address length: %w", err) + } + addrLen = int(domainAddrLen[0]) + default: + return "", 0, fmt.Errorf("unknown address type %#x", addrType[0]) + } + host := make([]byte, addrLen) + _, err = io.ReadFull(reader, host) + if err != nil { + return "", 0, fmt.Errorf("failed to read address: %w", err) + } + port := make([]byte, 2) + _, err = io.ReadFull(reader, port) + if err != nil { + return "", 0, fmt.Errorf("failed to read port: %w", err) + } + p := binary.BigEndian.Uint16(port) + portStr := strconv.FormatUint(uint64(p), 10) + addr := net.JoinHostPort(net.IP(host).String(), portStr) + return addr, addrLen, nil +} From 036091b6a05546ad96a1edcdee71f914a0918a4a Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 16 Jul 2024 12:39:08 -0600 Subject: [PATCH 16/54] refactor: use readAdress with proxyConn in stread dialer --- transport/socks5/stream_dialer.go | 43 +++---------------------------- 1 file changed, 4 insertions(+), 39 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index d0adaaa8..ac4f4331 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -16,13 +16,9 @@ package socks5 import ( "context" - "encoding/binary" "errors" "fmt" "io" - "net" - "net/netip" - "strconv" "github.com/Jigsaw-Code/outline-sdk/transport" ) @@ -198,7 +194,7 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo // buffer[1]: REP // buffer[2]: RSV // buffer[3]: ATYP - if _, err = io.ReadFull(proxyConn, buffer[:4]); err != nil { + if _, err = io.ReadFull(proxyConn, buffer[:3]); err != nil { return nil, "", fmt.Errorf("failed to read connect server response: %w", err) } @@ -212,41 +208,10 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo } // 4. Read BND.ADDR. - host := "" - var bndAddrLen int - switch buffer[3] { - case addrTypeIPv4: - if _, err := io.ReadFull(proxyConn, buffer[:4]); err != nil { - return nil, "", fmt.Errorf("failed to read bound IPv4 address: %w", err) - } - host = netip.AddrFrom4([4]byte(buffer[:4])).String() - case addrTypeIPv6: - if _, err := io.ReadFull(proxyConn, buffer[:16]); err != nil { - return nil, "", fmt.Errorf("failed to read bound IPv6 address: %w", err) - } - host = netip.AddrFrom16([16]byte(buffer[:16])).String() - case addrTypeDomainName: - // read address length - _, err := io.ReadFull(proxyConn, buffer[:1]) - if err != nil { - return nil, "", fmt.Errorf("failed to read address length in connect response: %w", err) - } - bndAddrLen = int(buffer[0]) - if _, err := io.ReadFull(proxyConn, buffer[:bndAddrLen]); err != nil { - return nil, "", fmt.Errorf("failed to read bound domain address: %w", err) - } - host = string(buffer[:bndAddrLen]) - default: - return nil, "", fmt.Errorf("invalid address type %v", buffer[3]) - } - - // Read BND.PORT - if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { - return nil, "", fmt.Errorf("failed to read bound port: %w", err) + bindAddr, _, err := readAddress(proxyConn) + if err != nil { + return nil, "", fmt.Errorf("failed to read bound address: %w", err) } - port := binary.BigEndian.Uint16(buffer[:2]) - portStr := strconv.FormatUint(uint64(port), 10) - bindAddr := net.JoinHostPort(host, portStr) dialSuccess = true return proxyConn, bindAddr, nil From 333ccf6f0bd6aa1c499ea24f269ca384a97b6f93 Mon Sep 17 00:00:00 2001 From: amir gh Date: Tue, 16 Jul 2024 15:16:45 -0600 Subject: [PATCH 17/54] preallocate buffer size in read address --- transport/socks5/packet_dialer.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_dialer.go index d1bcb114..e9ed16b7 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_dialer.go @@ -175,41 +175,44 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro func readAddress(reader io.Reader) (string, int, error) { // Read the address type - addrType := make([]byte, 1) + // The maximum buffer size is: + // 1 address type + 1 address length + 256 (max domain name length) + var buffer [1 + 1 + 256]byte addrLen := 0 - _, err := io.ReadFull(reader, addrType) + _, err := io.ReadFull(reader, buffer[:1]) if err != nil { return "", 0, fmt.Errorf("failed to read address type: %w", err) } // Read the address type - switch addrType[0] { + switch buffer[0] { case addrTypeIPv4: addrLen = 4 case addrTypeIPv6: addrLen = 16 case addrTypeDomainName: // Domain name's first byte is the length of the name - domainAddrLen := make([]byte, 1) - _, err := io.ReadFull(reader, domainAddrLen) + // Read domainAddrLen + _, err := io.ReadFull(reader, buffer[1:]) if err != nil { return "", 0, fmt.Errorf("failed to read domain address length: %w", err) } - addrLen = int(domainAddrLen[0]) + addrLen = int(buffer[1]) default: - return "", 0, fmt.Errorf("unknown address type %#x", addrType[0]) + return "", 0, fmt.Errorf("unknown address type %#x", buffer[0]) } - host := make([]byte, addrLen) - _, err = io.ReadFull(reader, host) + // Read host address + _, err = io.ReadFull(reader, buffer[:addrLen]) if err != nil { return "", 0, fmt.Errorf("failed to read address: %w", err) } - port := make([]byte, 2) - _, err = io.ReadFull(reader, port) + host := net.IP(buffer[:addrLen]).String() + // Read port number + _, err = io.ReadFull(reader, buffer[:2]) if err != nil { return "", 0, fmt.Errorf("failed to read port: %w", err) } - p := binary.BigEndian.Uint16(port) + p := binary.BigEndian.Uint16(buffer[:2]) portStr := strconv.FormatUint(uint64(p), 10) - addr := net.JoinHostPort(net.IP(host).String(), portStr) + addr := net.JoinHostPort(host, portStr) return addr, addrLen, nil } From 92a280d93a3390f1c14ed8aa9deb3862c49b63ce Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 18 Jul 2024 13:55:48 -0600 Subject: [PATCH 18/54] feat: use packet listenter interface --- .../{packet_dialer.go => packet_listener.go} | 128 +++++++----------- ...dialer_test.go => packet_listener_test.go} | 17 +-- 2 files changed, 55 insertions(+), 90 deletions(-) rename transport/socks5/{packet_dialer.go => packet_listener.go} (50%) rename transport/socks5/{packet_dialer_test.go => packet_listener_test.go} (85%) diff --git a/transport/socks5/packet_dialer.go b/transport/socks5/packet_listener.go similarity index 50% rename from transport/socks5/packet_dialer.go rename to transport/socks5/packet_listener.go index e9ed16b7..06870861 100644 --- a/transport/socks5/packet_dialer.go +++ b/transport/socks5/packet_listener.go @@ -17,12 +17,10 @@ package socks5 import ( "bytes" "context" - "encoding/binary" "errors" "fmt" "io" "net" - "strconv" "time" "github.com/Jigsaw-Code/outline-sdk/internal/slicepool" @@ -41,40 +39,40 @@ type packetConn struct { sc transport.StreamConn } -var _ net.Conn = (*packetConn)(nil) +var _ net.PacketConn = (*packetConn)(nil) -func (c *packetConn) LocalAddr() net.Addr { - return c.pc.LocalAddr() +func (p *packetConn) LocalAddr() net.Addr { + return p.pc.LocalAddr() } -func (c *packetConn) RemoteAddr() net.Addr { - return c.dstAddr +func (p *packetConn) RemoteAddr() net.Addr { + return p.dstAddr } -func (c *packetConn) SetDeadline(t time.Time) error { - return c.pc.SetDeadline(t) +func (p *packetConn) SetDeadline(t time.Time) error { + return p.pc.SetDeadline(t) } -func (c *packetConn) SetReadDeadline(t time.Time) error { - return c.pc.SetReadDeadline(t) +func (p *packetConn) SetReadDeadline(t time.Time) error { + return p.pc.SetReadDeadline(t) } func (c *packetConn) SetWriteDeadline(t time.Time) error { return c.pc.SetWriteDeadline(t) } -func (c *packetConn) Read(b []byte) (int, error) { +func (p *packetConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { lazySlice := udpPool.LazySlice() buffer := lazySlice.Acquire() defer lazySlice.Release() - n, err := c.pc.Read(buffer) + n, err = p.pc.Read(buffer) if err != nil { - return 0, err + return 0, nil, err } // Minimum size of header is 10 bytes // 2 bytes for reserved, 1 byte for fragment, 1 byte for address type, 4 byte for ipv4, 2 bytes for port if n < 10 { - return 0, fmt.Errorf("invalid SOCKS5 UDP packet: too short") + return 0, nil, fmt.Errorf("invalid SOCKS5 UDP packet: too short") } pkt := buffer[:n] @@ -82,37 +80,41 @@ func (c *packetConn) Read(b []byte) (int, error) { // Start parsing the header rsv := pkt[:2] if rsv[0] != 0x00 || rsv[1] != 0x00 { - return 0, fmt.Errorf("invalid reserved bytes: expected 0x0000, got %#x%#x", rsv[0], rsv[1]) + return 0, nil, fmt.Errorf("invalid reserved bytes: expected 0x0000, got %#x%#x", rsv[0], rsv[1]) } frag := pkt[2] if frag != 0 { - return 0, errors.New("fragmentation is not supported") + return 0, nil, errors.New("fragmentation is not supported") } // Do something with address? - _, addrLen, err := readAddress(bytes.NewReader(pkt[3:])) + address, addrLen, err := readAddress(bytes.NewReader(pkt[3:])) if err != nil { - return 0, fmt.Errorf("failed to read address: %w", err) + return 0, nil, fmt.Errorf("failed to read address: %w", err) } // Calculate the start position of the actual data headerLength := 4 + addrLen + 2 // RSV (2) + FRAG (1) + ATYP (1) + ADDR (variable) + PORT (2) if n < headerLength { - return 0, fmt.Errorf("invalid SOCKS5 UDP packet: header too short") + return 0, nil, fmt.Errorf("invalid SOCKS5 UDP packet: header too short") } // Copy the payload into the provided buffer payloadLength := n - headerLength if payloadLength > len(b) { - return 0, io.ErrShortBuffer + return 0, nil, io.ErrShortBuffer } copy(b, buffer[headerLength:n]) - return payloadLength, nil + addr, err = net.ResolveUDPAddr("udp", address) + if err != nil { + return 0, nil, fmt.Errorf("failed to resolve address: %w", err) + } + return payloadLength, addr, nil } -func (c *packetConn) Write(b []byte) (int, error) { +func (p *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { // Encapsulate the payload in a SOCKS5 UDP packet as specified in // https://datatracker.ietf.org/doc/html/rfc1928#section-7 // The minimum preallocated header size (10 bytes) @@ -123,34 +125,27 @@ func (c *packetConn) Write(b []byte) (int, error) { // To be appended below: // ATYP, IPv4, IPv6, Domain Name, Port ) - header, err := appendSOCKS5Address(header, c.dstAddr.String()) + header, err := appendSOCKS5Address(header, addr.String()) if err != nil { return 0, fmt.Errorf("failed to append SOCKS5 address: %w", err) } // Combine the header and the payload fullPacket := append(header, b...) - return c.pc.Write(fullPacket) + return p.pc.Write(fullPacket) } -func (c *packetConn) Close() error { - return errors.Join(c.sc.Close(), c.pc.Close()) +func (p *packetConn) Close() error { + return errors.Join(p.sc.Close(), p.pc.Close()) } // DialPacket creates a packet [net.Conn] via SOCKS5. -func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, error) { - netDstAddr, err := transport.MakeNetAddr("udp", dstAddr) - if err != nil { - return nil, fmt.Errorf("failed to parse address: %w", err) - } - sc, bindAddr, err := d.request(ctx, CmdUDPAssociate, "0.0.0.0:0") - //fmt.Println("Bound address is:", bindAddr) +func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) { + // Connect to the SOCKS5 server and perform UDP association + sc, bindAddr, err := c.request(ctx, CmdUDPAssociate, "0.0.0.0:0") if err != nil { return nil, err } - // Wait for the bind to be ready - // time.Sleep(10 * time.Millisecond) - host, port, err := net.SplitHostPort(bindAddr) if err != nil { return nil, fmt.Errorf("failed to parse bound address: %w", err) @@ -164,55 +159,24 @@ func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, erro host = schost } - pc, err := d.pd.DialPacket(ctx, net.JoinHostPort(host, port)) + packetEndpoint := &transport.PacketDialerEndpoint{Dialer: c.pd, Address: net.JoinHostPort(host, port)} + err = c.EnablePacketListener(packetEndpoint) if err != nil { - sc.Close() - return nil, fmt.Errorf("failed to connect to packet endpoint: %w", err) + return nil, fmt.Errorf("failed to enable packet listener: %w", err) } - return &packetConn{netDstAddr, pc, sc}, nil -} - -func readAddress(reader io.Reader) (string, int, error) { - // Read the address type - // The maximum buffer size is: - // 1 address type + 1 address length + 256 (max domain name length) - var buffer [1 + 1 + 256]byte - addrLen := 0 - _, err := io.ReadFull(reader, buffer[:1]) + proxyConn, err := c.pe.ConnectPacket(ctx) if err != nil { - return "", 0, fmt.Errorf("failed to read address type: %w", err) - } - // Read the address type - switch buffer[0] { - case addrTypeIPv4: - addrLen = 4 - case addrTypeIPv6: - addrLen = 16 - case addrTypeDomainName: - // Domain name's first byte is the length of the name - // Read domainAddrLen - _, err := io.ReadFull(reader, buffer[1:]) - if err != nil { - return "", 0, fmt.Errorf("failed to read domain address length: %w", err) - } - addrLen = int(buffer[1]) - default: - return "", 0, fmt.Errorf("unknown address type %#x", buffer[0]) - } - // Read host address - _, err = io.ReadFull(reader, buffer[:addrLen]) - if err != nil { - return "", 0, fmt.Errorf("failed to read address: %w", err) + sc.Close() + return nil, fmt.Errorf("could not connect to packet endpoint: %w", err) } - host := net.IP(buffer[:addrLen]).String() - // Read port number - _, err = io.ReadFull(reader, buffer[:2]) - if err != nil { - return "", 0, fmt.Errorf("failed to read port: %w", err) + return &packetConn{pc: proxyConn, sc: sc, dstAddr: proxyConn.RemoteAddr()}, nil +} + +func (c *Client) EnablePacketListener(endpoint transport.PacketEndpoint) error { + if endpoint == nil { + return errors.New("argument endpoint must not be nil") } - p := binary.BigEndian.Uint16(buffer[:2]) - portStr := strconv.FormatUint(uint64(p), 10) - addr := net.JoinHostPort(host, portStr) - return addr, addrLen, nil + c.pe = endpoint + return nil } diff --git a/transport/socks5/packet_dialer_test.go b/transport/socks5/packet_listener_test.go similarity index 85% rename from transport/socks5/packet_dialer_test.go rename to transport/socks5/packet_listener_test.go index c219fb59..c97b10a1 100644 --- a/transport/socks5/packet_dialer_test.go +++ b/transport/socks5/packet_listener_test.go @@ -14,7 +14,6 @@ import ( ) func TestSOCKS5Associate(t *testing.T) { - // Create a local listener // This creates a UDP server that responded to "ping" // message with "pong" response @@ -41,23 +40,24 @@ func TestSOCKS5Associate(t *testing.T) { time.Sleep(10 * time.Millisecond) // Connect, auth and connec to local server - dialer, err := NewDialer(&transport.TCPEndpoint{Address: proxyServerAddress}) - require.NotNil(t, dialer) + client, err := NewClient(&transport.TCPEndpoint{Address: proxyServerAddress}) + require.NotNil(t, client) require.NoError(t, err) - err = dialer.SetCredentials([]byte("testusername"), []byte("testpassword")) + err = client.SetCredentials([]byte("testusername"), []byte("testpassword")) require.NoError(t, err) - dialer.EnablePacket(&transport.UDPDialer{}) - conn, err := dialer.DialPacket(context.Background(), echoServerAddr.String()) + client.EnablePacket(&transport.UDPDialer{}) + conn, err := client.ListenPacket(context.Background()) require.NoError(t, err) defer conn.Close() // Send "ping" message - _, err = conn.Write([]byte("ping")) + _, err = conn.WriteTo([]byte("ping"), echoServerAddr) require.NoError(t, err) // max wait time for response conn.SetDeadline(time.Now().Add(time.Second)) response := make([]byte, 1024) - n, err := conn.Read(response) + n, addr, err := conn.ReadFrom(response) + require.Equal(t, echoServerAddr, addr) //conn.SetDeadline(time.Time{}) require.NoError(t, err) require.Equal(t, []byte("pong"), response[:n]) @@ -92,6 +92,7 @@ func setupUDPEchoServer(t *testing.T, serverAddr *net.UDPAddr) *net.UDPConn { //log.Printf("Error reading: %v", err) return } + //log.Printf("Received %s from %s\n", buf[:n], remote.String()) if bytes.Equal(buf[:n], []byte("ping")) { server.WriteTo([]byte("pong"), remote) } From f0ad65e9fcfc588bc2edb8ec30f82299c4ad8b08 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 18 Jul 2024 13:56:37 -0600 Subject: [PATCH 19/54] refactor: define readAddress and use for stream and packetlogic --- transport/socks5/socks5.go | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index c65921ed..4181a23f 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -18,6 +18,7 @@ import ( "encoding/binary" "errors" "fmt" + "io" "net" "strconv" ) @@ -126,3 +127,47 @@ func appendSOCKS5Address(b []byte, address string) ([]byte, error) { b = binary.BigEndian.AppendUint16(b, uint16(portNum)) return b, nil } + +func readAddress(reader io.Reader) (string, int, error) { + // Read the address type + // The maximum buffer size is: + // 1 address type + 1 address length + 256 (max domain name length) + var buffer [1 + 1 + 256]byte + addrLen := 0 + _, err := io.ReadFull(reader, buffer[:1]) + if err != nil { + return "", 0, fmt.Errorf("failed to read address type: %w", err) + } + // Read the address type + switch buffer[0] { + case addrTypeIPv4: + addrLen = 4 + case addrTypeIPv6: + addrLen = 16 + case addrTypeDomainName: + // Domain name's first byte is the length of the name + // Read domainAddrLen + _, err := io.ReadFull(reader, buffer[1:]) + if err != nil { + return "", 0, fmt.Errorf("failed to read domain address length: %w", err) + } + addrLen = int(buffer[1]) + default: + return "", 0, fmt.Errorf("unknown address type %#x", buffer[0]) + } + // Read host address + _, err = io.ReadFull(reader, buffer[:addrLen]) + if err != nil { + return "", 0, fmt.Errorf("failed to read address: %w", err) + } + host := net.IP(buffer[:addrLen]).String() + // Read port number + _, err = io.ReadFull(reader, buffer[:2]) + if err != nil { + return "", 0, fmt.Errorf("failed to read port: %w", err) + } + p := binary.BigEndian.Uint16(buffer[:2]) + portStr := strconv.FormatUint(uint64(p), 10) + addr := net.JoinHostPort(host, portStr) + return addr, addrLen, nil +} From 2e3b1739f3d1eb4de2ca5687537c540254a25d92 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 18 Jul 2024 13:57:02 -0600 Subject: [PATCH 20/54] refactor: rename new dialer to new client --- transport/socks5/stream_dialer_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 78c0ccb4..56721357 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -32,16 +32,16 @@ import ( ) func TestSOCKS5Dialer_NewStreamDialerNil(t *testing.T) { - dialer, err := NewDialer(nil) - require.Nil(t, dialer) + client, err := NewClient(nil) + require.Nil(t, client) require.Error(t, err) } func TestSOCKS5Dialer_BadConnection(t *testing.T) { - dialer, err := NewDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"}) - require.NotNil(t, dialer) + client, err := NewClient(&transport.TCPEndpoint{Address: "127.0.0.0:0"}) + require.NotNil(t, client) require.NoError(t, err) - _, err = dialer.DialStream(context.Background(), "example.com:443") + _, err = client.DialStream(context.Background(), "example.com:443") require.Error(t, err) } @@ -50,7 +50,7 @@ func TestSOCKS5Dialer_BadAddress(t *testing.T) { require.NoError(t, err, "Failed to create TCP listener: %v", err) defer listener.Close() - dialer, err := NewDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) + dialer, err := NewClient(&transport.TCPEndpoint{Address: listener.Addr().String()}) require.NotNil(t, dialer) require.NoError(t, err) @@ -97,9 +97,9 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req // Client go func() { defer running.Done() - dialer, err := NewDialer(&transport.TCPEndpoint{Address: listener.Addr().String()}) + client, err := NewClient(&transport.TCPEndpoint{Address: listener.Addr().String()}) require.NoError(tb, err) - serverConn, err := dialer.DialStream(context.Background(), destAddr) + serverConn, err := client.DialStream(context.Background(), destAddr) if replyCode != 0 { require.ErrorIs(tb, err, replyCode) var extractedReplyCode ReplyCode @@ -188,11 +188,11 @@ func TestConnectWithoutAuth(t *testing.T) { address := listener.Addr().String() // Create a SOCKS5 client - dialer, err := NewDialer(&transport.TCPEndpoint{Address: address}) - require.NotNil(t, dialer) + client, err := NewClient(&transport.TCPEndpoint{Address: address}) + require.NotNil(t, client) require.NoError(t, err) - _, err = dialer.DialStream(context.Background(), address) + _, err = client.DialStream(context.Background(), address) require.NoError(t, err) } @@ -221,7 +221,7 @@ func TestConnectWithAuth(t *testing.T) { // wait for server to start time.Sleep(10 * time.Millisecond) - dialer, err := NewDialer(&transport.TCPEndpoint{Address: address}) + dialer, err := NewClient(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) require.NoError(t, err) err = dialer.SetCredentials([]byte("testusername"), []byte("testpassword")) From 7bdae189ec4a50576fbac67e4acb99bcf0263e31 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 18 Jul 2024 13:57:33 -0600 Subject: [PATCH 21/54] refactor: rename new dialer to new client in stream dialer --- transport/socks5/stream_dialer.go | 37 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index ac4f4331..6facb547 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -32,23 +32,24 @@ type credentials struct { // NewDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 // proxy listening at the given [transport.StreamEndpoint]. -func NewDialer(streamEndpoint transport.StreamEndpoint) (*Dialer, error) { +func NewClient(streamEndpoint transport.StreamEndpoint) (*Client, error) { if streamEndpoint == nil { return nil, errors.New("argument endpoint must not be nil") } - return &Dialer{se: streamEndpoint, cred: nil}, nil + return &Client{se: streamEndpoint, cred: nil}, nil } -type Dialer struct { +type Client struct { se transport.StreamEndpoint + pe transport.PacketEndpoint pd transport.PacketDialer cred *credentials } -var _ transport.StreamDialer = (*Dialer)(nil) -var _ transport.PacketDialer = (*Dialer)(nil) +var _ transport.StreamDialer = (*Client)(nil) +var _ transport.PacketListener = (*Client)(nil) -func (d *Dialer) SetCredentials(username, password []byte) error { +func (c *Client) SetCredentials(username, password []byte) error { if len(username) > 255 { return errors.New("username exceeds 255 bytes") } @@ -63,16 +64,16 @@ func (d *Dialer) SetCredentials(username, password []byte) error { return errors.New("password must be at least 1 byte") } - d.cred = &credentials{username: username, password: password} + c.cred = &credentials{username: username, password: password} return nil } -func (d *Dialer) EnablePacket(packetDialer transport.PacketDialer) { - d.pd = packetDialer +func (c *Client) EnablePacket(packetDialer transport.PacketDialer) { + c.pd = packetDialer } -func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transport.StreamConn, string, error) { - proxyConn, err := d.se.ConnectStream(ctx) +func (c *Client) request(ctx context.Context, cmd byte, dstAddr string) (transport.StreamConn, string, error) { + proxyConn, err := c.se.ConnectStream(ctx) if err != nil { return nil, "", fmt.Errorf("could not connect to SOCKS5 proxy: %w", err) } @@ -93,7 +94,7 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo var buffer [(1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256]byte var b []byte - if d.cred == nil { + if c.cred == nil { // Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth) // +----+----------+----------+ // |VER | NMETHODS | METHODS | @@ -113,10 +114,10 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ b = append(b, 1) - b = append(b, byte(len(d.cred.username))) - b = append(b, d.cred.username...) - b = append(b, byte(len(d.cred.password))) - b = append(b, d.cred.password...) + b = append(b, byte(len(c.cred.username))) + b = append(b, c.cred.username...) + b = append(b, byte(len(c.cred.password))) + b = append(b, c.cred.password...) } // CMD Request: @@ -222,8 +223,8 @@ func (d *Dialer) request(ctx context.Context, cmd byte, dstAddr string) (transpo // the connect requests in one packet, to avoid an additional roundtrip. // The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which // you can check against the error constants in this package using [errors.Is]. -func (d *Dialer) DialStream(ctx context.Context, dstAddr string) (transport.StreamConn, error) { - proxyConn, _, err := d.request(ctx, CmdConnect, dstAddr) +func (c *Client) DialStream(ctx context.Context, dstAddr string) (transport.StreamConn, error) { + proxyConn, _, err := c.request(ctx, CmdConnect, dstAddr) if err != nil { return nil, err } From eeec84c0afe6be9f15afe194275faf9a2a0c41a6 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 18 Jul 2024 13:58:21 -0600 Subject: [PATCH 22/54] update wrapPacketDialerWithSOCKS5 with packet listener --- x/config/socks5.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/x/config/socks5.go b/x/config/socks5.go index 55ab4be7..f818b7e0 100644 --- a/x/config/socks5.go +++ b/x/config/socks5.go @@ -27,7 +27,7 @@ func wrapStreamDialerWithSOCKS5(innerSD func() (transport.StreamDialer, error), return nil, err } endpoint := transport.StreamDialerEndpoint{Dialer: sd, Address: configURL.Host} - dialer, err := socks5.NewDialer(&endpoint) + client, err := socks5.NewClient(&endpoint) if err != nil { return nil, err } @@ -35,21 +35,20 @@ func wrapStreamDialerWithSOCKS5(innerSD func() (transport.StreamDialer, error), if userInfo != nil { username := userInfo.Username() password, _ := userInfo.Password() - err := dialer.SetCredentials([]byte(username), []byte(password)) + err := client.SetCredentials([]byte(username), []byte(password)) if err != nil { return nil, err } } - return dialer, nil + + return client, nil } -func wrapPacketDialerWithSOCKS5(associateSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.PacketDialer, error) { - sd, err := associateSD() - if err != nil { - return nil, err - } - endpoint := transport.StreamDialerEndpoint{Dialer: sd, Address: configURL.Host} - dialer, err := socks5.NewDialer(&endpoint) +func wrapPacketDialerWithSOCKS5(_ func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.PacketDialer, error) { + // Use a TCP dialer to connect to the SOCKS5 server. + sd := &transport.TCPDialer{} + streamEndpoint := transport.StreamDialerEndpoint{Dialer: sd, Address: configURL.Host} + client, err := socks5.NewClient(&streamEndpoint) if err != nil { return nil, err } @@ -57,7 +56,7 @@ func wrapPacketDialerWithSOCKS5(associateSD func() (transport.StreamDialer, erro if userInfo != nil { username := userInfo.Username() password, _ := userInfo.Password() - err := dialer.SetCredentials([]byte(username), []byte(password)) + err := client.SetCredentials([]byte(username), []byte(password)) if err != nil { return nil, err } @@ -67,6 +66,7 @@ func wrapPacketDialerWithSOCKS5(associateSD func() (transport.StreamDialer, erro if err != nil { return nil, err } - dialer.EnablePacket(pd) - return dialer, nil + client.EnablePacket(pd) + packetDialer := transport.PacketListenerDialer{Listener: client} + return packetDialer, nil } From b81b28f46231e3dda65453ce4abfe38530402d65 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sat, 20 Jul 2024 19:08:56 -0600 Subject: [PATCH 23/54] using bytes.NewBuffer to read from buffer + code refactor --- transport/socks5/packet_listener.go | 106 ++++++++++++++-------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/transport/socks5/packet_listener.go b/transport/socks5/packet_listener.go index 06870861..af70268e 100644 --- a/transport/socks5/packet_listener.go +++ b/transport/socks5/packet_listener.go @@ -61,122 +61,124 @@ func (c *packetConn) SetWriteDeadline(t time.Time) error { return c.pc.SetWriteDeadline(t) } -func (p *packetConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { +// ReadFrom reads the packet from the SOCKS5 server and extract the payload +// The packet format is specified in https://datatracker.ietf.org/doc/html/rfc1928#section-7 +func (p *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { lazySlice := udpPool.LazySlice() buffer := lazySlice.Acquire() defer lazySlice.Release() - n, err = p.pc.Read(buffer) + + n, err := p.pc.Read(buffer) if err != nil { return 0, nil, err } - // Minimum size of header is 10 bytes - // 2 bytes for reserved, 1 byte for fragment, 1 byte for address type, 4 byte for ipv4, 2 bytes for port + // Minimum packet size if n < 10 { return 0, nil, fmt.Errorf("invalid SOCKS5 UDP packet: too short") } - pkt := buffer[:n] + // Using bytes.Buffer to handle data + buf := bytes.NewBuffer(buffer[:n]) - // Start parsing the header - rsv := pkt[:2] + // Read and check reserved bytes + rsv := make([]byte, 2) + if _, err := buf.Read(rsv); err != nil { + return 0, nil, err + } if rsv[0] != 0x00 || rsv[1] != 0x00 { return 0, nil, fmt.Errorf("invalid reserved bytes: expected 0x0000, got %#x%#x", rsv[0], rsv[1]) } - frag := pkt[2] + // Read fragment byte + frag, err := buf.ReadByte() + if err != nil { + return 0, nil, err + } if frag != 0 { return 0, nil, errors.New("fragmentation is not supported") } - // Do something with address? - address, addrLen, err := readAddress(bytes.NewReader(pkt[3:])) + // Read address using socks.ReadAddr which must now accept a bytes.Buffer directly + address, err := readAddr(buf) if err != nil { return 0, nil, fmt.Errorf("failed to read address: %w", err) } - // Calculate the start position of the actual data - headerLength := 4 + addrLen + 2 // RSV (2) + FRAG (1) + ATYP (1) + ADDR (variable) + PORT (2) - if n < headerLength { - return 0, nil, fmt.Errorf("invalid SOCKS5 UDP packet: header too short") + // Convert the address to a net.Addr + addr, err := transport.MakeNetAddr("udp", address.String()) + if err != nil { + return 0, nil, fmt.Errorf("failed to convert address: %w", err) } - // Copy the payload into the provided buffer - payloadLength := n - headerLength + // Payload handling: remaining bytes in the buffer are the payload + payload := buf.Bytes() + payloadLength := len(payload) if payloadLength > len(b) { return 0, nil, io.ErrShortBuffer } - copy(b, buffer[headerLength:n]) + copy(b, payload) - addr, err = net.ResolveUDPAddr("udp", address) - if err != nil { - return 0, nil, fmt.Errorf("failed to resolve address: %w", err) - } return payloadLength, addr, nil } +// Writeto encapsulates the payload in a SOCKS5 UDP packet as specified in +// https://datatracker.ietf.org/doc/html/rfc1928#section-7 +// and write it to the SOCKS5 server via the underlying connection. func (p *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { - // Encapsulate the payload in a SOCKS5 UDP packet as specified in - // https://datatracker.ietf.org/doc/html/rfc1928#section-7 + // The minimum preallocated header size (10 bytes) - header := make([]byte, 10) - header = append(header[:0], + lazySlice := udpPool.LazySlice() + buffer := lazySlice.Acquire() + defer lazySlice.Release() + buffer = append(buffer[:0], 0x00, 0x00, // Reserved 0x00, // Fragment number // To be appended below: // ATYP, IPv4, IPv6, Domain Name, Port ) - header, err := appendSOCKS5Address(header, addr.String()) + buffer, err := appendSOCKS5Address(buffer, addr.String()) if err != nil { return 0, fmt.Errorf("failed to append SOCKS5 address: %w", err) } // Combine the header and the payload - fullPacket := append(header, b...) - return p.pc.Write(fullPacket) + return p.pc.Write(append(buffer, b...)) } +// Close closes both the underlying stream and packet connections. func (p *packetConn) Close() error { return errors.Join(p.sc.Close(), p.pc.Close()) } -// DialPacket creates a packet [net.Conn] via SOCKS5. +// ListenPacket creates a [net.PacketConn] for dialing to SOCKS5 server. func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) { // Connect to the SOCKS5 server and perform UDP association - sc, bindAddr, err := c.request(ctx, CmdUDPAssociate, "0.0.0.0:0") + // Since local address is not known in advance, we use unspecified address + // which means the server is going to accept incoming packets from any address + // on the bind port on the server. The bind address is determined and returned by + // the server. + // https://datatracker.ietf.org/doc/html/rfc1928#section-6 + // Whoile binding address to specific client address has its advantages, it also creates some + // challenges such as NAT traveral if client is behind NAT. + sc, bindAddr, err := c.connectAndRequest(ctx, CmdUDPAssociate, "0.0.0.0:0") if err != nil { return nil, err } - host, port, err := net.SplitHostPort(bindAddr) - if err != nil { - return nil, fmt.Errorf("failed to parse bound address: %w", err) - } - - if ipAddr := net.ParseIP(host); ipAddr != nil && ipAddr.IsUnspecified() { + // If the returned bind IP address is unspecified (i.e. "0.0.0.0" or "::"), + // then use the IP address of the SOCKS5 server + if ipAddr := bindAddr.IP; ipAddr != nil && ipAddr.IsUnspecified() { schost, _, err := net.SplitHostPort(sc.RemoteAddr().String()) if err != nil { return nil, fmt.Errorf("failed to parse tcp address: %w", err) } - host = schost + bindAddr.IP = net.ParseIP(schost) } - packetEndpoint := &transport.PacketDialerEndpoint{Dialer: c.pd, Address: net.JoinHostPort(host, port)} - err = c.EnablePacketListener(packetEndpoint) - if err != nil { - return nil, fmt.Errorf("failed to enable packet listener: %w", err) - } - - proxyConn, err := c.pe.ConnectPacket(ctx) + packetEndpoint := &transport.PacketDialerEndpoint{Dialer: c.pd, Address: bindAddr.String()} + proxyConn, err := packetEndpoint.ConnectPacket(ctx) if err != nil { sc.Close() return nil, fmt.Errorf("could not connect to packet endpoint: %w", err) } return &packetConn{pc: proxyConn, sc: sc, dstAddr: proxyConn.RemoteAddr()}, nil } - -func (c *Client) EnablePacketListener(endpoint transport.PacketEndpoint) error { - if endpoint == nil { - return errors.New("argument endpoint must not be nil") - } - c.pe = endpoint - return nil -} From ee99f91f980b86285288759f00ba838da80fd7e6 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sat, 20 Jul 2024 19:09:31 -0600 Subject: [PATCH 24/54] testing Read Address --- transport/socks5/socks5_test.go | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/transport/socks5/socks5_test.go b/transport/socks5/socks5_test.go index 8dbfd26f..a98da55a 100644 --- a/transport/socks5/socks5_test.go +++ b/transport/socks5/socks5_test.go @@ -15,12 +15,99 @@ package socks5 import ( + "bytes" + "net" "strings" "testing" "github.com/stretchr/testify/require" ) +func TestReadAddr(t *testing.T) { + tests := []struct { + name string + input []byte + want *address + wantErr bool + }{ + + { + name: "IPv4 Example", + input: append([]byte{addrTypeIPv4}, append(net.IPv4(192, 168, 1, 1).To4(), []byte{0x01, 0xF4}...)...), + want: &address{IP: net.IPv4(192, 168, 1, 1), Port: 500}, + wantErr: false, + }, + { + name: "IPv6 Full", + input: append([]byte{addrTypeIPv6}, append(net.ParseIP("2001:db8::1").To16(), []byte{0x04, 0xD2}...)...), + want: &address{IP: net.ParseIP("2001:db8::1"), Port: 1234}, + wantErr: false, + }, + { + name: "IPv6 Compressed", + input: append([]byte{addrTypeIPv6}, append(net.ParseIP("fe80::204:61ff:fe9d:f156").To16(), []byte{0x00, 0x50}...)...), + want: &address{IP: net.ParseIP("fe80::204:61ff:fe9d:f156"), Port: 80}, + wantErr: false, + }, + { + name: "IPv6 Loopback", + input: append([]byte{addrTypeIPv6}, append(net.IPv6loopback.To16(), []byte{0x1F, 0x90}...)...), + want: &address{IP: net.IPv6loopback, Port: 8080}, + wantErr: false, + }, + { + name: "Domain Short", + input: append([]byte{addrTypeDomainName, 0x0b}, append([]byte("example.com"), []byte{0x23, 0x28}...)...), + want: &address{Name: "example.com", Port: 9000}, + wantErr: false, + }, + { + name: "Domain Long", + input: append([]byte{addrTypeDomainName, 0x3B}, append([]byte("very-long-domain-name-used-for-testing-purposes.example.com"), []byte{0x00, 0x50}...)...), + want: &address{Name: "very-long-domain-name-used-for-testing-purposes.example.com", Port: 80}, + wantErr: false, + }, + { + name: "Unrecognized Address Type", + input: []byte{0x00}, + want: nil, + wantErr: true, + }, + { + name: "Short Input", + input: []byte{addrTypeIPv4}, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := bytes.NewReader(tt.input) + got, err := readAddr(r) + if tt.wantErr { + require.Error(t, err, "Expected an error but got none") + } else { + require.NoError(t, err, "Did not expect an error but got one") + } + + if !tt.wantErr && !compareAddresses(got, tt.want) { + t.Errorf("readAddr() got = %v, want %v", got, tt.want) + } + }) + } +} + +func compareAddresses(a1, a2 *address) bool { + if a1 == nil || a2 == nil { + return a1 == a2 + } + if a1.IP != nil && !a1.IP.Equal(a2.IP) || a1.Name != a2.Name || a1.Port != a2.Port { + return false + } + return true +} + func TestAppendSOCKS5Address_IPv4(t *testing.T) { b := []byte{} b, err := appendSOCKS5Address(b, "8.8.8.8:853") From 8be47f8c446cfb90b00c007878e345269fb1adb7 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sat, 20 Jul 2024 19:10:07 -0600 Subject: [PATCH 25/54] defining address struct and updating readAddress --- transport/socks5/socks5.go | 89 +++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 4181a23f..af59d33e 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -87,6 +87,29 @@ const ( addrTypeIPv6 = 0x04 ) +// address is a SOCKS-specific address. +// Either Name or IP is used exclusively. +type address struct { + Name string // fully-qualified domain name + IP net.IP + Port int +} + +func (a *address) Network() string { return "socks5" } + +// Address returns a string suitable to dial; prefer returning IP-based +// address, fallback to Name +func (a *address) String() string { + if a == nil { + return "" + } + port := strconv.Itoa(a.Port) + if a.IP != nil { + return net.JoinHostPort(a.IP.String(), port) + } + return net.JoinHostPort(a.Name, port) +} + // appendSOCKS5Address adds the address to buffer b in SOCKS5 format, // as specified in https://datatracker.ietf.org/doc/html/rfc1928#section-4 func appendSOCKS5Address(b []byte, address string) ([]byte, error) { @@ -128,46 +151,44 @@ func appendSOCKS5Address(b []byte, address string) ([]byte, error) { return b, nil } -func readAddress(reader io.Reader) (string, int, error) { - // Read the address type - // The maximum buffer size is: - // 1 address type + 1 address length + 256 (max domain name length) - var buffer [1 + 1 + 256]byte - addrLen := 0 - _, err := io.ReadFull(reader, buffer[:1]) - if err != nil { - return "", 0, fmt.Errorf("failed to read address type: %w", err) +func readAddr(r io.Reader) (*address, error) { + address := &address{} + + var addrType [1]byte + if _, err := r.Read(addrType[:]); err != nil { + return nil, err } - // Read the address type - switch buffer[0] { + + switch addrType[0] { case addrTypeIPv4: - addrLen = 4 + addr := make(net.IP, net.IPv4len) + if _, err := io.ReadFull(r, addr); err != nil { + return nil, err + } + address.IP = addr case addrTypeIPv6: - addrLen = 16 + addr := make(net.IP, net.IPv6len) + if _, err := io.ReadFull(r, addr); err != nil { + return nil, err + } + address.IP = addr case addrTypeDomainName: - // Domain name's first byte is the length of the name - // Read domainAddrLen - _, err := io.ReadFull(reader, buffer[1:]) - if err != nil { - return "", 0, fmt.Errorf("failed to read domain address length: %w", err) + if _, err := r.Read(addrType[:]); err != nil { + return nil, err + } + addrLen := int(addrType[0]) + fqdn := make([]byte, addrLen) + if _, err := io.ReadFull(r, fqdn); err != nil { + return nil, err } - addrLen = int(buffer[1]) + address.Name = string(fqdn) default: - return "", 0, fmt.Errorf("unknown address type %#x", buffer[0]) + return nil, errors.New("unrecognized address type") } - // Read host address - _, err = io.ReadFull(reader, buffer[:addrLen]) - if err != nil { - return "", 0, fmt.Errorf("failed to read address: %w", err) - } - host := net.IP(buffer[:addrLen]).String() - // Read port number - _, err = io.ReadFull(reader, buffer[:2]) - if err != nil { - return "", 0, fmt.Errorf("failed to read port: %w", err) + var port [2]byte + if _, err := io.ReadFull(r, port[:]); err != nil { + return nil, err } - p := binary.BigEndian.Uint16(buffer[:2]) - portStr := strconv.FormatUint(uint64(p), 10) - addr := net.JoinHostPort(host, portStr) - return addr, addrLen, nil + address.Port = int(binary.BigEndian.Uint16(port[:])) + return address, nil } From 0e6bae3528d8c7025dbe65ce61726397d3ccc7ec Mon Sep 17 00:00:00 2001 From: amir gh Date: Sat, 20 Jul 2024 19:10:52 -0600 Subject: [PATCH 26/54] Refactor: request and remove redundant code --- transport/socks5/stream_dialer.go | 78 ++++++++++++++++++------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 6facb547..4a07cb5d 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -41,7 +41,6 @@ func NewClient(streamEndpoint transport.StreamEndpoint) (*Client, error) { type Client struct { se transport.StreamEndpoint - pe transport.PacketEndpoint pd transport.PacketDialer cred *credentials } @@ -72,18 +71,9 @@ func (c *Client) EnablePacket(packetDialer transport.PacketDialer) { c.pd = packetDialer } -func (c *Client) request(ctx context.Context, cmd byte, dstAddr string) (transport.StreamConn, string, error) { - proxyConn, err := c.se.ConnectStream(ctx) - if err != nil { - return nil, "", fmt.Errorf("could not connect to SOCKS5 proxy: %w", err) - } - dialSuccess := false - defer func() { - if !dialSuccess { - proxyConn.Close() - } - }() - +// request sends a SOCKS5 request to the server to perform a command (e.g., connect, udp associate), +// performs authentication (if provided), returns the bound address. +func (c *Client) request(conn io.ReadWriter, cmd byte, dstAddr string) (*address, error) { // For protocol details, see https://datatracker.ietf.org/doc/html/rfc1928#section-3 // Creating a single buffer for method selection, authentication, and connection request // Buffer large enough for method, auth, and connect requests with a domain name address. @@ -129,17 +119,17 @@ func (c *Client) request(ctx context.Context, cmd byte, dstAddr string) (transpo // +----+-----+-------+------+----------+----------+ b = append(b, 5, cmd, 0) // TODO: Probably more memory efficient if remoteAddr is added to the buffer directly. - b, err = appendSOCKS5Address(b, dstAddr) + b, err := appendSOCKS5Address(b, dstAddr) if err != nil { - return nil, "", fmt.Errorf("failed to create SOCKS5 address: %w", err) + return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err) } // We merge the method and CMD requests and only perform one write // because we send a single authentication method, so there's no point // in waiting for the response. This eliminates a roundtrip. - _, err = proxyConn.Write(b) + _, err = conn.Write(b) if err != nil { - return nil, "", fmt.Errorf("failed to write combined SOCKS5 request: %w", err) + return nil, fmt.Errorf("failed to write combined SOCKS5 request: %w", err) } // Reading the response: @@ -151,11 +141,11 @@ func (c *Client) request(ctx context.Context, cmd byte, dstAddr string) (transpo // +----+--------+ // buffer[0]: VER, buffer[1]: METHOD // Reuse buffer for better performance. - if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil { - return nil, "", fmt.Errorf("failed to read method server response: %w", err) + if _, err = io.ReadFull(conn, buffer[:2]); err != nil { + return nil, fmt.Errorf("failed to read method server response: %w", err) } if buffer[0] != 5 { - return nil, "", fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) + return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } switch buffer[1] { @@ -171,17 +161,17 @@ func (c *Client) request(ctx context.Context, cmd byte, dstAddr string) (transpo // +----+--------+ // VER = 1 means the server should be expecting username/password authentication. // buffer[2]: VER, buffer[3]: STATUS - if _, err = io.ReadFull(proxyConn, buffer[2:4]); err != nil { - return nil, "", fmt.Errorf("failed to read authentication version and status: %w", err) + if _, err = io.ReadFull(conn, buffer[2:4]); err != nil { + return nil, fmt.Errorf("failed to read authentication version and status: %w", err) } if buffer[2] != 1 { - return nil, "", fmt.Errorf("invalid authentication version %v. Expected 1", buffer[2]) + return nil, fmt.Errorf("invalid authentication version %v. Expected 1", buffer[2]) } if buffer[3] != 0 { - return nil, "", fmt.Errorf("authentication failed: %v", buffer[3]) + return nil, fmt.Errorf("authentication failed: %v", buffer[3]) } default: - return nil, "", fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1]) + return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1]) } // 3. Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT). @@ -195,26 +185,48 @@ func (c *Client) request(ctx context.Context, cmd byte, dstAddr string) (transpo // buffer[1]: REP // buffer[2]: RSV // buffer[3]: ATYP - if _, err = io.ReadFull(proxyConn, buffer[:3]); err != nil { - return nil, "", fmt.Errorf("failed to read connect server response: %w", err) + if _, err = io.ReadFull(conn, buffer[:3]); err != nil { + return nil, fmt.Errorf("failed to read connect server response: %w", err) } if buffer[0] != 5 { - return nil, "", fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) + return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0]) } // if REP is not 0, it means the server returned an error. if buffer[1] != 0 { - return nil, "", ReplyCode(buffer[1]) + return nil, ReplyCode(buffer[1]) } // 4. Read BND.ADDR. - bindAddr, _, err := readAddress(proxyConn) + bindAddr, err := readAddr(conn) + if err != nil { + return nil, fmt.Errorf("failed to read bound address: %w", err) + } + + return bindAddr, nil +} + +// connectAndRequest manages the connection lifecycle and delegates the SOCKS5 communication to the request function. +func (c *Client) connectAndRequest(ctx context.Context, cmd byte, dstAddr string) (transport.StreamConn, *address, error) { + proxyConn, err := c.se.ConnectStream(ctx) + if err != nil { + return nil, nil, fmt.Errorf("could not connect to SOCKS5 proxy: %w", err) + } + + // Close the connection if an error occurs in any subsequent steps + // to avoid keeping the connection open. + defer func(conn transport.StreamConn) { + if conn != nil && err != nil { + conn.Close() + } + }(proxyConn) + + bindAddr, err := c.request(proxyConn, cmd, dstAddr) if err != nil { - return nil, "", fmt.Errorf("failed to read bound address: %w", err) + return nil, nil, err } - dialSuccess = true return proxyConn, bindAddr, nil } @@ -224,7 +236,7 @@ func (c *Client) request(ctx context.Context, cmd byte, dstAddr string) (transpo // The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which // you can check against the error constants in this package using [errors.Is]. func (c *Client) DialStream(ctx context.Context, dstAddr string) (transport.StreamConn, error) { - proxyConn, _, err := c.request(ctx, CmdConnect, dstAddr) + proxyConn, _, err := c.connectAndRequest(ctx, CmdConnect, dstAddr) if err != nil { return nil, err } From 78bf0feb9a859265767cf861a1fdd3c4774c2135 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 21 Jul 2024 15:31:21 -0600 Subject: [PATCH 27/54] use innerSD to dial for associate command --- x/config/socks5.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x/config/socks5.go b/x/config/socks5.go index f818b7e0..105f6817 100644 --- a/x/config/socks5.go +++ b/x/config/socks5.go @@ -44,9 +44,11 @@ func wrapStreamDialerWithSOCKS5(innerSD func() (transport.StreamDialer, error), return client, nil } -func wrapPacketDialerWithSOCKS5(_ func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.PacketDialer, error) { - // Use a TCP dialer to connect to the SOCKS5 server. - sd := &transport.TCPDialer{} +func wrapPacketDialerWithSOCKS5(innerSD func() (transport.StreamDialer, error), innerPD func() (transport.PacketDialer, error), configURL *url.URL) (transport.PacketDialer, error) { + sd, err := innerSD() + if err != nil { + return nil, err + } streamEndpoint := transport.StreamDialerEndpoint{Dialer: sd, Address: configURL.Host} client, err := socks5.NewClient(&streamEndpoint) if err != nil { From 7320cb5750c8ebb83a4c76e7ace918389e8ca3b8 Mon Sep 17 00:00:00 2001 From: amir gh Date: Sun, 21 Jul 2024 15:35:44 -0600 Subject: [PATCH 28/54] fixing small issues in tests --- x/config/config_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x/config/config_test.go b/x/config/config_test.go index 2308c34c..6ded7a15 100644 --- a/x/config/config_test.go +++ b/x/config/config_test.go @@ -27,11 +27,11 @@ func TestSanitizeConfig(t *testing.T) { require.NoError(t, err) // Test that a invalid cypher is rejected. - sanitizedConfig, err := SanitizeConfig("split:5|ss://jhvdsjkfhvkhsadvf@example.com:1234?prefix=HTTP%2F1.1%20") + _, err = SanitizeConfig("split:5|ss://jhvdsjkfhvkhsadvf@example.com:1234?prefix=HTTP%2F1.1%20") require.Error(t, err) // Test that a valid config is accepted and user info is redacted. - sanitizedConfig, err = SanitizeConfig("split:5|ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20") + sanitizedConfig, err := SanitizeConfig("split:5|ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20") require.NoError(t, err) require.Equal(t, "split:5|ss://REDACTED@example.com:1234?prefix=HTTP%2F1.1+", sanitizedConfig) From 8ee279d8b968817b66474f84d693fd76681d302c Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 22 Jul 2024 23:02:33 -0600 Subject: [PATCH 29/54] tighten API with io.close use & netip.Addr --- transport/socks5/packet_listener.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/transport/socks5/packet_listener.go b/transport/socks5/packet_listener.go index af70268e..ca80e492 100644 --- a/transport/socks5/packet_listener.go +++ b/transport/socks5/packet_listener.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "net" + "net/netip" "time" "github.com/Jigsaw-Code/outline-sdk/internal/slicepool" @@ -34,9 +35,8 @@ const clientUDPBufferSize = 16 * 1024 var udpPool = slicepool.MakePool(clientUDPBufferSize) type packetConn struct { - dstAddr net.Addr - pc net.Conn - sc transport.StreamConn + pc net.Conn + sc io.Closer } var _ net.PacketConn = (*packetConn)(nil) @@ -45,10 +45,6 @@ func (p *packetConn) LocalAddr() net.Addr { return p.pc.LocalAddr() } -func (p *packetConn) RemoteAddr() net.Addr { - return p.dstAddr -} - func (p *packetConn) SetDeadline(t time.Time) error { return p.pc.SetDeadline(t) } @@ -105,7 +101,7 @@ func (p *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { } // Convert the address to a net.Addr - addr, err := transport.MakeNetAddr("udp", address.String()) + addr, err := transport.MakeNetAddr("udp", addrToString(address)) if err != nil { return 0, nil, fmt.Errorf("failed to convert address: %w", err) } @@ -121,7 +117,7 @@ func (p *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { return payloadLength, addr, nil } -// Writeto encapsulates the payload in a SOCKS5 UDP packet as specified in +// WriteTo encapsulates the payload in a SOCKS5 UDP packet as specified in // https://datatracker.ietf.org/doc/html/rfc1928#section-7 // and write it to the SOCKS5 server via the underlying connection. func (p *packetConn) WriteTo(b []byte, addr net.Addr) (int, error) { @@ -166,19 +162,22 @@ func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) { // If the returned bind IP address is unspecified (i.e. "0.0.0.0" or "::"), // then use the IP address of the SOCKS5 server - if ipAddr := bindAddr.IP; ipAddr != nil && ipAddr.IsUnspecified() { + if ipAddr := bindAddr.IP; ipAddr.IsValid() && ipAddr.IsUnspecified() { schost, _, err := net.SplitHostPort(sc.RemoteAddr().String()) if err != nil { return nil, fmt.Errorf("failed to parse tcp address: %w", err) } - bindAddr.IP = net.ParseIP(schost) + + bindAddr.IP, err = netip.ParseAddr(schost) + if err != nil { + return nil, fmt.Errorf("failed to parse bind address: %w", err) + } } - packetEndpoint := &transport.PacketDialerEndpoint{Dialer: c.pd, Address: bindAddr.String()} - proxyConn, err := packetEndpoint.ConnectPacket(ctx) + proxyConn, err := c.pd.DialPacket(ctx, addrToString(bindAddr)) if err != nil { sc.Close() return nil, fmt.Errorf("could not connect to packet endpoint: %w", err) } - return &packetConn{pc: proxyConn, sc: sc, dstAddr: proxyConn.RemoteAddr()}, nil + return &packetConn{pc: proxyConn, sc: sc}, nil } From d4fcda66726686bc4d0b3a249af825aa04739287 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 22 Jul 2024 23:03:15 -0600 Subject: [PATCH 30/54] tests: make more clear and add benchmark for readAddr --- transport/socks5/socks5_test.go | 91 +++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/transport/socks5/socks5_test.go b/transport/socks5/socks5_test.go index a98da55a..8394348e 100644 --- a/transport/socks5/socks5_test.go +++ b/transport/socks5/socks5_test.go @@ -16,7 +16,8 @@ package socks5 import ( "bytes" - "net" + "io" + "net/netip" "strings" "testing" @@ -33,31 +34,57 @@ func TestReadAddr(t *testing.T) { { name: "IPv4 Example", - input: append([]byte{addrTypeIPv4}, append(net.IPv4(192, 168, 1, 1).To4(), []byte{0x01, 0xF4}...)...), - want: &address{IP: net.IPv4(192, 168, 1, 1), Port: 500}, + input: []byte{addrTypeIPv4, 192, 168, 1, 1, 0x01, 0xF4}, + want: &address{IP: netip.AddrFrom4([4]byte{192, 168, 1, 1}), Port: 500}, wantErr: false, }, { - name: "IPv6 Full", - input: append([]byte{addrTypeIPv6}, append(net.ParseIP("2001:db8::1").To16(), []byte{0x04, 0xD2}...)...), - want: &address{IP: net.ParseIP("2001:db8::1"), Port: 1234}, + name: "IPv6 Full", + input: []byte{ + addrTypeIPv6, + 0x20, 0x01, 0x0d, 0xb8, // first 4 bytes of the IPv6 address + 0x00, 0x00, 0x00, 0x00, // middle zeroes are often omitted in shorthand notation + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, // last segment with the "1" + 0x04, 0xD2, // port number 1234 + }, + want: &address{IP: netip.MustParseAddr("2001:db8::1"), Port: 1234}, wantErr: false, }, { - name: "IPv6 Compressed", - input: append([]byte{addrTypeIPv6}, append(net.ParseIP("fe80::204:61ff:fe9d:f156").To16(), []byte{0x00, 0x50}...)...), - want: &address{IP: net.ParseIP("fe80::204:61ff:fe9d:f156"), Port: 80}, + name: "IPv6 Compressed", + input: []byte{ + addrTypeIPv6, + 0xfe, 0x80, 0x00, 0x00, // first 4 bytes with "fe80", and then three zeroed segments + 0x00, 0x00, 0x00, 0x00, + 0x02, 0x04, 0x61, 0xff, // "0204:61ff" + 0xfe, 0x9d, 0xf1, 0x56, // "fe9d:f156" + 0x00, 0x50, // port number 80 in hexadecimal + }, + want: &address{IP: netip.MustParseAddr("fe80::204:61ff:fe9d:f156"), Port: 80}, wantErr: false, }, { - name: "IPv6 Loopback", - input: append([]byte{addrTypeIPv6}, append(net.IPv6loopback.To16(), []byte{0x1F, 0x90}...)...), - want: &address{IP: net.IPv6loopback, Port: 8080}, + name: "IPv6 Loopback", + input: []byte{ + addrTypeIPv6, + 0x00, 0x00, 0x00, 0x00, // eight zeroed-out segments + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, // last segment is "0001" + 0x1F, 0x90, // port number 8080 in hexadecimal + }, + want: &address{IP: netip.IPv6Loopback(), Port: 8080}, wantErr: false, }, { - name: "Domain Short", - input: append([]byte{addrTypeDomainName, 0x0b}, append([]byte("example.com"), []byte{0x23, 0x28}...)...), + name: "Domain Short", + input: []byte{ + addrTypeDomainName, // Address type for domain name + 0x0b, // Length of the domain name "example.com" which is 11 characters + 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', // The domain name "example.com" + 0x23, 0x28, // Port number 9000 in hexadecimal + }, want: &address{Name: "example.com", Port: 9000}, wantErr: false, }, @@ -98,11 +125,45 @@ func TestReadAddr(t *testing.T) { } } +func BenchmarkReadAddr(b *testing.B) { + tests := []struct { + name string + input []byte + }{ + { + name: "IPv4", + input: append([]byte{addrTypeIPv4}, append(netip.AddrFrom4([4]byte{192, 168, 1, 1}).AsSlice(), []byte{0x00, 0x50}...)...), + }, + { + name: "IPv6", + input: append([]byte{addrTypeIPv6}, append(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}).AsSlice(), []byte{0x1F, 0x90}...)...), + }, + { + name: "Domain", + input: append([]byte{addrTypeDomainName, 0x0b}, append([]byte("example.com"), []byte{0x23, 0x28}...)...), + }, + } + + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { + reader := bytes.NewReader(tt.input) + b.ResetTimer() + for i := 0; i < b.N; i++ { + reader.Seek(0, io.SeekStart) + _, err := readAddr(reader) + if err != nil { + b.Error("readAddr failed:", err) + } + } + }) + } +} + func compareAddresses(a1, a2 *address) bool { if a1 == nil || a2 == nil { return a1 == a2 } - if a1.IP != nil && !a1.IP.Equal(a2.IP) || a1.Name != a2.Name || a1.Port != a2.Port { + if (a1.IP != netip.Addr{}) && a1.IP != a2.IP || a1.Name != a2.Name || a1.Port != a2.Port { return false } return true From 81ea1f238b097e0a09f55703125de019db1bd8ba Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 22 Jul 2024 23:04:09 -0600 Subject: [PATCH 31/54] use unit16 for port, netip.Addr & refactor readAddress --- transport/socks5/socks5.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index af59d33e..821db767 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "net" + "net/netip" "strconv" ) @@ -91,20 +92,18 @@ const ( // Either Name or IP is used exclusively. type address struct { Name string // fully-qualified domain name - IP net.IP - Port int + IP netip.Addr + Port uint16 } -func (a *address) Network() string { return "socks5" } - // Address returns a string suitable to dial; prefer returning IP-based // address, fallback to Name -func (a *address) String() string { +func addrToString(a *address) string { if a == nil { return "" } - port := strconv.Itoa(a.Port) - if a.IP != nil { + port := strconv.Itoa(int(a.Port)) + if a.IP.IsValid() { return net.JoinHostPort(a.IP.String(), port) } return net.JoinHostPort(a.Name, port) @@ -161,17 +160,17 @@ func readAddr(r io.Reader) (*address, error) { switch addrType[0] { case addrTypeIPv4: - addr := make(net.IP, net.IPv4len) - if _, err := io.ReadFull(r, addr); err != nil { + var addr [4]byte + if _, err := io.ReadFull(r, addr[:]); err != nil { return nil, err } - address.IP = addr + address.IP = netip.AddrFrom4(addr) case addrTypeIPv6: - addr := make(net.IP, net.IPv6len) - if _, err := io.ReadFull(r, addr); err != nil { + var addr [16]byte + if _, err := io.ReadFull(r, addr[:]); err != nil { return nil, err } - address.IP = addr + address.IP = netip.AddrFrom16(addr) case addrTypeDomainName: if _, err := r.Read(addrType[:]); err != nil { return nil, err @@ -189,6 +188,6 @@ func readAddr(r io.Reader) (*address, error) { if _, err := io.ReadFull(r, port[:]); err != nil { return nil, err } - address.Port = int(binary.BigEndian.Uint16(port[:])) + address.Port = binary.BigEndian.Uint16(port[:]) return address, nil } From c96b843519c92311ba04b5d8d8237f3f379865f1 Mon Sep 17 00:00:00 2001 From: amir gh Date: Wed, 24 Jul 2024 12:28:18 -0600 Subject: [PATCH 32/54] tests: removed hardcoded ports & wait for server to be ready --- transport/socks5/packet_listener_test.go | 52 ++++++++++++++++-------- transport/socks5/stream_dialer_test.go | 6 ++- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/transport/socks5/packet_listener_test.go b/transport/socks5/packet_listener_test.go index c97b10a1..f1d8a55e 100644 --- a/transport/socks5/packet_listener_test.go +++ b/transport/socks5/packet_listener_test.go @@ -19,7 +19,7 @@ func TestSOCKS5Associate(t *testing.T) { // message with "pong" response locIP := net.ParseIP("127.0.0.1") // Create a local listener - echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12199} + echoServerAddr := &net.UDPAddr{IP: locIP, Port: 0} echoServer := setupUDPEchoServer(t, echoServerAddr) defer echoServer.Close() @@ -29,17 +29,25 @@ func TestSOCKS5Associate(t *testing.T) { }} proxySrv := socks5.NewServer( socks5.WithAuthMethods([]socks5.Authenticator{cator}), - //socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))), ) - // Start listening - proxyServerAddress := "127.0.0.1:12355" + + // Create SOCKS5 proxy on localhost with a random port + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + proxyServerAddress := listener.Addr().String() + go func() { - err := proxySrv.ListenAndServe("tcp", proxyServerAddress) + err := proxySrv.Serve(listener) + defer listener.Close() require.NoError(t, err) }() - time.Sleep(10 * time.Millisecond) - // Connect, auth and connec to local server + // Wait until the server is ready. + ready := make(chan bool, 1) + go waitUntilServerReady(proxyServerAddress, ready) + <-ready + + // Connect to local proxy, auth and start the PacketConn. client, err := NewClient(&transport.TCPEndpoint{Address: proxyServerAddress}) require.NotNil(t, client) require.NoError(t, err) @@ -50,29 +58,27 @@ func TestSOCKS5Associate(t *testing.T) { require.NoError(t, err) defer conn.Close() - // Send "ping" message - _, err = conn.WriteTo([]byte("ping"), echoServerAddr) + // Send "ping" message. + _, err = conn.WriteTo([]byte("ping"), echoServer.LocalAddr()) require.NoError(t, err) - // max wait time for response + // Max wait time for response. conn.SetDeadline(time.Now().Add(time.Second)) response := make([]byte, 1024) n, addr, err := conn.ReadFrom(response) - require.Equal(t, echoServerAddr, addr) - //conn.SetDeadline(time.Time{}) + require.Equal(t, echoServer.LocalAddr().String(), addr.String()) require.NoError(t, err) require.Equal(t, []byte("pong"), response[:n]) } func TestUDPLoopBack(t *testing.T) { - // Create a local listener + // Create a local listener. locIP := net.ParseIP("127.0.0.1") - // Create a local listener - echoServerAddr := &net.UDPAddr{IP: locIP, Port: 12199} + echoServerAddr := &net.UDPAddr{IP: locIP, Port: 0} echoServer := setupUDPEchoServer(t, echoServerAddr) defer echoServer.Close() packDialer := transport.UDPDialer{} - conn, err := packDialer.DialPacket(context.Background(), echoServerAddr.String()) + conn, err := packDialer.DialPacket(context.Background(), echoServer.LocalAddr().String()) require.NoError(t, err) conn.Write([]byte("ping")) response := make([]byte, 1024) @@ -89,10 +95,8 @@ func setupUDPEchoServer(t *testing.T, serverAddr *net.UDPAddr) *net.UDPConn { for { n, remote, err := server.ReadFrom(buf) if err != nil { - //log.Printf("Error reading: %v", err) return } - //log.Printf("Received %s from %s\n", buf[:n], remote.String()) if bytes.Equal(buf[:n], []byte("ping")) { server.WriteTo([]byte("pong"), remote) } @@ -105,3 +109,15 @@ func setupUDPEchoServer(t *testing.T, serverAddr *net.UDPAddr) *net.UDPConn { return server } + +func waitUntilServerReady(addr string, ready chan<- bool) { + for { + conn, err := net.Dial("tcp", addr) + if err == nil { + conn.Close() + ready <- true + return + } + time.Sleep(10 * time.Millisecond) + } +} diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index 56721357..ba545dbe 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -218,8 +218,10 @@ func TestConnectWithAuth(t *testing.T) { defer listener.Close() require.NoError(t, err) }() - // wait for server to start - time.Sleep(10 * time.Millisecond) + // wait for server to start. + ready := make(chan bool, 1) + go waitUntilServerReady(address, ready) + <-ready dialer, err := NewClient(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) From 48786cc43d2abfb599d611fd557a4e1d1458de66 Mon Sep 17 00:00:00 2001 From: amir gh Date: Wed, 24 Jul 2024 12:28:51 -0600 Subject: [PATCH 33/54] fix error --- transport/socks5/packet_listener.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/packet_listener.go b/transport/socks5/packet_listener.go index ca80e492..fbbbf4ed 100644 --- a/transport/socks5/packet_listener.go +++ b/transport/socks5/packet_listener.go @@ -70,7 +70,7 @@ func (p *packetConn) ReadFrom(b []byte) (int, net.Addr, error) { } // Minimum packet size if n < 10 { - return 0, nil, fmt.Errorf("invalid SOCKS5 UDP packet: too short") + return 0, nil, errors.New("invalid SOCKS5 UDP packet: too short") } // Using bytes.Buffer to handle data From 6a3650959db692d4ccfe723b3bea804c027c6d0f Mon Sep 17 00:00:00 2001 From: amir gh Date: Wed, 24 Jul 2024 12:29:25 -0600 Subject: [PATCH 34/54] ensure addrLength is under 255 --- transport/socks5/socks5.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 821db767..603d8133 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -176,6 +176,9 @@ func readAddr(r io.Reader) (*address, error) { return nil, err } addrLen := int(addrType[0]) + if addrLen > 255 { + return nil, fmt.Errorf("domain name length = %v is over 255", addrLen) + } fqdn := make([]byte, addrLen) if _, err := io.ReadFull(r, fqdn); err != nil { return nil, err From ce03ed6efa3e4949c18d464061ce4a07c84c658e Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Wed, 24 Jul 2024 12:32:40 -0600 Subject: [PATCH 35/54] Update transport/socks5/stream_dialer.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 4a07cb5d..be7cffe7 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -214,16 +214,9 @@ func (c *Client) connectAndRequest(ctx context.Context, cmd byte, dstAddr string return nil, nil, fmt.Errorf("could not connect to SOCKS5 proxy: %w", err) } - // Close the connection if an error occurs in any subsequent steps - // to avoid keeping the connection open. - defer func(conn transport.StreamConn) { - if conn != nil && err != nil { - conn.Close() - } - }(proxyConn) - bindAddr, err := c.request(proxyConn, cmd, dstAddr) if err != nil { + proxyConn.Close() return nil, nil, err } From aa185720ab4eb0be5852e14ba48fa49f0548e9c2 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Wed, 24 Jul 2024 12:32:58 -0600 Subject: [PATCH 36/54] Update transport/socks5/stream_dialer.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index be7cffe7..339eb4ce 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -67,6 +67,7 @@ func (c *Client) SetCredentials(username, password []byte) error { return nil } +// EnablePacket enables the use of the [Client] as a [transport.PacketListener]. It takes the [transport.PacketDialer] used to connect to the SOCKS5 packet endpoint. func (c *Client) EnablePacket(packetDialer transport.PacketDialer) { c.pd = packetDialer } From 55db9981e5a49a59b4a682c03a4d81f7555674a9 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Wed, 24 Jul 2024 12:33:09 -0600 Subject: [PATCH 37/54] Update transport/socks5/socks5_test.go Co-authored-by: Vinicius Fortuna --- transport/socks5/socks5_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/socks5_test.go b/transport/socks5/socks5_test.go index 8394348e..8e474530 100644 --- a/transport/socks5/socks5_test.go +++ b/transport/socks5/socks5_test.go @@ -90,7 +90,7 @@ func TestReadAddr(t *testing.T) { }, { name: "Domain Long", - input: append([]byte{addrTypeDomainName, 0x3B}, append([]byte("very-long-domain-name-used-for-testing-purposes.example.com"), []byte{0x00, 0x50}...)...), + input: append([]byte{addrTypeDomainName, 0x3B}, append([]byte("very-long-domain-name-used-for-testing-purposes.example.com"), 0x00, 0x50)...), want: &address{Name: "very-long-domain-name-used-for-testing-purposes.example.com", Port: 80}, wantErr: false, }, From 54342ee3d4f03e3b55d43e73d501c620cab28598 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Wed, 24 Jul 2024 12:33:29 -0600 Subject: [PATCH 38/54] Update transport/socks5/stream_dialer.go Co-authored-by: Vinicius Fortuna --- transport/socks5/stream_dialer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go index 339eb4ce..63fc68d3 100644 --- a/transport/socks5/stream_dialer.go +++ b/transport/socks5/stream_dialer.go @@ -30,7 +30,7 @@ type credentials struct { password []byte } -// NewDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5 +// NewClient creates a SOCKS5 client that routes connections to a SOCKS5 // proxy listening at the given [transport.StreamEndpoint]. func NewClient(streamEndpoint transport.StreamEndpoint) (*Client, error) { if streamEndpoint == nil { From a8a8772c06e831fc49673a2efb2b82e6f0bc81c7 Mon Sep 17 00:00:00 2001 From: amir gh Date: Wed, 24 Jul 2024 12:47:44 -0600 Subject: [PATCH 39/54] tests: use string for IPv4 --- transport/socks5/socks5.go | 2 +- transport/socks5/socks5_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 603d8133..1a996021 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -177,7 +177,7 @@ func readAddr(r io.Reader) (*address, error) { } addrLen := int(addrType[0]) if addrLen > 255 { - return nil, fmt.Errorf("domain name length = %v is over 255", addrLen) + return nil, fmt.Errorf("domain name length %v is over 255", addrLen) } fqdn := make([]byte, addrLen) if _, err := io.ReadFull(r, fqdn); err != nil { diff --git a/transport/socks5/socks5_test.go b/transport/socks5/socks5_test.go index 8e474530..1c3df418 100644 --- a/transport/socks5/socks5_test.go +++ b/transport/socks5/socks5_test.go @@ -35,7 +35,7 @@ func TestReadAddr(t *testing.T) { { name: "IPv4 Example", input: []byte{addrTypeIPv4, 192, 168, 1, 1, 0x01, 0xF4}, - want: &address{IP: netip.AddrFrom4([4]byte{192, 168, 1, 1}), Port: 500}, + want: &address{IP: netip.MustParseAddr("192.168.1.1"), Port: 500}, wantErr: false, }, { From a577cb611b8ff92ff84dde353ee01a43409b9181 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:06:05 -0600 Subject: [PATCH 40/54] Update transport/socks5/packet_listener_test.go Co-authored-by: Vinicius Fortuna --- transport/socks5/packet_listener_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/transport/socks5/packet_listener_test.go b/transport/socks5/packet_listener_test.go index f1d8a55e..5f07a461 100644 --- a/transport/socks5/packet_listener_test.go +++ b/transport/socks5/packet_listener_test.go @@ -34,6 +34,7 @@ func TestSOCKS5Associate(t *testing.T) { // Create SOCKS5 proxy on localhost with a random port listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) + defer listener.Close() proxyServerAddress := listener.Addr().String() go func() { From 95573359fb46afca531e0185de85b330c963e30f Mon Sep 17 00:00:00 2001 From: amir gh Date: Wed, 24 Jul 2024 19:16:22 -0600 Subject: [PATCH 41/54] reverse the last change --- transport/socks5/packet_listener_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/transport/socks5/packet_listener_test.go b/transport/socks5/packet_listener_test.go index 5f07a461..f1d8a55e 100644 --- a/transport/socks5/packet_listener_test.go +++ b/transport/socks5/packet_listener_test.go @@ -34,7 +34,6 @@ func TestSOCKS5Associate(t *testing.T) { // Create SOCKS5 proxy on localhost with a random port listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - defer listener.Close() proxyServerAddress := listener.Addr().String() go func() { From f81b4008bda748748a29689fa0200e3f11eb860e Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 25 Jul 2024 17:15:20 -0600 Subject: [PATCH 42/54] test:removing unnecessary wait for server listening --- transport/socks5/packet_listener_test.go | 25 ++++-------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/transport/socks5/packet_listener_test.go b/transport/socks5/packet_listener_test.go index f1d8a55e..d5ed2f85 100644 --- a/transport/socks5/packet_listener_test.go +++ b/transport/socks5/packet_listener_test.go @@ -14,16 +14,16 @@ import ( ) func TestSOCKS5Associate(t *testing.T) { - // Create a local listener + // Create a local listener. // This creates a UDP server that responded to "ping" - // message with "pong" response + // message with "pong" response. locIP := net.ParseIP("127.0.0.1") // Create a local listener echoServerAddr := &net.UDPAddr{IP: locIP, Port: 0} echoServer := setupUDPEchoServer(t, echoServerAddr) defer echoServer.Close() - // Create a socks server to proxy "ping" message + // Create a socks server to proxy "ping" message. cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{ "testusername": "testpassword", }} @@ -31,7 +31,7 @@ func TestSOCKS5Associate(t *testing.T) { socks5.WithAuthMethods([]socks5.Authenticator{cator}), ) - // Create SOCKS5 proxy on localhost with a random port + // Create SOCKS5 proxy on localhost with a random port. listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) proxyServerAddress := listener.Addr().String() @@ -42,11 +42,6 @@ func TestSOCKS5Associate(t *testing.T) { require.NoError(t, err) }() - // Wait until the server is ready. - ready := make(chan bool, 1) - go waitUntilServerReady(proxyServerAddress, ready) - <-ready - // Connect to local proxy, auth and start the PacketConn. client, err := NewClient(&transport.TCPEndpoint{Address: proxyServerAddress}) require.NotNil(t, client) @@ -109,15 +104,3 @@ func setupUDPEchoServer(t *testing.T, serverAddr *net.UDPAddr) *net.UDPConn { return server } - -func waitUntilServerReady(addr string, ready chan<- bool) { - for { - conn, err := net.Dial("tcp", addr) - if err == nil { - conn.Close() - ready <- true - return - } - time.Sleep(10 * time.Millisecond) - } -} From 8e149bc25fb4a279f3d1e8a011e86a75c4efe986 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 25 Jul 2024 17:16:20 -0600 Subject: [PATCH 43/54] test:removing casting addrLength to type int --- transport/socks5/socks5.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/transport/socks5/socks5.go b/transport/socks5/socks5.go index 1a996021..ebebcf71 100644 --- a/transport/socks5/socks5.go +++ b/transport/socks5/socks5.go @@ -175,10 +175,9 @@ func readAddr(r io.Reader) (*address, error) { if _, err := r.Read(addrType[:]); err != nil { return nil, err } - addrLen := int(addrType[0]) - if addrLen > 255 { - return nil, fmt.Errorf("domain name length %v is over 255", addrLen) - } + addrLen := addrType[0] + // addrLen btye type maximum value is 255 which + // prevents passing larger then 255 values for domain names. fqdn := make([]byte, addrLen) if _, err := io.ReadFull(r, fqdn); err != nil { return nil, err From 0d4ef7719592098a666ebc081db775c8f17ca8d7 Mon Sep 17 00:00:00 2001 From: amir gh Date: Thu, 25 Jul 2024 17:16:48 -0600 Subject: [PATCH 44/54] test:removing unnecessary wait for server listening from stream --- transport/socks5/stream_dialer_test.go | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index ba545dbe..c986e4da 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -23,7 +23,6 @@ import ( "sync" "testing" "testing/iotest" - "time" "github.com/Jigsaw-Code/outline-sdk/transport" "github.com/stretchr/testify/assert" @@ -168,10 +167,10 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req } func TestConnectWithoutAuth(t *testing.T) { - // Create a SOCKS5 server + // Create a SOCKS5 server. server := socks5.NewServer() - // Create SOCKS5 proxy on localhost with a random port + // Create SOCKS5 proxy on localhost with a random port. listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) @@ -182,12 +181,9 @@ func TestConnectWithoutAuth(t *testing.T) { require.NoError(t, err) }() - // wait for server to start - time.Sleep(10 * time.Millisecond) - address := listener.Addr().String() - // Create a SOCKS5 client + // Create a SOCKS5 client. client, err := NewClient(&transport.TCPEndpoint{Address: address}) require.NotNil(t, client) require.NoError(t, err) @@ -207,7 +203,7 @@ func TestConnectWithAuth(t *testing.T) { socks5.WithAuthMethods([]socks5.Authenticator{cator}), ) - // Create SOCKS5 proxy on localhost with a random port + // Create SOCKS5 proxy on localhost with a random port. listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) address := listener.Addr().String() @@ -218,10 +214,6 @@ func TestConnectWithAuth(t *testing.T) { defer listener.Close() require.NoError(t, err) }() - // wait for server to start. - ready := make(chan bool, 1) - go waitUntilServerReady(address, ready) - <-ready dialer, err := NewClient(&transport.TCPEndpoint{Address: address}) require.NotNil(t, dialer) @@ -231,7 +223,7 @@ func TestConnectWithAuth(t *testing.T) { _, err = dialer.DialStream(context.Background(), address) require.NoError(t, err) - // Try to connect with incorrect credentials + // Try to connect with incorrect credentials. err = dialer.SetCredentials([]byte("testusername"), []byte("wrongpassword")) require.NoError(t, err) _, err = dialer.DialStream(context.Background(), address) From 470a9290ec57334bc44ff634ca30a369a8371791 Mon Sep 17 00:00:00 2001 From: amir gh Date: Fri, 26 Jul 2024 15:26:35 -0600 Subject: [PATCH 45/54] ensure socks server goroutine terminates by closing the listener --- transport/socks5/packet_listener_test.go | 7 +++++-- transport/socks5/stream_dialer_test.go | 12 ++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/transport/socks5/packet_listener_test.go b/transport/socks5/packet_listener_test.go index d5ed2f85..0fc628bb 100644 --- a/transport/socks5/packet_listener_test.go +++ b/transport/socks5/packet_listener_test.go @@ -3,6 +3,7 @@ package socks5 import ( "bytes" "context" + "errors" "net" "testing" "time" @@ -34,12 +35,14 @@ func TestSOCKS5Associate(t *testing.T) { // Create SOCKS5 proxy on localhost with a random port. listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) + defer listener.Close() proxyServerAddress := listener.Addr().String() go func() { err := proxySrv.Serve(listener) - defer listener.Close() - require.NoError(t, err) + if !errors.Is(err, net.ErrClosed) && err != nil { + require.NoError(t, err) // Assert no error if it's not the expected close error + } }() // Connect to local proxy, auth and start the PacketConn. diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go index c986e4da..6ce0b597 100644 --- a/transport/socks5/stream_dialer_test.go +++ b/transport/socks5/stream_dialer_test.go @@ -173,12 +173,14 @@ func TestConnectWithoutAuth(t *testing.T) { // Create SOCKS5 proxy on localhost with a random port. listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) + defer listener.Close() go func() { err := server.Serve(listener) - defer listener.Close() t.Log("server is listening...") - require.NoError(t, err) + if !errors.Is(err, net.ErrClosed) && err != nil { + require.NoError(t, err) // Assert no error if it's not the expected close error + } }() address := listener.Addr().String() @@ -206,13 +208,15 @@ func TestConnectWithAuth(t *testing.T) { // Create SOCKS5 proxy on localhost with a random port. listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) + defer listener.Close() address := listener.Addr().String() // Create SOCKS5 proxy on localhost port 8001 go func() { err := server.Serve(listener) - defer listener.Close() - require.NoError(t, err) + if !errors.Is(err, net.ErrClosed) && err != nil { + require.NoError(t, err) // Assert no error if it's not the expected close error + } }() dialer, err := NewClient(&transport.TCPEndpoint{Address: address}) From 1f1002ba2cb29ffd98ce98a43aa601b8766dc0c5 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:12:17 -0600 Subject: [PATCH 46/54] Update go.mod --- x/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/go.mod b/x/go.mod index e9c14043..b9673de3 100644 --- a/x/go.mod +++ b/x/go.mod @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x go 1.21 require ( - github.com/Jigsaw-Code/outline-sdk v0.0.16 + github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 // Use github.com/Psiphon-Labs/psiphon-tunnel-core@staging-client as per // https://github.com/Psiphon-Labs/psiphon-tunnel-core/?tab=readme-ov-file#using-psiphon-with-go-modules github.com/Psiphon-Labs/psiphon-tunnel-core v1.0.11-0.20240619172145-03cade11f647 From 86a73a04c860574e3367d08b61b3820506a82f03 Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:14:02 -0600 Subject: [PATCH 47/54] Update go.sum --- x/go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/go.sum b/x/go.sum index 7f9a2635..37a086a2 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,3 +1,5 @@ +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 h1:1t6cnyP4liW9hjmnVrDn6Bc5m2qlAcjPurf9UEye4Hg= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= filippo.io/bigmod v0.0.1 h1:OaEqDr3gEbofpnHbGqZweSL/bLMhy1pb54puiCDeuOA= filippo.io/bigmod v0.0.1/go.mod h1:KyzqAbH7bRH6MOuOF1TPfUjvLoi0mRF2bIyD2ouRNQI= filippo.io/keygen v0.0.0-20230306160926-5201437acf8e h1:+xwUCyMiCWKWsI0RowhzB4sngpUdMHgU6lLuWJCX5Dg= From 8c8cd4e416f9031da19df53c340481a81bbc5ccf Mon Sep 17 00:00:00 2001 From: Amir Gh <117060873+amircybersec@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:20:02 -0600 Subject: [PATCH 48/54] Update go.sum --- x/go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/go.sum b/x/go.sum index 37a086a2..61bd22d3 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,3 +1,5 @@ +github.com/Jigsaw-Code/outline-sdk v0.0.15 h1:2OfYum4vllfIgoDa/X9drA2I57knXFPREv4kMZkjTuI= +github.com/Jigsaw-Code/outline-sdk v0.0.15/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 h1:1t6cnyP4liW9hjmnVrDn6Bc5m2qlAcjPurf9UEye4Hg= github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= filippo.io/bigmod v0.0.1 h1:OaEqDr3gEbofpnHbGqZweSL/bLMhy1pb54puiCDeuOA= From 439d13b9d06926e60fde2fac0dd6f8c2e6a83919 Mon Sep 17 00:00:00 2001 From: amir gh Date: Fri, 26 Jul 2024 17:30:28 -0600 Subject: [PATCH 49/54] update mod files --- x/go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/x/go.sum b/x/go.sum index 61bd22d3..37a086a2 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,5 +1,3 @@ -github.com/Jigsaw-Code/outline-sdk v0.0.15 h1:2OfYum4vllfIgoDa/X9drA2I57knXFPREv4kMZkjTuI= -github.com/Jigsaw-Code/outline-sdk v0.0.15/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 h1:1t6cnyP4liW9hjmnVrDn6Bc5m2qlAcjPurf9UEye4Hg= github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= filippo.io/bigmod v0.0.1 h1:OaEqDr3gEbofpnHbGqZweSL/bLMhy1pb54puiCDeuOA= From c05e496e635301b4113be43cd080ae1d068c7113 Mon Sep 17 00:00:00 2001 From: amir gh Date: Fri, 26 Jul 2024 17:43:14 -0600 Subject: [PATCH 50/54] check return err values --- transport/socks5/packet_listener_test.go | 11 ++++++++--- transport/socks5/socks5_test.go | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/transport/socks5/packet_listener_test.go b/transport/socks5/packet_listener_test.go index 0fc628bb..0d2ac053 100644 --- a/transport/socks5/packet_listener_test.go +++ b/transport/socks5/packet_listener_test.go @@ -60,7 +60,8 @@ func TestSOCKS5Associate(t *testing.T) { _, err = conn.WriteTo([]byte("ping"), echoServer.LocalAddr()) require.NoError(t, err) // Max wait time for response. - conn.SetDeadline(time.Now().Add(time.Second)) + err = conn.SetDeadline(time.Now().Add(time.Second)) + require.NoError(t, err) response := make([]byte, 1024) n, addr, err := conn.ReadFrom(response) require.Equal(t, echoServer.LocalAddr().String(), addr.String()) @@ -78,7 +79,8 @@ func TestUDPLoopBack(t *testing.T) { packDialer := transport.UDPDialer{} conn, err := packDialer.DialPacket(context.Background(), echoServer.LocalAddr().String()) require.NoError(t, err) - conn.Write([]byte("ping")) + _, err = conn.Write([]byte("ping")) + require.NoError(t, err) response := make([]byte, 1024) n, err := conn.Read(response) require.NoError(t, err) @@ -96,7 +98,10 @@ func setupUDPEchoServer(t *testing.T, serverAddr *net.UDPAddr) *net.UDPConn { return } if bytes.Equal(buf[:n], []byte("ping")) { - server.WriteTo([]byte("pong"), remote) + _, err := server.WriteTo([]byte("pong"), remote) + if err != nil { + return + } } } }() diff --git a/transport/socks5/socks5_test.go b/transport/socks5/socks5_test.go index 1c3df418..f5696251 100644 --- a/transport/socks5/socks5_test.go +++ b/transport/socks5/socks5_test.go @@ -149,9 +149,10 @@ func BenchmarkReadAddr(b *testing.B) { reader := bytes.NewReader(tt.input) b.ResetTimer() for i := 0; i < b.N; i++ { - reader.Seek(0, io.SeekStart) - _, err := readAddr(reader) - if err != nil { + if _, err := reader.Seek(0, io.SeekStart); err != nil { + b.Error("Seek failed:", err) + } + if _, err := readAddr(reader); err != nil { b.Error("readAddr failed:", err) } } From 9e6e65f20c85ebad168d6585ca813e8c56a0d359 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 29 Jul 2024 09:51:42 -0600 Subject: [PATCH 51/54] update go.sum --- x/go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x/go.sum b/x/go.sum index 37a086a2..61bd22d3 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,3 +1,5 @@ +github.com/Jigsaw-Code/outline-sdk v0.0.15 h1:2OfYum4vllfIgoDa/X9drA2I57knXFPREv4kMZkjTuI= +github.com/Jigsaw-Code/outline-sdk v0.0.15/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 h1:1t6cnyP4liW9hjmnVrDn6Bc5m2qlAcjPurf9UEye4Hg= github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= filippo.io/bigmod v0.0.1 h1:OaEqDr3gEbofpnHbGqZweSL/bLMhy1pb54puiCDeuOA= From 94cc2aef746061788b0855af7420c4036b5ed88f Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 29 Jul 2024 09:59:57 -0600 Subject: [PATCH 52/54] update go.mod and go.sum --- x/go.mod | 2 +- x/go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/x/go.mod b/x/go.mod index b9673de3..03a9acf7 100644 --- a/x/go.mod +++ b/x/go.mod @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x go 1.21 require ( - github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 + github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240725231648-0d4ef7719592 // Use github.com/Psiphon-Labs/psiphon-tunnel-core@staging-client as per // https://github.com/Psiphon-Labs/psiphon-tunnel-core/?tab=readme-ov-file#using-psiphon-with-go-modules github.com/Psiphon-Labs/psiphon-tunnel-core v1.0.11-0.20240619172145-03cade11f647 diff --git a/x/go.sum b/x/go.sum index 61bd22d3..cd812475 100644 --- a/x/go.sum +++ b/x/go.sum @@ -1,7 +1,3 @@ -github.com/Jigsaw-Code/outline-sdk v0.0.15 h1:2OfYum4vllfIgoDa/X9drA2I57knXFPREv4kMZkjTuI= -github.com/Jigsaw-Code/outline-sdk v0.0.15/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= -github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 h1:1t6cnyP4liW9hjmnVrDn6Bc5m2qlAcjPurf9UEye4Hg= -github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= filippo.io/bigmod v0.0.1 h1:OaEqDr3gEbofpnHbGqZweSL/bLMhy1pb54puiCDeuOA= filippo.io/bigmod v0.0.1/go.mod h1:KyzqAbH7bRH6MOuOF1TPfUjvLoi0mRF2bIyD2ouRNQI= filippo.io/keygen v0.0.0-20230306160926-5201437acf8e h1:+xwUCyMiCWKWsI0RowhzB4sngpUdMHgU6lLuWJCX5Dg= @@ -10,6 +6,8 @@ github.com/AndreasBriese/bbloom v0.0.0-20170702084017-28f7e881ca57 h1:CVuXDbdzPW github.com/AndreasBriese/bbloom v0.0.0-20170702084017-28f7e881ca57/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240725231648-0d4ef7719592 h1:32i7krnS7FD7MxDy2TeLdEZimsVhKVZ4vlNU1dLl/4A= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240725231648-0d4ef7719592/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e h1:NPfqIbzmijrl0VclX2t8eO5EPBhqe47LLGKpRrcVjXk= github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e/go.mod h1:ZdY5pBfat/WVzw3eXbIf7N1nZN0XD5H5+X8ZMDWbCs4= github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7 h1:Hx/NCZTnvoKZuIBwSmxE58KKoNLXIGG6hBJYN7pj9Ag= From 53ff0595984bed87a6476d8b8bc3689f802aa04a Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 29 Jul 2024 10:28:38 -0600 Subject: [PATCH 53/54] update go.mod and go.sum to @470a929 --- x/go.mod | 2 +- x/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x/go.mod b/x/go.mod index 03a9acf7..0b322d15 100644 --- a/x/go.mod +++ b/x/go.mod @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x go 1.21 require ( - github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240725231648-0d4ef7719592 + github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240726212635-470a9290ec57 // Use github.com/Psiphon-Labs/psiphon-tunnel-core@staging-client as per // https://github.com/Psiphon-Labs/psiphon-tunnel-core/?tab=readme-ov-file#using-psiphon-with-go-modules github.com/Psiphon-Labs/psiphon-tunnel-core v1.0.11-0.20240619172145-03cade11f647 diff --git a/x/go.sum b/x/go.sum index cd812475..7c6f5f80 100644 --- a/x/go.sum +++ b/x/go.sum @@ -6,8 +6,8 @@ github.com/AndreasBriese/bbloom v0.0.0-20170702084017-28f7e881ca57 h1:CVuXDbdzPW github.com/AndreasBriese/bbloom v0.0.0-20170702084017-28f7e881ca57/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240725231648-0d4ef7719592 h1:32i7krnS7FD7MxDy2TeLdEZimsVhKVZ4vlNU1dLl/4A= -github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240725231648-0d4ef7719592/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240726212635-470a9290ec57 h1:XNSV0dGW48J8DmdmCnk/txGHf9glAPqa6Xme/rFWn7c= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240726212635-470a9290ec57/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e h1:NPfqIbzmijrl0VclX2t8eO5EPBhqe47LLGKpRrcVjXk= github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e/go.mod h1:ZdY5pBfat/WVzw3eXbIf7N1nZN0XD5H5+X8ZMDWbCs4= github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7 h1:Hx/NCZTnvoKZuIBwSmxE58KKoNLXIGG6hBJYN7pj9Ag= From 1db188b6d5abd700ca5652cdb02cb3aefa0458e1 Mon Sep 17 00:00:00 2001 From: amir gh Date: Mon, 29 Jul 2024 10:33:15 -0600 Subject: [PATCH 54/54] update doc.go for socks5 --- x/config/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/config/doc.go b/x/config/doc.go index 539fcf29..a34e3fa6 100644 --- a/x/config/doc.go +++ b/x/config/doc.go @@ -40,7 +40,7 @@ Shadowsocks proxy (compatible with Outline's access keys, package [github.com/Ji ss://[USERINFO]@[HOST]:[PORT]?prefix=[PREFIX] -SOCKS5 proxy (currently streams only, package [github.com/Jigsaw-Code/outline-sdk/transport/socks5]) +SOCKS5 proxy (works with both stream and packet dialers, package [github.com/Jigsaw-Code/outline-sdk/transport/socks5]) socks5://[USERINFO]@[HOST]:[PORT]