Chameleon.Net.Extensions.Http 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Chameleon.Net.Extensions.Http --version 0.1.0
                    
NuGet\Install-Package Chameleon.Net.Extensions.Http -Version 0.1.0
                    
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="Chameleon.Net.Extensions.Http" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Chameleon.Net.Extensions.Http" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Chameleon.Net.Extensions.Http" />
                    
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 Chameleon.Net.Extensions.Http --version 0.1.0
                    
#r "nuget: Chameleon.Net.Extensions.Http, 0.1.0"
                    
#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 Chameleon.Net.Extensions.Http@0.1.0
                    
#: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=Chameleon.Net.Extensions.Http&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Chameleon.Net.Extensions.Http&version=0.1.0
                    
Install as a Cake Tool

Chameleon.Net

An HttpMessageHandler and a WebSocket connector for .NET whose connections look like a specific real client on the wire: the TLS ClientHello, the HTTP/2 connection preface and frames, and the order, casing and defaults of the HTTP headers. Each client is described by a profile; the built-in ones were captured from the real clients and are checked against them by tests.

Why this exists

Servers and the CDNs in front of them (Cloudflare, Akamai, ...) don't only look at the User-Agent. They also fingerprint how a client talks:

  • TLS: the ClientHello lists cipher suites, extensions, groups and signature algorithms in an order that is specific to each TLS library and version. JA3 and JA4 turn it into a short hash.
  • HTTP/2: the SETTINGS, WINDOW_UPDATE and PRIORITY frames a client opens with, and the order of its pseudo-headers, differ between Chrome, Firefox, OkHttp, curl... (the "Akamai fingerprint").
  • HTTP headers: which headers are sent, in which order, with which casing and default values.

A .NET HttpClient has its own fingerprint on each of these: SChannel on Windows, OpenSSL on Linux, and .NET's HTTP stack. It matches none of the clients an app usually claims to be. If a service expects its Android app (OkHttp) or a browser, a .NET client saying so in its User-Agent stands out at the first packet, and bot protection blocks it or serves it a challenge. This happens even when the traffic is legitimate: testing your own backend through its real edge, a desktop or server companion to a mobile app, research on fingerprinting itself.

.NET gives no way to change any of this: SslStream doesn't let you choose the ClientHello, and SocketsHttpHandler decides its HTTP/2 frames and header order itself. Chameleon.Net replaces those layers so that a connection reproduces a real client's fingerprint on every one of them.

How it works

A request goes through four layers. Each one is driven by the profile:

  1. Transport: a TCP connection, possibly through an HTTP (CONNECT) or SOCKS5 proxy. The proxy doesn't change what the target sees.
  2. TLS, by BouncyCastle rather than the OS:
    • The profile supplies the content (cipher suites, groups, signature algorithms, ALPN...). BouncyCastle runs the handshake and owns the key schedule.
    • The ClientHello bytes are written by Chameleon.Net itself: extensions in the profile's order, GREASE values, Chrome's per-connection extension shuffle, and padding. The handshake transcript is fed those exact bytes, so what goes on the wire is what gets authenticated.
    • Chameleon.Net also fills in what browsers need and BouncyCastle lacks: X25519MLKEM768 post-quantum key shares, compressed certificates (brotli/zlib/zstd), ALPS, GREASE ECH, and TLS 1.3 session resumption with tickets reused like BoringSSL does.
    • BouncyCastle runs in its non-blocking mode: Chameleon.Net reads and writes the socket with async I/O and hands the bytes to it. A connection waiting for data, such as an idle WebSocket, holds no thread.
  3. HTTP: HTTP/1.1 and HTTP/2 (with HPACK) are implemented here, not taken from .NET, so the profile controls the HTTP/2 preface, stream priorities, pseudo-header order, first stream id, header order, casing and defaults per request kind. Connection pooling, redirects, cookies and decompression follow the emulated client (OkHttp's pool semantics for the OkHttp profile).
  4. WebSocket: the upgrade request is written in the profile's header order over the same TLS layer (ALPN http/1.1). The connection is then handed to .NET's own WebSocket for framing.

A profile (ClientProfile) is plain data: a TlsProfile, an Http2Profile, a HeaderProfile and a WebSocketProfile. The built-in ones were captured from the real clients. Their tests replay those captures offline (JA3/JA4, HTTP/2 frames, header lists), and explicit live tests check that an echo service such as tls.peet.ws sees the same fingerprints from Chameleon.Net as from the real client.

Built-in profiles

Profile Client JA4 Akamai HTTP/2
OkHttp4Android13 OkHttp 4.12 on Android 13 (Conscrypt) t13d1513h2_… 4:16777216\|16711681\|0\|m,p,a,s
Chromium152Windows Chromium 152 t13d1516h2_8daaf6152771_806a8c22fdea 1:65536;2:0;4:6291456;6:262144\|15663105\|0\|m,a,s,p
Edge153Windows Microsoft Edge 153 t13d1516h2_8daaf6152771_806a8c22fdea 1:65536;2:0;4:6291456;6:262144\|15663105\|0\|m,a,s,p
Firefox156Windows Firefox 156 t13d1517h2_8daaf6152771_3cbfd9057e0d 1:65536;2:0;4:131072;5:16384\|12517377\|0\|m,p,a,s

What the profiles reproduce, depending on the client: cipher suite and extension order, GREASE (placement and per-connection values), Chrome's extension shuffle, X25519MLKEM768 post-quantum key shares, certificate compression, ALPS, GREASE ECH, record size limit, TLS 1.3 session resumption, HTTP/2 SETTINGS / WINDOW_UPDATE / HEADERS priority / pseudo-header order / first stream id, header order and defaults per request kind (navigation, fetch(), WebSocket), and OkHttp's connection pooling and header handling.

Usage

Requires .NET 10.

using Chameleon.Net;
using Chameleon.Net.Http;
using Chameleon.Net.Profiles;

using var client = new HttpClient(new ChameleonHttpMessageHandler(BuiltInProfiles.OkHttp4Android13));
var body = await client.GetStringAsync("https://example.com/");

Headers you set on the request are kept; what the profile's client would add on its own (User-Agent, Accept-Encoding, ...) is added when missing, in the client's order. Responses are decompressed like the client would (gzip, deflate, br, zstd).

Browser profiles distinguish request kinds, because browsers send different headers and HTTP/2 priorities for each:

using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/");
request.Options.Set(ChameleonRequestOptions.Kind, RequestKind.Navigate); // default: Fetch

WebSockets

using Chameleon.Net.WebSockets;

var connector = new ChameleonWebSocketConnector();
using var socket = await connector.ConnectAsync(new Uri("wss://example.com/ws"), BuiltInProfiles.OkHttp4Android13);

ChameleonWebSocketOptions adds headers, sub-protocols and a keep-alive interval. A non-101 answer throws WebSocketUpgradeRejectedException with the status code.

Options

using System.Net;
using Chameleon.Net.Tls;

var options = new ChameleonOptions
{
    ProfileSelector = new RoundRobinProfileSelector([BuiltInProfiles.Chromium152Windows, BuiltInProfiles.Edge153Windows]),
    Proxy = new WebProxy("socks5://user:pass@proxy.example:1080"),
    Cookies = new CookieContainer(),
    TlsSessionCache = new TlsSessionCache(),
    LoggerFactory = loggerFactory,
};

using var client = new HttpClient(new ChameleonHttpMessageHandler(options));
var connector = new ChameleonWebSocketConnector(options);
  • ProfileSelector: FixedProfileSelector, RoundRobinProfileSelector, RandomProfileSelector (optionally weighted with WeightedProfile). Chosen per new connection.
  • Proxy: http:// proxies (CONNECT for https and wss, absolute-form requests for plain http, like OkHttp) and socks5:// proxies, with credentials from the URI or IWebProxy.Credentials.
  • Cookies: a CookieContainer, sent at the profile's position and filled from Set-Cookie.
  • TlsSessionResumption (on by default) and TlsSessionCache: give the handler and the WebSocket connector the same cache and the WebSocket connection resumes the TLS session of earlier requests, as one OkHttpClient does.
  • CertificateValidator: defaults to the OS trust store.
  • ConnectTimeout, AllowAutoRedirect, MaxAutomaticRedirections, LoggerFactory.

IHttpClientFactory and dependency injection

The Chameleon.Net.Extensions.Http package plugs the handler into IHttpClientFactory:

services.AddHttpClient("api", client => client.BaseAddress = new Uri("https://api.example.com/"))
    .UseChameleon(BuiltInProfiles.OkHttp4Android13, options => options.Cookies = new CookieContainer());

services.AddHttpClient<GitHubClient>()
    .UseChameleon(BuiltInProfiles.Chromium152Windows);

// Profile selection or options taken from the container:
services.AddHttpClient("rotating")
    .UseChameleon((provider, options) => options.ProfileSelector = provider.GetRequiredService<IProfileSelector>());

services.AddChameleonWebSocketConnector(); // ChameleonWebSocketConnector and IWebSocketConnector, as singletons
  • Logging goes to the container's ILoggerFactory.
  • Every client and the WebSocket connector share one TlsSessionCache, so a WebSocket resumes the TLS session of earlier requests to the same host, like one OkHttpClient used for both. Tickets are still kept apart per profile and per certificate validator.
  • UseChameleon sets the handler lifetime to infinite. The handler closes idle connections itself (OkHttp's pool rules), and IHttpClientFactory's default two-minute rotation would throw away its connections, TLS sessions and cookies, which the real clients keep. Call SetHandlerLifetime afterwards to change it.

Checking a fingerprint

TlsFingerprinter.Compute(ClientHelloParser.Parse(bytes)) gives JA3, JA4 and JA4_r for a captured ClientHello record; AkamaiFingerprint.Compute(profile.Http2) gives the Akamai HTTP/2 string. Services such as https://tls.peet.ws/api/all show what a server sees.

Creating a profile

docs/creating-a-profile.md covers capturing a client (packet capture, tls.peet.ws, or a scripted headless browser), turning the capture into a profile, and verifying it.

Limitations

  • HTTP/3 and QUIC are not implemented; neither is real ECH (GREASE ECH is).
  • Delegated credentials (offered by the Firefox profile) are not supported if a server uses them.
  • WebSockets always go over HTTP/1.1 (no RFC 8441).
  • TCP/IP-level fingerprints (TTL, window size, options) come from the OS.

Building and testing

dotnet build Chameleon.Net.slnx
dotnet test --solution Chameleon.Net.slnx

The default test run is offline. Tests that talk to real servers (tls.peet.ws, Cloudflare, Google, ...) are marked explicit and run with:

dotnet test --solution Chameleon.Net.slnx -- --explicit only

CI runs the offline tests on Linux and Windows for every push and pull request; the live tests can be started by hand from the Actions tab.

Versioning and releases

Versions come from git tags, through MinVer, following semantic versioning. Nothing in the repository holds a version number.

  • A commit tagged v1.2.3 builds as 1.2.3, and v1.2.3-preview.1 as 1.2.3-preview.1.
  • Commits after the latest tag build as the next patch preview, e.g. 1.2.4-preview.0.5 five commits after v1.2.3. Before any tag it's 0.0.0-preview.0.N.

To release, tag the commit and push the tag:

git tag v0.1.0
git push origin v0.1.0

The Release workflow then builds, tests, packs, publishes the packages to nuget.org and creates a GitHub release with the packages attached (marked as pre-release when the version has a suffix). It publishes through nuget.org trusted publishing, so no API key is stored. Both packages (Chameleon.Net and Chameleon.Net.Extensions.Http) are released together, with the same version. Publishing needs a trusted publishing policy on nuget.org for this repository and release.yml, and a NUGET_USER repository secret with the nuget.org user name.

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.0 68 10/4/2026
0.3.1 72 10/4/2026
0.3.0 72 10/4/2026
0.2.0 72 10/3/2026
0.1.0 72 10/2/2026