This release removes Uri and the read and write timeouts from the public API, and adds Shadowsocks. It also closes two holes in the exception contract of ConnectAsync. A failed TLS handshake and a failed socket creation escaped as raw exceptions instead of ProxyProtocolException. A checker found both while it ran the public API over thousands of real nodes.
Breaking changes
Removed from the public API:
ProxyClientFactoryand itsInstance. The sameCreatemethods are now static onProxy. Their parameters are nowproxyLinkandcredentials(before:linkandnetworkCredential).IProxyClient.ProxyUriandProxyClient.ProxyUri. AUricannot hold mostvmess://links. Forvless,trojanandvmessit kept onlyscheme://host:port. For the classic proxies it held the password, so anything that logged it logged the password.Proxy.ConnectAsync(Uri, ...): all three overloads.ProxyUriExtensionswith bothConnectThroughProxyAsyncoverloads.IProxyClient.ReadTimeoutandWriteTimeout, with theirProxyClientimplementations. The library copied them toSocket.ReceiveTimeoutandSendTimeout, which apply only to synchronous calls. Every handshake is asynchronous, so they never bounded a connect.
New enum members. An exhaustive switch needs a default arm:
ProxyType.ShadowsocksProxyErrorCode.TlsHandshakeFailed
Changed behavior:
- A failed
SslStreamhandshake now givesProxyProtocolExceptionwithTlsHandshakeFailed. This covers the HTTPS proxy, Trojan, and VLESS or VMess withsecurity=tls. Before, an expired certificate or a refused SNI escaped as a rawAuthenticationException. A REALITY failure is stillRealityHandshakeException. - A cancellation from your own
CancellationTokenis nowOperationCanceledExceptionin every phase. Before, the TCP connect reported it asProxyProtocolExceptionwithConnectionFailed. A timeout is stillProxyErrorCode.Timeout. HttpsProxyClientsends the proxy name as SNI and checks the proxy certificate against it. Before, it used the CONNECT target, so every HTTPS proxy failed the default check unless its certificate named the target.- Percent-escaped credentials in classic proxy links (
http,https,socks4,socks4a,socks5) are now decoded. For example,socks5://user:p%40ss@hostsends the passwordp@ss. - A target host with a space or an ASCII control character gives
ArgumentException. So does such a proxy host in a constructor or inProxy.Create(type, ...). A link with such a host givesFormatException. - A SOCKS4 or SOCKS4a user id with a NUL gives
ArgumentException, from a constructor and from a link. ConnectAsync(Stream, host, port)now checks its arguments like the other overloads. A null host or a port outside 1-65535 givesArgumentException.- The library refuses a control character in a
wsorhttpupgradepath or Host value. A link givesFormatException, and a client constructor givesArgumentException. A space in the path now goes out as%20. - The library refuses a REALITY configuration with an undecodable
pbkorsid, a server name over 253 characters, more than 16 ALPN protocols, or an ALPN protocol that is empty or over 255 bytes. A link givesFormatException, and theVlessClientconstructor givesArgumentException. Before, these failed at connect. - On options that you build yourself, an empty
SniorHostHeadernow counts as absent. The TLS or REALITY server name falls through to the next value. socks4://,socks4a://andsocks5://links without a host giveFormatException, the same ashttp://.- The socket is dual-mode unless you set
LocalEndPoint. A proxy name with both IPv4 and IPv6 addresses gets both tries, in the order that the resolver returns. ProxyHostis an IPv6 address without brackets, however the host arrived.- A read on a disposed tunnel stream can throw
ObjectDisposedExceptionsynchronously instead of from the returnedValueTask.
Migration
| 4.x | 5.0 |
|---|---|
ProxyClientFactory.Instance.Create(link) |
Proxy.Create(link) |
ProxyClientFactory.Instance.Create(type, host, port, credentials) |
Proxy.Create(type, host, port, credentials) |
Proxy.ConnectAsync(uri, host, port, ...) |
Proxy.ConnectAsync(uri.OriginalString, host, port, ...) |
Proxy.ConnectAsync(uri, stream, host, port) |
Proxy.Create(uri).ConnectAsync(stream, host, port) |
uri.ConnectThroughProxyAsync(host, port, ...) |
Proxy.Create(uri).ConnectAsync(host, port, ...) |
client.ProxyUri |
client.ToString() for logs, ProxyHost and ProxyPort for the address, or client.SourceLink for the full link |
client.ReadTimeout / client.WriteTimeout |
Pass a CancellationToken to reads and writes, or set ReadTimeout / WriteTimeout on the returned stream when its CanTimeout is true |
catch (AuthenticationException) around ConnectAsync |
catch (ProxyProtocolException ex) when (ex.ErrorCode == ProxyErrorCode.TlsHandshakeFailed) |
Proxy.Create(Uri) stays. Use it with WebProxy.Address or IWebProxy.GetProxy.
SourceLink is set only when Proxy.Create(string) or Proxy.TryCreate made the client. A client from a constructor or from FromShareLink has null there.
New
- Shadowsocks AEAD over TCP:
ShadowsocksClient,ShadowsocksOptions,ShadowsocksShareLink. The supported ciphers areaes-128-gcm,aes-192-gcm,aes-256-gcmandchacha20-ietf-poly1305. Both link grammars parse: the legacy base64 blob and SIP002. The library refuses every other cipher and everyplugin=by name, before it writes a byte.chacha20-ietf-poly1305needs OS support: Windows 11 or Server 2022, not Windows 10. - The connect target as an
EndPoint:IProxyClient.ConnectAsync(EndPoint, ...),IProxyClient.ConnectAsync(Stream, EndPoint, ...)andProxy.ConnectAsync(string, EndPoint, ...). This is the shape thatSocketsHttpHandler.ConnectCallbackgives, so anHttpClientgoes through any supported proxy in one line. OnIProxyClientthey are default interface members, so your own implementations get them without a change. Proxy.TryCreate(link, out client)andProxy.TryCreate(link, out client, out error). They never throw, whatever the input. Theerrorstring starts with the exception type, so you can group the refusals of a large subscription.IProxyClient.SourceLink: the link text that made the client. It keeps the uuid, sni and transport thatscheme://host:portloses. It is a default interface member.ProxyClient.ToString()givesscheme://host:portwithout credentials.- The package declares itself trimmable and AOT-compatible. It builds with no IL warnings on all four targets.
Fixed
- The library now reaches a proxy at an IPv6 address. Before, it reported a healthy IPv6 node as
ConnectionFailed. - HTTP CONNECT puts an IPv6 target in brackets.
- A SOCKS string of 256 to about 1000 bytes gives
SocksStringTooLong, not a rawOverflowException. - A failed bind or handle exhaustion during socket creation gives
ProxyProtocolException, not a rawSocketException. - A timeout that fired while the handshake returned could dispose the socket of a stream already given to the caller. The timeout is now a linked cancellation, and nothing it registers outlives the call.
- A CR LF in a target host could inject headers and a second request into HTTP CONNECT. A NUL could split a SOCKS4a host or user id. A CR LF in a
wspath,host=orsni=could inject headers toward the node. The library now refuses all of these. - A password with
@,#,/or?no longer makes a client constructor orProxy.Create(type, ...)throwUriFormatException. - Buffers that held credentials go back to the shared pool cleared. So do the pooled arrays that the HTTP CONNECT, WebSocket and REALITY code used for tunnel bytes.
- A
vmess://link with@in the#remarknow parses. An empty"ps"no longer hides the#remark. - Share-link base64 decodes the same on .NET 10 and .NET 11.
- REALITY is as strict as the Go client around the Finished message of the server. It refuses application data before the Finished and handshake bytes after it.
- The REALITY client sends ALPN as UTF-8, as
SslStreamand Xray do.
Performance
- Tunnel streams no longer box their async state machines on reads that complete asynchronously.
- The REALITY key schedule computes each HKDF output as its single block, with no allocation per call.
Not in this release
grpcandxhttptransports, Hysteria2 and TUIC (QUIC), and UDP.- Vision's TLS-in-TLS splice. It is a throughput optimization, and the wire format is complete.
- A browser-grade ClientHello fingerprint for REALITY. See
docs/reality-fingerprint-plan.md. - Shadowsocks AEAD-2022, stream ciphers, plugins, and
wsor TLS transport for Shadowsocks.
Full Changelog: v4.0.0...v5.0.0