TRAFFICPILOT 1.0.0 A local HTTP proxy that meters traffic per host. ------------------------------------------------------------------------------ WHAT IT IS ------------------------------------------------------------------------------ TrafficPilot is a real HTTP forward proxy that you run on your own machine. Point a program's HTTP proxy setting at it, and every request that program makes is measured: which host it talked to, how many bytes went out, how many came back, how long it took. Those measurements are appended to a JSON-lines traffic log, and "trafficpilot report" turns that log into per-host totals, averages and a share-of-bandwidth percentage. It answers "what is actually using my bandwidth" with measurements instead of guesses, because the bytes are counted as they cross the wire. It is a desktop command-line program. Go standard library only, no third-party dependencies, single binary, nothing to install on the system. ------------------------------------------------------------------------------ READ THIS FIRST: WHAT TRAFFICPILOT CAN AND CANNOT SEE ------------------------------------------------------------------------------ HTTPS TRAFFIC IS TUNNELLED, NOT DECRYPTED. When a client asks for an https:// URL it sends the proxy a CONNECT request. TrafficPilot opens a plain TCP connection to that host and then copies bytes back and forth without looking inside them. Everything inside that tunnel is TLS: TrafficPilot cannot read it, and does not try to. For a tunnelled request the log records: the host name the client asked for (from the CONNECT line) the number of bytes sent to it (TLS handshake and records included) the number of bytes received from it (same) the status of the tunnel and its duration and NOTHING ELSE. There is no path, no query string, no header, no cookie, no request or response body. The "path" field is absent from CONNECT records precisely because there is nothing honest to put in it, and "resp_body_bytes" is 0 because inside a tunnel no body is identifiable. TrafficPilot DELIBERATELY DOES NOT: - install a root certificate anywhere, in any trust store - generate certificates for the hosts you visit - intercept, terminate or re-encrypt TLS ("HTTPS interception") - modify any byte of any payload it relays That is a deliberate design decision, not a missing feature. Tools that show you the inside of your HTTPS traffic do it by making your machine trust a certificate authority they control. TrafficPilot does not ask you for that trust, so what it can tell you about HTTPS stops at the host and the byte count. For plain HTTP, which is not encrypted, the log also records the method, the path and the status, because those genuinely did cross the wire in clear. TRAFFICPILOT ONLY SEES A PROGRAM'S TRAFFIC IF THAT PROGRAM IS CONFIGURED TO USE IT. It is not a packet capture, not a firewall, not a system-wide hook. It sees exactly what is sent to it and nothing else. A program that ignores proxy settings, uses its own DNS-over-HTTPS, or talks a non-HTTP protocol will not show up in the log at all. Absence from the report is not proof of silence. ------------------------------------------------------------------------------ QUICK START ------------------------------------------------------------------------------ # 1. run the proxy trafficpilot proxy --listen 127.0.0.1:8080 --log traffic.jsonl # 2. in another terminal, send a program through it curl -x http://127.0.0.1:8080 http://example.com/ http_proxy=http://127.0.0.1:8080 https_proxy=http://127.0.0.1:8080 some-program # 3. see where the bytes went trafficpilot report --log traffic.jsonl --top 10 # or watch it live while it happens trafficpilot watch --log traffic.jsonl --interval 2s Stop the proxy with Ctrl-C or SIGTERM: it closes the listener, flushes the log and prints a session summary. ------------------------------------------------------------------------------ COMMANDS ------------------------------------------------------------------------------ trafficpilot proxy --listen [flags] Runs the metering forward proxy. --listen address to listen on (default 127.0.0.1:8080) --log append one JSON line per completed request --allow only these hosts may be proxied (repeatable) --block refuse these hosts (repeatable; block beats allow) --max-body cut a relayed response body after N bytes (0 = off) --json print each metered request as JSON --quiet do not print per-request lines -h, --help show help and exit 0 Handles absolute-form request URIs for plain HTTP ("GET http://host/path HTTP/1.1", which is what a client sends to a proxy) and CONNECT for HTTPS. trafficpilot report --log [flags] Per host: requests, bytes in, bytes out, their total, average duration and that host's share of all metered bytes. Plus whole-log totals and the busiest host. --log traffic log written by the proxy (required) --top show only the N busiest hosts (0 = all) --json emit JSON instead of text -h, --help show help and exit 0 trafficpilot watch --log [flags] Live tail: every interval, prints what arrived since the last one, with a running cumulative total. --log traffic log to follow (required) --interval summary interval (default 2s) --from-start include records already in the log -h, --help show help and exit 0 trafficpilot help | -h | --help show help, exit 0 trafficpilot version | -v | --version print the version A bad invocation prints the reason and the usage to stderr and exits 1. ------------------------------------------------------------------------------ THE TRAFFIC LOG ------------------------------------------------------------------------------ One JSON object per line, appended and fsynced as each request completes, so the file is always valid even if the proxy is killed. A plain HTTP record: {"ts":"2026-08-11T06:54:16.6Z","method":"GET","host":"127.0.0.1", "path":"/big.bin","proto":"http","status":200,"req_bytes":104, "resp_bytes":1048782,"req_body_bytes":0,"resp_body_bytes":1048576, "duration_ms":2.153,"result":"ok"} A CONNECT (HTTPS) record - note the absent path: {"ts":"2026-08-11T06:53:38.5Z","method":"CONNECT","host":"localhost", "proto":"connect","status":200,"req_bytes":753,"resp_bytes":67677, "req_body_bytes":0,"resp_body_bytes":0,"duration_ms":60.9, "result":"ok","reason":"tunnelled, not decrypted"} FIELDS ts RFC3339 UTC timestamp, when the request started method HTTP method, or CONNECT for a tunnel host hostname the client asked for, port removed, lowercased path request URI - PLAIN HTTP ONLY, absent for CONNECT proto "http" (relayed and measured) or "connect" (tunnelled) status status returned to the client (403 refused, 502 upstream unreachable, 200 for an established tunnel) req_bytes every byte written to the host: for plain HTTP the request line, headers and body; for CONNECT everything the client pushed through the tunnel, TLS handshake included resp_bytes every byte read back from the host, on the wire, undecoded req_body_bytes request payload only (plain HTTP only) resp_body_bytes response payload only, exactly as handed to the client (plain HTTP only; 0 for a tunnel) duration_ms wall time from first byte of the request to completion result "ok", "refused" (a policy said no) or "error" reason why, when there is a why BYTE ACCOUNTING Bytes are counted on the upstream socket itself, so the numbers are what actually crossed the wire, not an estimate from Content-Length. The proxy disables transparent decompression and connection reuse, so a gzipped response is measured compressed - as it was billed to you - and every byte belongs to exactly one request. resp_bytes minus resp_body_bytes is the response header overhead. For a tunnel, resp_bytes minus the payload you know about is TLS overhead. HOST GLOBS Shell-style, case-insensitive, matched against the hostname with the port removed: "example.com", "*.example.com", "127.0.0.*", "*". A refused request is answered with 403 and written to the log as "refused" with the reason. Block is evaluated before allow. ------------------------------------------------------------------------------ EXIT CODES ------------------------------------------------------------------------------ 0 success, or help 1 bad invocation, unreadable log, port already in use, listener failure ------------------------------------------------------------------------------ LIMITS, PLAINLY ------------------------------------------------------------------------------ - HTTPS bodies are invisible. See the section above; this is by design. - Only traffic explicitly sent to the proxy is measured. - Per-request byte counts include protocol overhead on the wire; they will not match a "content length" figure and are not meant to. - The proxy does not cache, rewrite, or authenticate. It measures. - --max-body cuts a response short on purpose: the client sees a truncated transfer and the log says why. Leave it at 0 unless you want that. - WebSocket upgrades over plain HTTP are not tunnelled; use CONNECT. ------------------------------------------------------------------------------ ROADMAP ------------------------------------------------------------------------------ - System-wide capture, so traffic is measured whether or not a program was configured to use the proxy. - Per-process attribution, so a host in the report can be traced back to the program that asked for it. - Bandwidth limiting, so a host or a process can be given a ceiling instead of only being counted. ------------------------------------------------------------------------------ BUILDING ------------------------------------------------------------------------------ go build -o trafficpilot . Go 1.24, standard library only. GOPROXY=off works: there is nothing to download. Prebuilt binaries are in dist/: trafficpilot-linux-amd64 trafficpilot-darwin-amd64 trafficpilot-darwin-arm64 trafficpilot-windows-amd64.exe