# curl_cffi proxy lab

Loopback lab behind the ipvolt guide [curl_cffi proxies](https://ipvolt.com/guides/curl-cffi-proxy-setup).
It records what curl_cffi does with proxy credentials, a 407 from the proxy for `https://` and `http://`
URLs, proxy environment variables, `socks5://` and `socks5h://` proxy URLs, a failed SOCKS handshake,
`AsyncSession` and the `retry` argument. Everything runs on loopback addresses. No proxy account is needed.

## Files

| File | What it is |
| --- | --- |
| `run_lab.py` | Starts the fixtures, runs every case with `python <case>.py` in a new interpreter with a clean environment, writes `results.json` |
| `run_lab.sh` | Runs `run_lab.py` with a private copy of `/etc/hosts` that contains the line from `hosts.lab` |
| `cases.py` | The cases: name, environment variables and the script |
| `lab_common.py` | The fixture URLs and the `show()` / `show_error()` helpers every case imports |
| `auth_proxy.py` | HTTP proxy that requires Basic authentication, supports `CONNECT` and absolute-form requests, answers 407 with `Proxy-Authenticate` otherwise and 502 when it cannot reach the target |
| `socks5_server.py` | SOCKS5 server without authentication that logs the address type and destination of each request; with `--reply N` it answers every request with that reply code |
| `jobs_target.py` | HTTP target that loses the response to the first `POST` of each body |
| `hosts.lab` | `127.0.0.2 dnsprobe.example`, the name used to see who resolves the hostname |
| `requirements.txt` | `curl_cffi==0.16.3` |
| `results.json` | The recorded run |
| `version_matrix.py` | Installs several curl_cffi releases in throwaway virtual environments and records what each raises for a rejected `CONNECT` and a SOCKS failure |
| `versions.json` | The recorded version comparison |

The other targets are `python -m http.server` on `127.0.0.1:8080` and `127.0.0.2:8080`, and the same server
with `--tls-cert` on port 8443, which needs Python 3.14. The runner creates a two-day self-signed certificate
with the `openssl` command and passes its path to the cases as `LAB_CA`, so every `https://` case verifies
the certificate with `verify=CA`.

Three proxies run: ports 8888 and 8889 accept `user` / `pass`, port 8890 accepts `user` / `p@ss:w/rd`.
Two SOCKS5 servers run: port 1080 connects and logs, port 1081 logs and answers reply code 1.

## Run it

```sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
sudo sh run_lab.sh
```

`run_lab.sh` needs root on Linux because it mounts a private copy of `/etc/hosts` in a new mount namespace;
the machine's own file is not edited. On a machine you own you can add the line from `hosts.lab` to
`/etc/hosts` and run `python run_lab.py` instead.

Ports 8080, 8081, 8443, 8888, 8889, 8890, 1080 and 1081 must be free, and port 9 must be closed: the cases
use it as the port nothing listens on. The runner stops if a fixture port is in use. A full run takes about
two minutes. One case, `proxy_host_does_not_resolve`, asks the system resolver for `proxy.invalid`.

The version comparison is separate and is the only part that downloads anything after the install:

```sh
python version_matrix.py
```

To run one case by hand, start the fixtures yourself, save the case's `code` from `results.json` as a file
next to `lab_common.py`, set `LAB_CA` to a CA file and run it with `python`.

## Reading results.json

The top level has the versions, the fixture ports, the hosts entry and the text of `lab_common.py`. Each
case has:

- `command` and `code`: what ran.
- `env`: the only variables the case received besides `PATH`, `HOME`, `PYTHONPATH` and `LAB_CA`.
- `stdout`: a `RESPONSE ...` line for a response, or an `EXCEPTION <class>: <message>` line followed by a
  `DETAIL` line with the curl error code, the `isinstance` checks and the status of `error.response`.
- `proxy_log`: the requests the proxies received during the case, with the proxy port, the method, the
  target, whether the `Proxy-Authorization` header was `ok`, `wrong` or `missing`, and the names of all
  headers received.
- `socks_log` and `failing_socks_log`: what the two SOCKS5 servers logged during the case.
- `target_log`: what the jobs target logged during the case.

A case that went direct has an empty `proxy_log` and a 200 response.

## Limits

Linux only as recorded (Ubuntu 26.04.1, x86_64). Not covered: macOS and Windows, Digest and NTLM proxy
authentication, authenticated SOCKS proxies, proxies reached over TLS, HTTP/3 and WebSockets. The proxies
and SOCKS servers are test fixtures: bind them to loopback only.
