1
+ /* Copyright 2010-present MongoDB Inc.
2
+ *
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+
16
+ using System ;
17
+ using System . Buffers ;
18
+ using System . Buffers . Binary ;
19
+ using System . IO ;
20
+ using System . Net ;
21
+ using System . Net . Sockets ;
22
+ using System . Text ;
23
+ using System . Threading ;
24
+ using MongoDB . Driver . GridFS ;
25
+
26
+ namespace MongoDB . Driver . Core . Connections
27
+ {
28
+ internal static class Socks5Helper
29
+ {
30
+ // Schemas for requests/responses are taken from the following RFCs:
31
+ // SOCKS Protocol Version 5 - https://datatracker.ietf.org/doc/html/rfc1928
32
+ // Username/Password Authentication for SOCKS V5 - https://datatracker.ietf.org/doc/html/rfc1929
33
+
34
+ private const byte ProtocolVersion5 = 0x05 ;
35
+ private const byte SubnegotiationVersion = 0x01 ;
36
+ private const byte CmdConnect = 0x01 ;
37
+ private const byte MethodNoAuth = 0x00 ;
38
+ private const byte MethodUsernamePassword = 0x02 ;
39
+ private const byte AddressTypeIPv4 = 0x01 ;
40
+ private const byte AddressTypeDomain = 0x03 ;
41
+ private const byte AddressTypeIPv6 = 0x04 ;
42
+ private const byte Socks5Success = 0x00 ;
43
+
44
+ private const int BufferSize = 512 ;
45
+
46
+ public static void PerformSocks5Handshake ( Stream stream , string targetHost , int targetPort , string proxyUsername , string proxyPassword , CancellationToken cancellationToken )
47
+ {
48
+ var buffer = ArrayPool < byte > . Shared . Rent ( BufferSize ) ;
49
+ try
50
+ {
51
+ var useAuth = ! string . IsNullOrEmpty ( proxyUsername ) && ! string . IsNullOrEmpty ( proxyPassword ) ;
52
+
53
+ // Greeting request
54
+ // +----+----------+----------+
55
+ // |VER | NMETHODS | METHODS |
56
+ // +----+----------+----------+
57
+ // | 1 | 1 | 1 to 255 |
58
+ // +----+----------+----------+
59
+ buffer [ 0 ] = ProtocolVersion5 ;
60
+
61
+ if ( ! useAuth )
62
+ {
63
+ buffer [ 1 ] = 1 ;
64
+ buffer [ 2 ] = MethodNoAuth ;
65
+ }
66
+ else
67
+ {
68
+ buffer [ 1 ] = 2 ;
69
+ buffer [ 2 ] = MethodNoAuth ;
70
+ buffer [ 3 ] = MethodUsernamePassword ;
71
+ }
72
+
73
+ stream . Write ( buffer , 0 , useAuth ? 4 : 3 ) ;
74
+ stream . Flush ( ) ;
75
+
76
+ // Greeting response
77
+ // +----+--------+
78
+ // |VER | METHOD |
79
+ // +----+--------+
80
+ // | 1 | 1 |
81
+ // +----+--------+
82
+
83
+ stream . ReadBytes ( buffer , 0 , 2 , cancellationToken ) ;
84
+
85
+ VerifyProtocolVersion ( buffer [ 0 ] ) ;
86
+
87
+ var method = buffer [ 1 ] ;
88
+ if ( method == MethodUsernamePassword )
89
+ {
90
+ if ( ! useAuth )
91
+ {
92
+ //We should not reach here
93
+ throw new IOException ( "SOCKS5 proxy requires authentication, but no credentials were provided." ) ;
94
+ }
95
+
96
+ // Authentication request
97
+ // +----+------+----------+------+----------+
98
+ // |VER | ULEN | UNAME | PLEN | PASSWD |
99
+ // +----+------+----------+------+----------+
100
+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
101
+ // +----+------+----------+------+----------+
102
+ buffer [ 0 ] = SubnegotiationVersion ;
103
+ var usernameLength = EncodeString ( proxyUsername , buffer . AsSpan ( 2 ) , nameof ( proxyUsername ) ) ;
104
+ buffer [ 1 ] = usernameLength ;
105
+ var passwordLength = EncodeString ( proxyPassword , buffer . AsSpan ( 3 + usernameLength ) , nameof ( proxyPassword ) ) ;
106
+ buffer [ 2 + usernameLength ] = passwordLength ;
107
+
108
+ var authLength = 3 + usernameLength + passwordLength ;
109
+ stream . Write ( buffer , 0 , authLength ) ;
110
+ stream . Flush ( ) ;
111
+
112
+ // Authentication response
113
+ // +----+--------+
114
+ // |VER | STATUS |
115
+ // +----+--------+
116
+ // | 1 | 1 |
117
+ // +----+--------+
118
+ stream . ReadBytes ( buffer , 0 , 2 , cancellationToken ) ;
119
+ if ( buffer [ 0 ] != SubnegotiationVersion || buffer [ 1 ] != Socks5Success )
120
+ {
121
+ throw new IOException ( "SOCKS5 authentication failed." ) ;
122
+ }
123
+ }
124
+ else if ( method != MethodNoAuth )
125
+ {
126
+ throw new IOException ( "SOCKS5 proxy requires unsupported authentication method." ) ;
127
+ }
128
+
129
+ // Connect request
130
+ // +----+-----+-------+------+----------+----------+
131
+ // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
132
+ // +----+-----+-------+------+----------+----------+
133
+ // | 1 | 1 | X'00' | 1 | Variable | 2 |
134
+ // +----+-----+-------+------+----------+----------+
135
+ buffer [ 0 ] = ProtocolVersion5 ;
136
+ buffer [ 1 ] = CmdConnect ;
137
+ buffer [ 2 ] = 0x00 ;
138
+ var addressLength = 0 ;
139
+
140
+ if ( IPAddress . TryParse ( targetHost , out var ip ) )
141
+ {
142
+ switch ( ip . AddressFamily )
143
+ {
144
+ case AddressFamily . InterNetwork :
145
+ buffer [ 3 ] = AddressTypeIPv4 ;
146
+ ip . TryWriteBytes ( buffer . AsSpan ( 4 ) , out _ ) ;
147
+ addressLength = 4 ;
148
+ break ;
149
+ case AddressFamily . InterNetworkV6 :
150
+ buffer [ 3 ] = AddressTypeIPv6 ;
151
+ ip . TryWriteBytes ( buffer . AsSpan ( 4 ) , out _ ) ;
152
+ addressLength = 16 ;
153
+ break ;
154
+ default :
155
+ throw new IOException ( "Invalid target host address family. Only IPv4 and IPv6 are supported." ) ;
156
+ }
157
+ }
158
+ else
159
+ {
160
+ buffer [ 3 ] = AddressTypeDomain ;
161
+ var hostLength = EncodeString ( targetHost , buffer . AsSpan ( 5 ) , nameof ( targetHost ) ) ;
162
+ buffer [ 4 ] = hostLength ;
163
+ addressLength = hostLength + 1 ;
164
+ }
165
+
166
+ BinaryPrimitives . WriteUInt16BigEndian ( buffer . AsSpan ( addressLength + 4 ) , ( ushort ) targetPort ) ;
167
+
168
+ stream . Write ( buffer , 0 , addressLength + 6 ) ;
169
+ stream . Flush ( ) ;
170
+
171
+ // Connect response
172
+ // +----+-----+-------+------+----------+----------+
173
+ // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT |
174
+ // +----+-----+-------+------+----------+----------+
175
+ // | 1 | 1 | X'00' | 1 | Variable | 2 |
176
+ // +----+-----+-------+------+----------+----------+
177
+ stream . ReadBytes ( buffer , 0 , 5 , cancellationToken ) ;
178
+ VerifyProtocolVersion ( buffer [ 0 ] ) ;
179
+ if ( buffer [ 1 ] != Socks5Success )
180
+ {
181
+ throw new IOException ( $ "SOCKS5 connect failed with code 0x{ buffer [ 1 ] : X2} ") ;
182
+ }
183
+
184
+ var skip = buffer [ 3 ] switch
185
+ {
186
+ AddressTypeIPv4 => 4 + 2 ,
187
+ AddressTypeIPv6 => 16 + 2 ,
188
+ AddressTypeDomain => buffer [ 4 ] + 1 + 2 ,
189
+ _ => throw new IOException ( "Unknown address type in SOCKS5 reply." )
190
+ } ;
191
+
192
+ stream . ReadBytes ( buffer , 0 , skip , cancellationToken ) ;
193
+ // Address and port in response are ignored
194
+ }
195
+ finally
196
+ {
197
+ ArrayPool < byte > . Shared . Return ( buffer ) ;
198
+ }
199
+ }
200
+
201
+ private static void VerifyProtocolVersion ( byte version )
202
+ {
203
+ if ( version != ProtocolVersion5 )
204
+ {
205
+ throw new IOException ( "Invalid SOCKS version in method selection response." ) ;
206
+ }
207
+ }
208
+
209
+ private static byte EncodeString ( ReadOnlySpan < char > chars , Span < byte > buffer , string parameterName )
210
+ {
211
+ try
212
+ {
213
+ return checked ( ( byte ) Encoding . UTF8 . GetBytes ( chars , buffer ) ) ;
214
+ }
215
+ catch
216
+ {
217
+ throw new IOException ( $ "The { parameterName } could not be encoded as UTF-8.") ;
218
+ }
219
+ }
220
+ }
221
+ }
0 commit comments