Skip to content

Commit 32d1a25

Browse files
SOCKS4/4a/5 proxy support in SocketsHttpHandler (#48883)
* Separate method for proxy scheme validation. * Pass socks connection kind. * Unauthorized socks5 connection. * Username and password auth. * Fix response address. * Fix proxyUri value and assertion. * Use HttpConnectionKind for SOCKS. * Handle more connection kind assertions. * SOCKS4/4a support. * Move version selection into SocksHelper. * Call sync version of read write. * Cancellation by disposing stream. * Dispose cancellation registration. * IP addressing for SOCKS5. * IP addressing for SOCKS4. * Wrap write method. * Cancellation and optimization. * Optimize. * Apply suggestions from code review Co-authored-by: Miha Zupan <[email protected]> * Clarify logic. * Remove ssl assertion. * SocksException. * Make SocksException derive from IOException. * Use binary primitives to write port in BE. * Socks loopback test. * Expand test matrix. * Try to solve certificate issue. * Pass handler to httpclient. * Update ConnectToTcpHostAsync. * Remove custom self-signed cert use from Socks test * Fix LoopbackSocksServer's parsing of Socks4a domain name * Only set RequestVersionExact for H2C Setting it in general breaks H2 => H1.1 downgrade on platforms without ALPN * Add auth test. * Add IP in test matrix. * Only override host when required. * Don't attempt NT Auth for Socks proxies * Skip HTTP2 ssl test on platforms without ALPN support * Use NetworkCredential directly * Pass AddressFamily to sync Dns resolution too * Consistently check encoded string lengths * Fix Socks5 user/pass auth * Add IPv6 test for socks5 * Exception nits * Add exceptional tests. * Fix exceptional test. * Fix NRT compilation Co-authored-by: Miha Zupan <[email protected]> * Server shouldn't wait for request in exceptional test. * Add exception message to test. * Update auth failure handling. * SOCKS4 and 5 uses different auth model, requires different error message. * Revert accidental indent change. * Expand test matrix to include Sync HTTP1 * Read received bytes before returning error response in Socks4 loopback * Use named bool arguments * Improve exception messages * !IsEmpty => Length != 0 * Improve exception messages 2 * Avoid enforing Socks4 VN value Co-authored-by: Miha Zupan <[email protected]>
1 parent 1132baa commit 32d1a25

File tree

11 files changed

+971
-12
lines changed

11 files changed

+971
-12
lines changed

src/libraries/System.Net.Http/src/Resources/Strings.resx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@
406406
<value>Client certificate was not found in the personal (\"MY\") certificate store. In UWP, client certificates are only supported if they have been added to that certificate store.</value>
407407
</data>
408408
<data name="net_http_invalid_proxy_scheme" xml:space="preserve">
409-
<value>Only the 'http' scheme is allowed for proxies.</value>
409+
<value>Only the 'http', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies.</value>
410410
</data>
411411
<data name="net_http_request_invalid_char_encoding" xml:space="preserve">
412412
<value>Request headers must contain only ASCII characters.</value>
@@ -606,6 +606,33 @@
606606
<data name="net_http_synchronous_reads_not_supported" xml:space="preserve">
607607
<value>Synchronous reads are not supported, use ReadAsync instead.</value>
608608
</data>
609+
<data name="net_socks_auth_failed" xml:space="preserve">
610+
<value>Failed to authenticate with the SOCKS server.</value>
611+
</data>
612+
<data name="net_socks_bad_address_type" xml:space="preserve">
613+
<value>SOCKS server returned an unknown address type.</value>
614+
</data>
615+
<data name="net_socks_connection_failed" xml:space="preserve">
616+
<value>SOCKS server failed to connect to the destination.</value>
617+
</data>
618+
<data name="net_socks_ipv6_notsupported" xml:space="preserve">
619+
<value>SOCKS4 does not support IPv6 addresses.</value>
620+
</data>
621+
<data name="net_socks_no_auth_method" xml:space="preserve">
622+
<value>SOCKS server did not return a suitable authentication method.</value>
623+
</data>
624+
<data name="net_socks_no_ipv4_address" xml:space="preserve">
625+
<value>Failed to resolve the destination host to an IPv4 address.</value>
626+
</data>
627+
<data name="net_socks_unexpected_version" xml:space="preserve">
628+
<value>Unexpected SOCKS protocol version. Required {0}, got {1}.</value>
629+
</data>
630+
<data name="net_socks_string_too_long" xml:space="preserve">
631+
<value>Encoding the {0} took more than the maximum of 255 bytes.</value>
632+
</data>
633+
<data name="net_socks_auth_required" xml:space="preserve">
634+
<value>SOCKS server requested username &amp; password authentication.</value>
635+
</data>
609636
<data name="net_http_proxy_tunnel_returned_failure_status_code" xml:space="preserve">
610637
<value>The proxy tunnel request to proxy '{0}' failed with status code '{1}'."</value>
611638
</data>

src/libraries/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@
179179
<Compile Include="System\Net\Http\HttpTelemetry.AnyOS.cs" />
180180
<Compile Include="System\Net\Http\HttpUtilities.AnyOS.cs" />
181181
<Compile Include="System\Net\Http\SocketsHttpHandler\SystemProxyInfo.cs" />
182+
<Compile Include="System\Net\Http\SocketsHttpHandler\SocksHelper.cs" />
183+
<Compile Include="System\Net\Http\SocketsHttpHandler\SocksException.cs" />
182184
<Compile Include="$(CommonPath)System\Net\NTAuthentication.Common.cs"
183185
Link="Common\System\Net\NTAuthentication.Common.cs" />
184186
<Compile Include="$(CommonPath)System\Net\ContextFlagsPal.cs"

src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ internal static bool IsNonSecureWebSocketScheme(string scheme) =>
3434
internal static bool IsSecureWebSocketScheme(string scheme) =>
3535
string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase);
3636

37+
internal static bool IsSupportedProxyScheme(string scheme) =>
38+
string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme);
39+
40+
internal static bool IsSocksScheme(string scheme) =>
41+
string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) ||
42+
string.Equals(scheme, "socks4a", StringComparison.OrdinalIgnoreCase) ||
43+
string.Equals(scheme, "socks4", StringComparison.OrdinalIgnoreCase);
44+
3745
// Always specify TaskScheduler.Default to prevent us from using a user defined TaskScheduler.Current.
3846
//
3947
// Since we're not doing any CPU and/or I/O intensive operations, continue on the same thread.

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionKind.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ internal enum HttpConnectionKind : byte
1010
Proxy, // HTTP proxy usage for non-secure (HTTP) requests.
1111
ProxyTunnel, // Non-secure websocket (WS) connection using CONNECT tunneling through proxy.
1212
SslProxyTunnel, // HTTP proxy usage for secure (HTTPS/WSS) requests using SSL and proxy CONNECT.
13-
ProxyConnect // Connection used for proxy CONNECT. Tunnel will be established on top of this.
13+
ProxyConnect, // Connection used for proxy CONNECT. Tunnel will be established on top of this.
14+
SocksTunnel, // SOCKS proxy usage for HTTP requests.
15+
SslSocksTunnel // SOCKS proxy usage for HTTPS requests.
1416
}
1517
}

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,17 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK
190190
_http3Enabled = false;
191191
break;
192192

193+
case HttpConnectionKind.SocksTunnel:
194+
case HttpConnectionKind.SslSocksTunnel:
195+
Debug.Assert(host != null);
196+
Debug.Assert(port != 0);
197+
Debug.Assert(proxyUri != null);
198+
199+
_http3Enabled = false; // TODO: SOCKS supports UDP and may be used for HTTP3
200+
break;
201+
193202
default:
194-
Debug.Fail("Unkown HttpConnectionKind in HttpConnectionPool.ctor");
203+
Debug.Fail("Unknown HttpConnectionKind in HttpConnectionPool.ctor");
195204
break;
196205
}
197206

@@ -317,7 +326,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection
317326
public HttpAuthority? OriginAuthority => _originAuthority;
318327
public HttpConnectionSettings Settings => _poolManager.Settings;
319328
public HttpConnectionKind Kind => _kind;
320-
public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel;
329+
public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.SslSocksTunnel;
321330
public Uri? ProxyUri => _proxyUri;
322331
public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials;
323332
public byte[]? HostHeaderValueBytes => _hostHeaderValueBytes;
@@ -339,10 +348,10 @@ public byte[] Http2AltSvcOriginUri
339348

340349
Debug.Assert(_originAuthority != null);
341350
sb
342-
.Append(_kind == HttpConnectionKind.Https ? "https://" : "http://")
351+
.Append(IsSecure ? "https://" : "http://")
343352
.Append(_originAuthority.IdnHost);
344353

345-
if (_originAuthority.Port != (_kind == HttpConnectionKind.Https ? DefaultHttpsPort : DefaultHttpPort))
354+
if (_originAuthority.Port != (IsSecure ? DefaultHttpsPort : DefaultHttpPort))
346355
{
347356
sb
348357
.Append(':')
@@ -547,7 +556,7 @@ private async ValueTask<HttpConnectionBase> GetHttp11ConnectionAsync(HttpRequest
547556

548557
private async ValueTask<HttpConnectionBase> GetHttp2ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
549558
{
550-
Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.Http);
559+
Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.Http || _kind == HttpConnectionKind.SocksTunnel || _kind == HttpConnectionKind.SslSocksTunnel);
551560

552561
// See if we have an HTTP2 connection
553562
Http2Connection? http2Connection = GetExistingHttp2Connection();
@@ -603,7 +612,7 @@ private async ValueTask<HttpConnectionBase> GetHttp2ConnectionAsync(HttpRequestM
603612

604613
sslStream = stream as SslStream;
605614

606-
if (_kind == HttpConnectionKind.Http)
615+
if (!IsSecure)
607616
{
608617
http2Connection = await ConstructHttp2ConnectionAsync(stream, request, cancellationToken).ConfigureAwait(false);
609618

@@ -1148,7 +1157,7 @@ internal void BlocklistAuthority(HttpAuthority badAuthority)
11481157
Debug.Assert(_altSvcBlocklistTimerCancellation != null);
11491158
if (added)
11501159
{
1151-
_ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds)
1160+
_ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds)
11521161
.ContinueWith(t =>
11531162
{
11541163
lock (altSvcBlocklist)
@@ -1264,6 +1273,14 @@ public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool
12641273
case HttpConnectionKind.SslProxyTunnel:
12651274
stream = await EstablishProxyTunnelAsync(async, request.HasHeaders ? request.Headers : null, cancellationToken).ConfigureAwait(false);
12661275
break;
1276+
1277+
case HttpConnectionKind.SocksTunnel:
1278+
case HttpConnectionKind.SslSocksTunnel:
1279+
Debug.Assert(_originAuthority != null);
1280+
Debug.Assert(_proxyUri != null);
1281+
(socket, stream) = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false);
1282+
await SocksHelper.EstablishSocksTunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, async, cancellationToken).ConfigureAwait(false);
1283+
break;
12671284
}
12681285

12691286
Debug.Assert(stream != null);

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,20 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox
282282

283283
if (proxyUri != null)
284284
{
285-
Debug.Assert(HttpUtilities.IsSupportedNonSecureScheme(proxyUri.Scheme));
286-
if (sslHostName == null)
285+
Debug.Assert(HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme));
286+
if (HttpUtilities.IsSocksScheme(proxyUri.Scheme))
287+
{
288+
// Socks proxy
289+
if (sslHostName != null)
290+
{
291+
return new HttpConnectionKey(HttpConnectionKind.SslSocksTunnel, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity);
292+
}
293+
else
294+
{
295+
return new HttpConnectionKey(HttpConnectionKind.SocksTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity);
296+
}
297+
}
298+
else if (sslHostName == null)
287299
{
288300
if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme))
289301
{
@@ -394,7 +406,7 @@ public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool
394406
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception from {_proxy.GetType().Name}.GetProxy({request.RequestUri}): {ex}");
395407
}
396408

397-
if (proxyUri != null && proxyUri.Scheme != UriScheme.Http)
409+
if (proxyUri != null && !HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme))
398410
{
399411
throw new NotSupportedException(SR.net_http_invalid_proxy_scheme);
400412
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
6+
namespace System.Net.Http
7+
{
8+
internal class SocksException : IOException
9+
{
10+
public SocksException(string message) : base(message) { }
11+
12+
public SocksException(string message, Exception innerException) : base(message, innerException) { }
13+
}
14+
}

0 commit comments

Comments
 (0)