@@ -23,26 +23,54 @@ import (
23
23
"github.com/Jigsaw-Code/outline-sdk/transport"
24
24
)
25
25
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
+
26
33
// NewStreamDialer creates a [transport.StreamDialer] that routes connections to a SOCKS5
27
34
// proxy listening at the given [transport.StreamEndpoint].
28
- func NewStreamDialer (endpoint transport.StreamEndpoint ) (transport. StreamDialer , error ) {
35
+ func NewStreamDialer (endpoint transport.StreamEndpoint ) (* StreamDialer , error ) {
29
36
if endpoint == nil {
30
37
return nil , errors .New ("argument endpoint must not be nil" )
31
38
}
32
- return & streamDialer {proxyEndpoint : endpoint }, nil
39
+ return & StreamDialer {proxyEndpoint : endpoint , cred : nil }, nil
33
40
}
34
41
35
- type streamDialer struct {
42
+ type StreamDialer struct {
36
43
proxyEndpoint transport.StreamEndpoint
44
+ cred * credentials
37
45
}
38
46
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
+ }
40
67
41
68
// 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.
43
71
// The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which
44
72
// 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 ) {
46
74
proxyConn , err := c .proxyEndpoint .ConnectStream (ctx )
47
75
if err != nil {
48
76
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
55
83
}()
56
84
57
85
// 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
+ }
65
120
66
121
// 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
+ // +----+-----+-------+------+----------+----------+
68
128
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.
70
130
b , err = appendSOCKS5Address (b , remoteAddr )
71
131
if err != nil {
72
132
return nil , fmt .Errorf ("failed to create SOCKS5 address: %w" , err )
73
133
}
74
134
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.
77
138
_ , err = proxyConn .Write (b )
78
139
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 )
80
141
}
81
142
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 )
85
154
}
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 ])
88
157
}
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 ])
91
183
}
92
184
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).
94
186
// 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 )
97
198
}
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 ])
100
202
}
101
203
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 ])
105
207
}
106
208
107
- toRead := 0
108
- switch header [3 ] {
209
+ // 4. Read address and length
210
+ var bndAddrLen int
211
+ switch buffer [3 ] {
109
212
case addrTypeIPv4 :
110
- toRead = 4
213
+ bndAddrLen = 4
111
214
case addrTypeIPv6 :
112
- toRead = 16
215
+ bndAddrLen = 16
113
216
case addrTypeDomainName :
114
- _ , err := io .ReadFull (proxyConn , header [:1 ])
217
+ // buffer[8]: length of the domain name
218
+ _ , err := io .ReadFull (proxyConn , buffer [:1 ])
115
219
if err != nil {
116
220
return nil , fmt .Errorf ("failed to read address length in connect response: %w" , err )
117
221
}
118
- toRead = int (header [0 ])
222
+ bndAddrLen = int (buffer [0 ])
223
+ default :
224
+ return nil , fmt .Errorf ("invalid address type %v" , buffer [3 ])
119
225
}
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.
121
227
// 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 )
125
230
}
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 )
130
234
}
131
-
132
235
dialSuccess = true
133
236
return proxyConn , nil
134
237
}
0 commit comments