# SSL WRONG_VERSION_NUMBER behind a proxy: loopback lab

This download reproduces the TLS error `wrong version number` at both hops of a
proxied HTTPS request. It backs the guide at
https://ipvolt.com/guides/fix-ssl-wrong-version-number-proxy.

Everything listens on `127.0.0.1`. No proxy account is needed and no packet
leaves the machine.

| Listener | Port | Role |
| --- | --- | --- |
| plain-400 listener | 18400 | answers any input with a plaintext `HTTP/1.1 400 Bad Request` (a plain HTTP proxy port) |
| CONNECT proxy | 18480 | minimal plain-HTTP forward proxy; tunnels to loopback only, answers 400 to anything that is not HTTP |
| plain HTTP target | 18000 | `python -m http.server` |
| TLS target | 18443 | the same handler behind a throwaway self-signed certificate |

Cases:

- `case1-proxy-hop`: proxy `https://127.0.0.1:18400`, target `https://127.0.0.1:18443/`
- `case1-proxy-hop-connect-proxy`: proxy `https://127.0.0.1:18480`, same target
- `case2-target-hop`: proxy `http://127.0.0.1:18480`, target `https://127.0.0.1:18000/`
- `case2-direct`: the case 2 target with no proxy
- `control-fixed`: proxy `http://127.0.0.1:18480`, target `https://127.0.0.1:18443/` (returns 200)

## Run it

Requirements: Python 3.11 or later, the `openssl` command, and curl. Node.js 24
and Playwright are optional.

```sh
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python lab.py --out local-results.json --skip-playwright
```

Options:

- `--curl PATH` and `--python PATH` (repeatable) add more builds to every case,
  for example ones linked against a different OpenSSL. Each extra Python needs
  `requests` and `httpx` installed.
- `--skip-node` and `--skip-playwright` leave those clients out. The Playwright
  client needs `playwright` resolvable by Node (for example through `NODE_PATH`)
  and its Chromium installed.
- `--serve` only starts the listeners and prints the case URLs and the
  certificate path, so you can try commands by hand.

## Files

- `lab.py`: the listeners and the runner.
- `client_requests.py`, `client_httpx.py`: one GET each, printing the exception
  chain as JSON. `client_httpx_debug.py` repeats the HTTPX request with
  httpcore's debug log on.
- `client_fetch.mjs`: Node's built-in `fetch` with `NODE_USE_ENV_PROXY=1`.
- `client_playwright.cjs`: one Chromium navigation.
- `results.json`: the run recorded on 4 October 2026 on Ubuntu 26.04 (x86_64).

## Recorded run

- curl 8.18.0 (OpenSSL 3.5.5), 8.7.1 (OpenSSL 3.2.1), 8.12.1 (OpenSSL 3.4.1),
  8.17.0 (OpenSSL 3.6.0) and 8.22.0 (OpenSSL 4.0.2)
- Requests 2.34.2 with urllib3 2.8.0, and HTTPX 0.28.1 with httpcore 1.0.9, on
  Python 3.14.4 (OpenSSL 3.5.5), Python 3.12.3 (OpenSSL 3.0.13) and Python 3.11.3
  (OpenSSL 1.1.1s)
- Node.js 24.20.0 (OpenSSL 3.5.7, undici 7.29.0)
- Playwright 1.63.0 with Chromium 153.0.8010.12

`results.json` holds, per case and client, the exit code or exception chain, the
`%{http_connect}` value, the relevant `curl -v` lines and what the CONNECT proxy
logged. The certificate is created for each run and deleted afterwards.

The lab does not cover macOS or Windows, TLS libraries other than OpenSSL and
Chromium's own, SOCKS proxies, real TLS proxy ports, or a proxy that closes or
stays silent with no plaintext reply.
