Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding UDP support to Socks5 dialer #257

Merged
merged 55 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
cc54025
Start PacketDialer
fortuna May 8, 2024
a285b13
Add stuff
fortuna May 8, 2024
927a81d
fix: associate command typo
amircybersec Jul 2, 2024
52bbcbe
feat: impelment read/write encapsulation
amircybersec Jul 2, 2024
2fe6b06
some debugging lines added
amircybersec Jul 2, 2024
1ebea67
test: packet dialer tests
amircybersec Jul 2, 2024
5bb95cc
tests: with local udp echoserver and socks5 proxy
amircybersec Jul 8, 2024
95096b5
refactor: clean up
amircybersec Jul 8, 2024
2839607
ading wrapPacketDialerWithSOCKS5
amircybersec Jul 8, 2024
4f51b36
registering socks5 packet dialer type
amircybersec Jul 8, 2024
f75974e
update go mod/sum
amircybersec Jul 8, 2024
6026ef4
removed logging and some improvements
amircybersec Jul 8, 2024
e3f0a56
code review improvements
amircybersec Jul 10, 2024
3ffc663
add spec reference to udp dialer
amircybersec Jul 15, 2024
587bb40
feat: use slicepool + readAdress refactor
amircybersec Jul 16, 2024
036091b
refactor: use readAdress with proxyConn in stread dialer
amircybersec Jul 16, 2024
333ccf6
preallocate buffer size in read address
amircybersec Jul 16, 2024
92a280d
feat: use packet listenter interface
amircybersec Jul 18, 2024
f0ad65e
refactor: define readAddress and use for stream and packetlogic
amircybersec Jul 18, 2024
2e3b173
refactor: rename new dialer to new client
amircybersec Jul 18, 2024
7bdae18
refactor: rename new dialer to new client in stream dialer
amircybersec Jul 18, 2024
eeec84c
update wrapPacketDialerWithSOCKS5 with packet listener
amircybersec Jul 18, 2024
b81b28f
using bytes.NewBuffer to read from buffer + code refactor
amircybersec Jul 21, 2024
ee99f91
testing Read Address
amircybersec Jul 21, 2024
8be47f8
defining address struct and updating readAddress
amircybersec Jul 21, 2024
0e6bae3
Refactor: request and remove redundant code
amircybersec Jul 21, 2024
78bf0fe
use innerSD to dial for associate command
amircybersec Jul 21, 2024
7320cb5
fixing small issues in tests
amircybersec Jul 21, 2024
8ee279d
tighten API with io.close use & netip.Addr
amircybersec Jul 23, 2024
d4fcda6
tests: make more clear and add benchmark for readAddr
amircybersec Jul 23, 2024
81ea1f2
use unit16 for port, netip.Addr & refactor readAddress
amircybersec Jul 23, 2024
c96b843
tests: removed hardcoded ports & wait for server to be ready
amircybersec Jul 24, 2024
48786cc
fix error
amircybersec Jul 24, 2024
6a36509
ensure addrLength is under 255
amircybersec Jul 24, 2024
ce03ed6
Update transport/socks5/stream_dialer.go
amircybersec Jul 24, 2024
aa18572
Update transport/socks5/stream_dialer.go
amircybersec Jul 24, 2024
55db998
Update transport/socks5/socks5_test.go
amircybersec Jul 24, 2024
54342ee
Update transport/socks5/stream_dialer.go
amircybersec Jul 24, 2024
a8a8772
tests: use string for IPv4
amircybersec Jul 24, 2024
a577cb6
Update transport/socks5/packet_listener_test.go
amircybersec Jul 25, 2024
9557335
reverse the last change
amircybersec Jul 25, 2024
f81b400
test:removing unnecessary wait for server listening
amircybersec Jul 25, 2024
8e149bc
test:removing casting addrLength to type int
amircybersec Jul 25, 2024
0d4ef77
test:removing unnecessary wait for server listening from stream
amircybersec Jul 25, 2024
470a929
ensure socks server goroutine terminates by closing the listener
amircybersec Jul 26, 2024
74c5857
Merge branch 'main' into fortuna-socks
amircybersec Jul 26, 2024
1f1002b
Update go.mod
amircybersec Jul 26, 2024
86a73a0
Update go.sum
amircybersec Jul 26, 2024
8c8cd4e
Update go.sum
amircybersec Jul 26, 2024
439d13b
update mod files
amircybersec Jul 26, 2024
c05e496
check return err values
amircybersec Jul 26, 2024
9e6e65f
update go.sum
amircybersec Jul 29, 2024
94cc2ae
update go.mod and go.sum
amircybersec Jul 29, 2024
53ff059
update go.mod and go.sum to @470a929
amircybersec Jul 29, 2024
1db188b
update doc.go for socks5
amircybersec Jul 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions transport/socks5/packet_dialer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// 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"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
)

type packetConn struct {
dstAddr net.Addr
pc net.Conn
sc transport.StreamConn
}

var _ net.Conn = (*packetConn)(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) {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
// TODO: read header
amircybersec marked this conversation as resolved.
Show resolved Hide resolved
buffer := make([]byte, 65536) // Maximum size for UDP packet
fortuna marked this conversation as resolved.
Show resolved Hide resolved
n, err := c.pc.Read(buffer)
fortuna marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, err
}
// Minimum size of header is 10 bytes
if n < 10 {
return 0, fmt.Errorf("invalid SOCKS5 UDP packet: too short")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return 0, fmt.Errorf("invalid SOCKS5 UDP packet: too short")
return 0, errors.New("invalid SOCKS5 UDP packet: too short")

}
amircybersec marked this conversation as resolved.
Show resolved Hide resolved

// 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")
amircybersec marked this conversation as resolved.
Show resolved Hide resolved
}

atyp := buffer[3]
fortuna marked this conversation as resolved.
Show resolved Hide resolved
addrLen := 0
switch atyp {
case addrTypeIPv4:
addrLen = net.IPv4len
case addrTypeIPv6:
addrLen = net.IPv6len
amircybersec marked this conversation as resolved.
Show resolved Hide resolved
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?
amircybersec marked this conversation as resolved.
Show resolved Hide resolved
payloadLength = len(b)
}
copy(b, buffer[headerLength:n])

return payloadLength, nil
}

func (c *packetConn) Write(b []byte) (int, error) {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
// Encapsulate the payload in a SOCKS5 UDP packet
header := []byte{
amircybersec marked this conversation as resolved.
Show resolved Hide resolved
0x00, 0x00, // Reserved
0x00, // Fragment number
// To be appended below: ATYP, IPv4, IPv6, Domain name
// To be appended below: IP and port (destination address)
}
header, err := appendSOCKS5Address(header, c.dstAddr.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)
}

func (c *packetConn) Close() error {
return errors.Join(c.sc.Close(), c.pc.Close())
}

// DialPacket creates a packet [net.Conn] via SOCKS5.
func (d *Dialer) DialPacket(ctx context.Context, dstAddr string) (net.Conn, error) {
fortuna marked this conversation as resolved.
Show resolved Hide resolved
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")
fortuna marked this conversation as resolved.
Show resolved Hide resolved
//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)
}

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)
}
host = schost
}

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)
}

return &packetConn{netDstAddr, pc, sc}, nil
}
106 changes: 106 additions & 0 deletions transport/socks5/packet_dialer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package socks5

import (
"bytes"
"context"
"net"
"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"
)

func TestSOCKS5Associate(t *testing.T) {

// Create a local listener
// This creates a UDP server that responded to "ping"
// message with "pong" response
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{
"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)

// 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.UDPDialer{})
conn, err := dialer.DialPacket(context.Background(), echoServerAddr.String())
require.NoError(t, err)
defer conn.Close()

// Send "ping" message
_, err = conn.Write([]byte("ping"))
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)
//conn.SetDeadline(time.Time{})
require.NoError(t, err)
require.Equal(t, []byte("pong"), response[:n])
}

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: 12199}
echoServer := setupUDPEchoServer(t, echoServerAddr)
defer echoServer.Close()

packDialer := transport.UDPDialer{}
conn, err := packDialer.DialPacket(context.Background(), echoServerAddr.String())
require.NoError(t, err)
conn.Write([]byte("ping"))
response := make([]byte, 1024)
n, err := conn.Read(response)
require.NoError(t, err)
assert.Equal(t, []byte("pong"), response[:n])
}

func setupUDPEchoServer(t *testing.T, serverAddr *net.UDPAddr) *net.UDPConn {
server, err := net.ListenUDP("udp", serverAddr)
require.NoError(t, err)
go func() {
buf := make([]byte, 2048)
for {
n, remote, err := server.ReadFrom(buf)
if err != nil {
//log.Printf("Error reading: %v", err)
return
}
if bytes.Equal(buf[:n], []byte("ping")) {
server.WriteTo([]byte("pong"), remote)
}
}
}()

t.Cleanup(func() {
server.Close()
})

return server
}
7 changes: 7 additions & 0 deletions transport/socks5/socks5.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(3)
)

// SOCKS5 authentication methods, as specified in https://datatracker.ietf.org/doc/html/rfc1928#section-3
const (
authMethodNoAuth = 0x00
Expand Down
Loading