Skip to content

v5.0.0

Latest

Choose a tag to compare

@Titlehhhh Titlehhhh released this 30 Sep 16:06
9b4391f

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:

  • ProxyClientFactory and its Instance. The same Create methods are now static on Proxy. Their parameters are now proxyLink and credentials (before: link and networkCredential).
  • IProxyClient.ProxyUri and ProxyClient.ProxyUri. A Uri cannot hold most vmess:// links. For vless, trojan and vmess it kept only scheme://host:port. For the classic proxies it held the password, so anything that logged it logged the password.
  • Proxy.ConnectAsync(Uri, ...): all three overloads.
  • ProxyUriExtensions with both ConnectThroughProxyAsync overloads.
  • IProxyClient.ReadTimeout and WriteTimeout, with their ProxyClient implementations. The library copied them to Socket.ReceiveTimeout and SendTimeout, 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.Shadowsocks
  • ProxyErrorCode.TlsHandshakeFailed

Changed behavior:

  • A failed SslStream handshake now gives ProxyProtocolException with TlsHandshakeFailed. This covers the HTTPS proxy, Trojan, and VLESS or VMess with security=tls. Before, an expired certificate or a refused SNI escaped as a raw AuthenticationException. A REALITY failure is still RealityHandshakeException.
  • A cancellation from your own CancellationToken is now OperationCanceledException in every phase. Before, the TCP connect reported it as ProxyProtocolException with ConnectionFailed. A timeout is still ProxyErrorCode.Timeout.
  • HttpsProxyClient sends 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@host sends the password p@ss.
  • A target host with a space or an ASCII control character gives ArgumentException. So does such a proxy host in a constructor or in Proxy.Create(type, ...). A link with such a host gives FormatException.
  • 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 gives ArgumentException.
  • The library refuses a control character in a ws or httpupgrade path or Host value. A link gives FormatException, and a client constructor gives ArgumentException. A space in the path now goes out as %20.
  • The library refuses a REALITY configuration with an undecodable pbk or sid, a server name over 253 characters, more than 16 ALPN protocols, or an ALPN protocol that is empty or over 255 bytes. A link gives FormatException, and the VlessClient constructor gives ArgumentException. Before, these failed at connect.
  • On options that you build yourself, an empty Sni or HostHeader now counts as absent. The TLS or REALITY server name falls through to the next value.
  • socks4://, socks4a:// and socks5:// links without a host give FormatException, the same as http://.
  • 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.
  • ProxyHost is an IPv6 address without brackets, however the host arrived.
  • A read on a disposed tunnel stream can throw ObjectDisposedException synchronously instead of from the returned ValueTask.

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 are aes-128-gcm, aes-192-gcm, aes-256-gcm and chacha20-ietf-poly1305. Both link grammars parse: the legacy base64 blob and SIP002. The library refuses every other cipher and every plugin= by name, before it writes a byte. chacha20-ietf-poly1305 needs OS support: Windows 11 or Server 2022, not Windows 10.
  • The connect target as an EndPoint: IProxyClient.ConnectAsync(EndPoint, ...), IProxyClient.ConnectAsync(Stream, EndPoint, ...) and Proxy.ConnectAsync(string, EndPoint, ...). This is the shape that SocketsHttpHandler.ConnectCallback gives, so an HttpClient goes through any supported proxy in one line. On IProxyClient they are default interface members, so your own implementations get them without a change.
  • Proxy.TryCreate(link, out client) and Proxy.TryCreate(link, out client, out error). They never throw, whatever the input. The error string 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 that scheme://host:port loses. It is a default interface member.
  • ProxyClient.ToString() gives scheme://host:port without 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 raw OverflowException.
  • A failed bind or handle exhaustion during socket creation gives ProxyProtocolException, not a raw SocketException.
  • 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 ws path, host= or sni= could inject headers toward the node. The library now refuses all of these.
  • A password with @, #, / or ? no longer makes a client constructor or Proxy.Create(type, ...) throw UriFormatException.
  • 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 #remark now 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 SslStream and 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

  • grpc and xhttp transports, 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 ws or TLS transport for Shadowsocks.

Full Changelog: v4.0.0...v5.0.0