← Back

https_proxy: A Stealth HTTPS Forward Proxy Written in Rust

https_proxy: A Stealth HTTPS Forward Proxy Written in Rust cover image

Why a Stealth Proxy

In the previous post I described how to build an HTTPS forward proxy with Caddy plus acme.sh. That setup is practical enough, but it still depends on several pieces glued together: Caddy itself, a special fork of the forwardproxy plugin, and acme.sh renewing certificates from cron. If any one of them breaks, the proxy goes down.

https_proxy is a single-binary HTTPS forward proxy written in Rust. Certificate issuance, TLS termination, proxy forwarding and stealth disguise all live in one executable of under 7 MB. The idea behind it is simple:

  • From the outside it looks like an ordinary nginx server
  • Only clients holding the right credentials can use the proxy
  • Certificates are issued and renewed automatically by Let's Encrypt, with no extra tooling

Architecture and How It Works

The whole request path looks like this:

Client TLS Layer ACME auto cert TLS-ALPN-01 Stealth Check Proxy request? HTTP/1.1 & HTTP/2 Auth Check Basic Auth Multi-user creds Forwarding CONNECT tunnel HTTP forward Fake 404 nginx-style page 407 Auth Credentials needed Target Not proxy Auth failed Forward Normal flow Reject / decoy

1. TLS Termination and Automatic Certificates

https_proxy uses the tokio-rustls-acme crate to do ACME TLS-ALPN-01 validation. Unlike the more common HTTP-01 challenge, TLS-ALPN-01 only needs port 443, so you never have to open port 80. Issuance and renewal are fully automatic, driven by a background async task:

RUST
let acme_config = AcmeConfig::new([domain])
    .contact_push(format!("mailto:{}", config.acme.email))
    .cache(DirCache::new(cache_dir))
    .directory_lets_encrypt(!config.acme.staging);

The TLS config supports both TLS 1.2 and TLS 1.3, and ALPN negotiation offers h2 and http/1.1, so browsers and command line tools can both connect normally.

2. The Stealth Layer: Pretending to Be nginx

This is the most interesting part of the design. The stealth check is quite precise:

  • If the request method is CONNECT → this is a proxy request
  • If it is HTTP/2 but not CONNECT → it is definitely not a proxy request (the HTTP/2 :authority pseudo-header is always present, so it cannot be used to tell them apart)
  • If it is HTTP/1.x and the URI contains an authority (the absolute-URI form) → this is a proxy request

The flowchart below shows the full stealth check and authentication decision:

TLS request arrives Method is CONNECT ? Yes Proxy request No HTTP/2 ? Yes Fake 404 nginx style No URI has authority ? Yes Proxy request No Fake 404 nginx style

Every request that fails those conditions gets a 404 page byte-identical to nginx's:

HTML
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.24.0</center>
</body>
</html>

The Server response header is also set to whatever the config file says (nginx/1.24.0 by default). Whether a scanner probes over HTTP/1.1 or HTTP/2, what it sees is an ordinary nginx. A 404 says "no site configured here".

3. The Auth Layer

Once a request passes the stealth check, it goes through authentication. https_proxy uses standard HTTP Basic Auth and supports multiple username/password pairs. On failure it returns 407 Proxy Authentication Required with a Proxy-Authenticate header, so Chrome and friends pop up a credential prompt instead of showing some baffling error page.

4. Proxy Forwarding

After authentication there are two paths, depending on the request type:

CONNECT tunnel: the client sends CONNECT host:port, the proxy replies 200 and upgrades the connection, then uses tokio::io::copy_bidirectional to shuttle bytes between the client and the target server. The tunnel buffer is set to 128 KiB (16 times the default) to match TLS record sizes and cut down on syscalls.

HTTP forwarding: for plaintext HTTP requests, the proxy strips hop-by-hop headers such as Proxy-Authorization and Proxy-Connection, then forwards the request upstream. With TCP Fast Open off it reuses connections from a pool; with TFO on it connects manually so that TFO can be used.

5. HTTP/2 Support

https_proxy implements HTTP/2 fully, including the extended CONNECT protocol from RFC 8441. That means modern browsers like Chrome can use the proxy over HTTP/2 without falling back to HTTP/1.1.

Deployment Guide

Prerequisites

  • A server with a public IP
  • A domain whose A record points at that IP
  • Port 443 free on the server

Building

https_proxy is written in Rust and needs Rust 1.70+ and a C compiler.

Building directly on a Linux server:

Bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install build dependencies (Debian/Ubuntu)
apt install build-essential cmake

# Clone and build
git clone https://github.com/madeye/https_proxy.git
cd https_proxy
cargo build --release

Cross-compiling a Linux build on macOS:

Bash
docker run --platform linux/amd64 --rm -v "$(pwd)":/src -w /src \
  rust:latest cargo build --release --target x86_64-unknown-linux-gnu

The release build turns on LTO and strip, so the final binary is around 7 MB.

Configuration

You can generate a config with the built-in TUI wizard:

Bash
./target/release/https_proxy setup

Or edit config.yaml by hand:

YAML
listen: "0.0.0.0:443"
domain: "proxy.example.com"
acme:
  email: "[email protected]"
  staging: false
  cache_dir: "/var/lib/https_proxy/acme"
users:
  - username: "alice"
    password: "hunter2"
  - username: "bob"
    password: "correct-horse-battery-staple"
stealth:
  server_name: "nginx/1.24.0"
fast_open: true

What the fields mean:

FieldDescription
listenListen address, 0.0.0.0:443 by default
domainThe domain the ACME certificate is issued for
acme.emailContact email for Let's Encrypt
acme.stagingWhether to use the staging environment (worth turning on while testing, to avoid hitting rate limits)
acme.cache_dirCertificate cache directory
usersList of authorized users, multiple credentials supported
stealth.server_nameThe Server response header to impersonate
fast_openEnable TCP Fast Open to cut connection setup latency

Running It

Bash
# Run it directly
./target/release/https_proxy run --config config.yaml

# Or use the default config.yaml
./target/release/https_proxy

Installing as a System Service

On Linux you can install it as a systemd service in one command:

Bash
sudo ./target/release/https_proxy install

To uninstall:

Bash
sudo ./target/release/https_proxy uninstall

Using It from a Client

Command Line

Bash
# Reach a site through the proxy
curl --proxy https://alice:[email protected]:443 https://httpbin.org/ip

# Set the environment variables so every command uses the proxy
export https_proxy=https://alice:[email protected]:443
export http_proxy=https://alice:[email protected]:443
curl https://www.google.com

Browsers

Chrome, Firefox and others can point at an HTTPS proxy through the system proxy settings, or through an extension like SwitchyOmega. Pick HTTPS as the protocol and fill in the domain, port (443), username and password. The browser will prompt for credentials on the first request.

Checking the Disguise

Visit the proxy domain directly and you should get the nginx-style 404 page:

Bash
curl https://proxy.example.com/
# Returns: 404 Not Found (Server: nginx/1.24.0)

Compared with the Caddy Setup

https_proxyCaddy + forwardproxy
Number of componentsOne binaryCaddy + an xcaddy build + acme.sh
Certificate managementBuilt-in ACME (TLS-ALPN-01)Depends on external acme.sh
HTTP/2 proxyingYes (RFC 8441 extended CONNECT)Yes
Stealth disguiseBuilt-in nginx 404 decoyVia probe_resistance + file_server
TCP Fast OpenBuilt inNot supported
Config complexityA single YAML fileCaddyfile + acme.sh config + systemd
Binary size~7 MB~40 MB (Caddy)

Each has its strengths. The Caddy setup has the more mature ecosystem and can host websites and other reverse proxies at the same time; https_proxy wins on how little there is to deploy. One binary, one config file, ready to go.

Wrap-up

The idea behind https_proxy is to keep everything in one place:

  1. Single-binary deployment: no external tools, usable as soon as it compiles
  2. Automatic certificates: Let's Encrypt certificates issued and renewed on their own, no cron job
  3. Precise stealth: correctly identifies and disguises for both HTTP/1.1 and HTTP/2, so simple scanners find nothing
  4. Performance work: TCP Fast Open, a 128 KiB tunnel buffer, connection pooling
  5. Real authentication: multi-user Basic Auth, with native browser credential prompts