VpnHood.Net.Quic.Android 9.1.874

dotnet add package VpnHood.Net.Quic.Android --version 9.1.874
                    
NuGet\Install-Package VpnHood.Net.Quic.Android -Version 9.1.874
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="VpnHood.Net.Quic.Android" Version="9.1.874" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="VpnHood.Net.Quic.Android" Version="9.1.874" />
                    
Directory.Packages.props
<PackageReference Include="VpnHood.Net.Quic.Android" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add VpnHood.Net.Quic.Android --version 9.1.874
                    
#r "nuget: VpnHood.Net.Quic.Android, 9.1.874"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package VpnHood.Net.Quic.Android@9.1.874
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=VpnHood.Net.Quic.Android&version=9.1.874
                    
Install as a Cake Addin
#tool nuget:?package=VpnHood.Net.Quic.Android&version=9.1.874
                    
Install as a Cake Tool

VpnHood.Net.Quic.Android

Android implementation of the VpnHood QUIC abstractions (IQuicClient / IQuicConnection / Stream), backed by MsQuic through our own P/Invoke bindings over a bundled libmsquic.so.

Unlike Windows/Linux (VpnHood.Net.Quic.MsQuic), this project does not use System.Net.Quic. It is conceptually the Android sibling of VpnHood.Net.Quic.Ios (which is its own client over Apple Network.framework): each mobile platform needs a native QUIC client because the BCL's QUIC stack does not work there.


Why not System.Net.Quic on Android?

We tried two simpler approaches first; both are dead ends. The history is worth keeping so nobody repeats them:

  1. Reuse MsQuicClient (System.Net.Quic) + bundle libmsquic.so. QuicConnection.IsSupported does become true once libmsquic.so is present, and the QUIC handshake even starts — but the connection then hard-crashes (SIGSEGV) inside System.Net.Quic's certificate validation:

    #00 libcrypto.so  __aarch64_ldadd4_relax
    #01 libcrypto.so  X509_up_ref
    #02 libSystem.Security.Cryptography.Native.OpenSsl.so  CryptoNative_X509UpRef
    

    Root cause: System.Net.Quic validates the peer cert with the OpenSSL crypto backend (X509_up_ref, CryptoNative_*), but on Android X509Certificate2/X509Chain use the Android crypto backend (libSystem.Security.Cryptography.Native.Android). The cert handle it hands to OpenSSL is an Android handle, not an X509* — so it dereferences garbage. This is a fundamental backend split, not fixable by shipping OpenSSL libs (the cert object simply isn't an OpenSSL object). This is exactly why Microsoft lists System.Net.Quic as unsupported on Android.

  2. Bundle the OpenSSL crypto shim + libcrypto/libssl for Android. Removes the DllNotFoundException but not the crash above — same backend split. Abandoned.

The working approach (this project): drive libmsquic directly via P/Invoke and do certificate validation ourselves (msquic performs the TLS handshake with its own statically-linked OpenSSL; we never touch OpenSSL from managed code).


Architecture

Type Role
Interop/MsQuicApi Opens the native QUIC_API_TABLE + one registration once per process (lazy, on the bundled libmsquic.so). IsSupported probes this.
AndroidQuicClient : IQuicClient Opens a configuration (ALPN h3, client credential, the number of streams the server may open) + connection, starts it, awaits handshake, returns a connection.
AndroidQuicConnection : IQuicConnection Owns the connection handle; routes the unmanaged connection callback; opens outbound / accepts inbound streams; performs certificate validation.
AndroidQuicStream : Stream An async Stream over one msquic stream: RECEIVE → a Pipe that ReadAsync drains, holding at most 64 KB: past that the stream's receive events stop until the reader has made room, so QUIC's flow control stops the sender; WriteAsync → StreamSend awaited until SEND_COMPLETE (backpressure). A disposed stream or connection throws ObjectDisposedException.

This project is pure managed code. The native libmsquic.so and the MsQuic C# P/Invoke bindings (Microsoft.Quic namespace) both come from the self-contained package VpnHood.Net.Quic.MsQuic.AndroidNative, which this project references as a NuGet package pinned to one version. That package exposes the Microsoft.Quic binding types as public API, so all native-linking complexity is hidden behind one reference. Unmanaged callbacks here are [UnmanagedCallersOnly(CallConvs=[CallConvCdecl])] statics that resolve the managed instance from a GCHandle passed as the callback context.

Certificate validation (the important part)

The client credential uses:

CLIENT | INDICATE_CERTIFICATE_RECEIVED | DEFER_CERTIFICATE_VALIDATION | USE_PORTABLE_CERTIFICATES
  • INDICATE_CERTIFICATE_RECEIVED — raises PEER_CERTIFICATE_RECEIVED so we can inspect the cert.
  • DEFER_CERTIFICATE_VALIDATION — required. Without it msquic runs its own validation and rejects the (untrusted-on-Android) cert with TLS alert UNKNOWN_CA before our callback runs. With it, our callback's return value decides.
  • USE_PORTABLE_CERTIFICATES — the peer cert + chain arrive as DER (QUIC_BUFFERs), not native handles.

In PEER_CERTIFICATE_RECEIVED we then work out what SslStream would hand its callback, using the Android crypto backend (no OpenSSL):

  1. Build X509Certificate2 from the leaf DER.
  2. Add the intermediates from the event's Chain buffer to X509Chain.ChainPolicy.ExtraStore (otherwise the chain never builds → false RemoteCertificateChainErrors).
  3. Compute SslPolicyErrors: chain.Build(cert) → RemoteCertificateChainErrors; cert.MatchesHostname(TargetHost) → RemoteCertificateNameMismatch.
  4. Call the caller's CertificateValidationCallback, whose answer decides; the library pins nothing itself (VpnHood's client accepts errors == None, or its server's pinned hash). Return QUIC_STATUS_SUCCESS / QUIC_STATUS_BAD_CERTIFICATE.

SNI / connect target

MsQuic.ConnectionStart's ServerName is used as both the connect target and the TLS SNI. We must send the TargetHost as SNI (so the server returns the cert for that name) while connecting to the exact resolved IP. So we set QUIC_PARAM_CONN_REMOTE_ADDRESS to RemoteEndPoint and pass TargetHost as ServerName. (Sending the IP as SNI made the server return its default cert, whose name did not match TargetHost → validation failed.)


Native dependency & build

This project bundles no native libraries itself. Everything native comes from the referenced VpnHood.Net.Quic.MsQuic.AndroidNative package, which ships the prebuilt libmsquic.so per ABI (committed in that package's native/ folder) plus the Microsoft.Quic bindings. msquic statically links its own OpenSSL, so no libcrypto/libssl/.NET-OpenSSL-shim is needed.

  • That package lives in the sibling repo VpnHood.Net.Quic.MsQuic.AndroidNative (a msquic+OpenSSL fork) at android/AndroidNative/. See its android/DEV-GUIDE.md for how the .so is produced (build-android.ps1); the package commits the binary so consumers never build it.
  • The <AndroidNativeLibrary> items in that package flow transitively into any consuming APK (lib/<abi>/libmsquic.so), through this project and on to the app.
  • arm64-v8a, armeabi-v7a and x86_64 are produced (armeabi-v7a since package 8.0.7 — many Android TVs run a 32-bit userland), covering every ABI the apps publish. Should the lib ever be missing for an ABI, IsSupported is false there and CreateClient() throws NotSupportedException; VpnHood's client checks IsSupported first and uses TCP instead.

Wiring: AndroidQuicClientFactory (which AndroidVpnService hands the client in its ClientSocketFactory) exposes IsSupported / CreateClient() → AndroidQuicClient. QUIC engages only when the client's ChannelProtocol is Quic and the server advertises a QuicPort (otherwise TCP).


Testing (without the VPN)

A standalone test harness lives in VpnHood.Net.Quic.MsQuic.AndroidNative/android/:

  • quic-test-android/ — minimal APK that runs AndroidQuicClient against a VpnHood.NetTester QUIC echo server.
  • quic-test/ — desktop runner of the same logic (sanity check on Windows).

Run the echo server: nettester /ep <ip>:4040 /server. The client POSTs a ServerConfig (QuicPort, Domain, …) to http://<ip>:4040/config; the per-stream protocol is upSize(Int64) + downSize(Int64) + up bytes, then read down bytes.

To adb install a Debug APK directly, build it self-contained (-p:EmbedAssembliesIntoApk=true) — otherwise Fast Deployment leaves the assemblies off-device and the app aborts at startup (No assemblies found … Fast Deployment).

Product Compatible and additional computed target framework versions.
.NET net11.0-android37.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on VpnHood.Net.Quic.Android:

Package Downloads
VpnHood.Core.Client.Devices.Android

VpnHood client device provider for Android: the app's side, and what its VPN service shares

VpnHood.Core.Client.VpnServices.Host.Android

VpnHood's VPN service on Android: the VPN service's half, which runs the engine

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
9.1.874 0 11/15/2026
9.1.864 101 11/3/2026
9.0.860 109 10/31/2026
9.0.859.16-prerelease 99 10/30/2026
9.0.857 103 10/20/2026
9.0.856 94 10/19/2026
9.0.855 104 10/13/2026
8.2.853.11-prerelease 106 10/5/2026
8.2.853 137 9/23/2026
8.2.852 121 9/23/2026