Skip to content

Commit 212ef96

Browse files
authored
feat: add User/Pass Authentication to Sock5 Dialer with Tests (#189)
1 parent 9fe7a48 commit 212ef96

File tree

5 files changed

+243
-67
lines changed

5 files changed

+243
-67
lines changed

go.mod

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ require (
66
github.com/eycorsican/go-tun2socks v1.16.11
77
github.com/google/gopacket v1.1.19
88
github.com/shadowsocks/go-shadowsocks2 v0.1.5
9-
github.com/stretchr/testify v1.8.2
10-
golang.org/x/crypto v0.17.0
11-
golang.org/x/net v0.19.0
9+
github.com/stretchr/testify v1.8.4
10+
github.com/things-go/go-socks5 v0.0.5
11+
golang.org/x/crypto v0.18.0
12+
golang.org/x/net v0.20.0
1213
)
1314

1415
require (
@@ -17,7 +18,7 @@ require (
1718
github.com/kr/text v0.2.0 // indirect
1819
github.com/pmezard/go-difflib v1.0.0 // indirect
1920
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
20-
golang.org/x/sys v0.15.0 // indirect
21+
golang.org/x/sys v0.16.0 // indirect
2122
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
2223
gopkg.in/yaml.v3 v3.0.1 // indirect
2324
)

go.sum

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
32
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
43
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
54
github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8=
@@ -19,38 +18,34 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstv
1918
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
2019
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
2120
github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
22-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
23-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
24-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
25-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
26-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
27-
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
28-
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
21+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
22+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
23+
github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8=
24+
github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ=
2925
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
3026
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
3127
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
32-
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
33-
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
28+
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
29+
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
3430
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
3531
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
3632
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
3733
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
3834
golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
39-
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
40-
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
35+
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
36+
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
4137
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
4238
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
4339
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4440
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45-
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
46-
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
41+
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
42+
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
4743
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
4844
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
4945
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
5046
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5147
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
5248
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
5349
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
54-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5550
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5651
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

transport/socks5/socks5.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ const (
3737
ErrAddressTypeNotSupported = ReplyCode(0x08)
3838
)
3939

40+
// SOCKS5 authentication methods, as specified in https://datatracker.ietf.org/doc/html/rfc1928#section-3
41+
const (
42+
authMethodNoAuth = 0x00
43+
authMethodUserPass = 0x02
44+
)
45+
4046
var _ error = (ReplyCode)(0)
4147

4248
// Error returns a human-readable description of the error, based on the SOCKS5 RFC.

transport/socks5/stream_dialer.go

Lines changed: 151 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,54 @@ import (
2323
"github.com/Jigsaw-Code/outline-sdk/transport"
2424
)
2525

26+
// https://datatracker.ietf.org/doc/html/rfc1929
27+
// Credentials can be nil, and that means no authentication.
28+
type credentials struct {
29+
username []byte
30+
password []byte
31+
}
32+
2633
// NewStreamDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5
2734
// proxy listening at the given [transport.StreamEndpoint].
28-
func NewStreamDialer(endpoint transport.StreamEndpoint) (transport.StreamDialer, error) {
35+
func NewStreamDialer(endpoint transport.StreamEndpoint) (*StreamDialer, error) {
2936
if endpoint == nil {
3037
return nil, errors.New("argument endpoint must not be nil")
3138
}
32-
return &streamDialer{proxyEndpoint: endpoint}, nil
39+
return &StreamDialer{proxyEndpoint: endpoint, cred: nil}, nil
3340
}
3441

35-
type streamDialer struct {
42+
type StreamDialer struct {
3643
proxyEndpoint transport.StreamEndpoint
44+
cred *credentials
3745
}
3846

39-
var _ transport.StreamDialer = (*streamDialer)(nil)
47+
var _ transport.StreamDialer = (*StreamDialer)(nil)
48+
49+
func (c *StreamDialer) SetCredentials(username, password []byte) error {
50+
if len(username) > 255 {
51+
return errors.New("username exceeds 255 bytes")
52+
}
53+
if len(username) == 0 {
54+
return errors.New("username must be at least 1 byte")
55+
}
56+
57+
if len(password) > 255 {
58+
return errors.New("password exceeds 255 bytes")
59+
}
60+
if len(password) == 0 {
61+
return errors.New("password must be at least 1 byte")
62+
}
63+
64+
c.cred = &credentials{username: username, password: password}
65+
return nil
66+
}
4067

4168
// DialStream implements [transport.StreamDialer].DialStream using SOCKS5.
42-
// It will send the method and the connect requests in one packet, to avoid an unnecessary roundtrip.
69+
// It will send the auth method, auth credentials (if auth is chosen), and
70+
// the connect requests in one packet, to avoid an additional roundtrip.
4371
// The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which
4472
// you can check against the error constants in this package using [errors.Is].
45-
func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
73+
func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
4674
proxyConn, err := c.proxyEndpoint.ConnectStream(ctx)
4775
if err != nil {
4876
return nil, fmt.Errorf("could not connect to SOCKS5 proxy: %w", err)
@@ -55,80 +83,155 @@ func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (trans
5583
}()
5684

5785
// For protocol details, see https://datatracker.ietf.org/doc/html/rfc1928#section-3
58-
59-
// Buffer large enough for method and connect requests with a domain name address.
60-
header := [3 + 4 + 256 + 2]byte{}
61-
62-
// Method request:
63-
// VER = 5, NMETHODS = 1, METHODS = 0 (no auth)
64-
b := append(header[:0], 5, 1, 0)
86+
// Creating a single buffer for method selection, authentication, and connection request
87+
// Buffer large enough for method, auth, and connect requests with a domain name address.
88+
// The maximum buffer size is:
89+
// 3 (1 socks version + 1 method selection + 1 methods)
90+
// + 1 (auth version) + 1 (username length) + 255 (username) + 1 (password length) + 255 (password)
91+
// + 256 (max domain name length)
92+
var buffer [(1 + 1 + 1) + (1 + 1 + 255 + 1 + 255) + 256]byte
93+
var b []byte
94+
95+
if c.cred == nil {
96+
// Method selection part: VER = 5, NMETHODS = 1, METHODS = 0 (no auth)
97+
// +----+----------+----------+
98+
// |VER | NMETHODS | METHODS |
99+
// +----+----------+----------+
100+
// | 1 | 1 | 1 to 255 |
101+
// +----+----------+----------+
102+
b = append(buffer[:0], 5, 1, 0)
103+
} else {
104+
// https://datatracker.ietf.org/doc/html/rfc1929
105+
// Method selection part: VER = 5, NMETHODS = 1, METHODS = 2 (username/password)
106+
b = append(buffer[:0], 5, 1, authMethodUserPass)
107+
108+
// Authentication part: VER = 1, ULEN = 1, UNAME = 1~255, PLEN = 1, PASSWD = 1~255
109+
// +----+------+----------+------+----------+
110+
// |VER | ULEN | UNAME | PLEN | PASSWD |
111+
// +----+------+----------+------+----------+
112+
// | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
113+
// +----+------+----------+------+----------+
114+
b = append(b, 1)
115+
b = append(b, byte(len(c.cred.username)))
116+
b = append(b, c.cred.username...)
117+
b = append(b, byte(len(c.cred.password)))
118+
b = append(b, c.cred.password...)
119+
}
65120

66121
// Connect request:
67-
// VER = 5, CMD = 1 (connect), RSV = 0
122+
// VER = 5, CMD = 1 (connect), RSV = 0, DST.ADDR, DST.PORT
123+
// +----+-----+-------+------+----------+----------+
124+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
125+
// +----+-----+-------+------+----------+----------+
126+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
127+
// +----+-----+-------+------+----------+----------+
68128
b = append(b, 5, 1, 0)
69-
// Destination address Address (ATYP, DST.ADDR, DST.PORT)
129+
// TODO: Probably more memory efficient if remoteAddr is added to the buffer directly.
70130
b, err = appendSOCKS5Address(b, remoteAddr)
71131
if err != nil {
72132
return nil, fmt.Errorf("failed to create SOCKS5 address: %w", err)
73133
}
74134

75-
// We merge the method and connect requests because we send a single authentication
76-
// method, so there's no point in waiting for the response. This eliminates a roundtrip.
135+
// We merge the method and connect requests and only perform one write
136+
// because we send a single authentication method, so there's no point
137+
// in waiting for the response. This eliminates a roundtrip.
77138
_, err = proxyConn.Write(b)
78139
if err != nil {
79-
return nil, fmt.Errorf("failed to write SOCKS5 request: %w", err)
140+
return nil, fmt.Errorf("failed to write combined SOCKS5 request: %w", err)
80141
}
81142

82-
// Read method response (VER, METHOD).
83-
if _, err = io.ReadFull(proxyConn, header[:2]); err != nil {
84-
return nil, fmt.Errorf("failed to read method server response")
143+
// Reading the response:
144+
// 1. Read method response (VER, METHOD).
145+
// +----+--------+
146+
// |VER | METHOD |
147+
// +----+--------+
148+
// | 1 | 1 |
149+
// +----+--------+
150+
// buffer[0]: VER, buffer[1]: METHOD
151+
// Reuse buffer for better performance.
152+
if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil {
153+
return nil, fmt.Errorf("failed to read method server response: %w", err)
85154
}
86-
if header[0] != 5 {
87-
return nil, fmt.Errorf("invalid protocol version %v. Expected 5", header[0])
155+
if buffer[0] != 5 {
156+
return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0])
88157
}
89-
if header[1] != 0 {
90-
return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 0 (no auth)", header[1])
158+
159+
switch buffer[1] {
160+
case authMethodNoAuth:
161+
// No authentication required.
162+
case authMethodUserPass:
163+
// 2. Read authentication version and status
164+
// VER = 1, STATUS = 0
165+
// +----+--------+
166+
// |VER | STATUS |
167+
// +----+--------+
168+
// | 1 | 1 |
169+
// +----+--------+
170+
// VER = 1 means the server should be expecting username/password authentication.
171+
// buffer[2]: VER, buffer[3]: STATUS
172+
if _, err = io.ReadFull(proxyConn, buffer[2:4]); err != nil {
173+
return nil, fmt.Errorf("failed to read authentication version and status: %w", err)
174+
}
175+
if buffer[2] != 1 {
176+
return nil, fmt.Errorf("invalid authentication version %v. Expected 1", buffer[2])
177+
}
178+
if buffer[3] != 0 {
179+
return nil, fmt.Errorf("authentication failed: %v", buffer[3])
180+
}
181+
default:
182+
return nil, fmt.Errorf("unsupported SOCKS authentication method %v. Expected 2", buffer[1])
91183
}
92184

93-
// Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT).
185+
// 3. Read connect response (VER, REP, RSV, ATYP, BND.ADDR, BND.PORT).
94186
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6.
95-
if _, err = io.ReadFull(proxyConn, header[:4]); err != nil {
96-
return nil, fmt.Errorf("failed to read connect server response")
187+
// +----+-----+-------+------+----------+----------+
188+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
189+
// +----+-----+-------+------+----------+----------+
190+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
191+
// +----+-----+-------+------+----------+----------+
192+
// buffer[0]: VER
193+
// buffer[1]: REP
194+
// buffer[2]: RSV
195+
// buffer[3]: ATYP
196+
if _, err = io.ReadFull(proxyConn, buffer[:4]); err != nil {
197+
return nil, fmt.Errorf("failed to read connect server response: %w", err)
97198
}
98-
if header[0] != 5 {
99-
return nil, fmt.Errorf("invalid protocol version %v. Expected 5", header[0])
199+
200+
if buffer[0] != 5 {
201+
return nil, fmt.Errorf("invalid protocol version %v. Expected 5", buffer[0])
100202
}
101203

102-
// Check reply code (REP)
103-
if header[1] != 0 {
104-
return nil, ReplyCode(header[1])
204+
// if REP is not 0, it means the server returned an error.
205+
if buffer[1] != 0 {
206+
return nil, ReplyCode(buffer[1])
105207
}
106208

107-
toRead := 0
108-
switch header[3] {
209+
// 4. Read address and length
210+
var bndAddrLen int
211+
switch buffer[3] {
109212
case addrTypeIPv4:
110-
toRead = 4
213+
bndAddrLen = 4
111214
case addrTypeIPv6:
112-
toRead = 16
215+
bndAddrLen = 16
113216
case addrTypeDomainName:
114-
_, err := io.ReadFull(proxyConn, header[:1])
217+
// buffer[8]: length of the domain name
218+
_, err := io.ReadFull(proxyConn, buffer[:1])
115219
if err != nil {
116220
return nil, fmt.Errorf("failed to read address length in connect response: %w", err)
117221
}
118-
toRead = int(header[0])
222+
bndAddrLen = int(buffer[0])
223+
default:
224+
return nil, fmt.Errorf("invalid address type %v", buffer[3])
119225
}
120-
// Reads the bound address and port, but we currently ignore them.
226+
// 5. Reads the bound address and port, but we currently ignore them.
121227
// TODO(fortuna): Should we expose the remote bound address as the net.Conn.LocalAddr()?
122-
_, err = io.ReadFull(proxyConn, header[:toRead])
123-
if err != nil {
124-
return nil, fmt.Errorf("failed to read address in connect response: %w", err)
228+
if _, err := io.ReadFull(proxyConn, buffer[:bndAddrLen]); err != nil {
229+
return nil, fmt.Errorf("failed to read bound address: %w", err)
125230
}
126-
// We also ignore the remote bound port number.
127-
_, err = io.ReadFull(proxyConn, header[:2])
128-
if err != nil {
129-
return nil, fmt.Errorf("failed to read port number in connect response: %w", err)
231+
// We read but ignore the remote bound port number: BND.PORT
232+
if _, err = io.ReadFull(proxyConn, buffer[:2]); err != nil {
233+
return nil, fmt.Errorf("failed to read bound port: %w", err)
130234
}
131-
132235
dialSuccess = true
133236
return proxyConn, nil
134237
}

0 commit comments

Comments
 (0)