Lightweight, asynchronous HTTP/HTTPS proxy written in C. Zero dependencies, single file, minimal attack surface.
- HTTP request forwarding with header rewriting
- HTTPS tunneling via CONNECT method
- IPv4 and IPv6 support
- Single-threaded, non-blocking I/O with poll(2)
- Asynchronous DNS resolution via forked child processes
- Zero-copy kernel relay for CONNECT tunnels on OpenBSD (SO_SPLICE)
- Source IP access control lists (allow/deny with CIDR)
- CONNECT port whitelist (default: 443 only)
- Private/reserved address blocking (SSRF protection)
- Per-IP connection limits
- Privilege dropping after bind
- OpenBSD pledge(2)/unveil(2) and Linux seccomp-BPF sandboxing
- Automatic bind retry on restart
- ~26 KB memory per connection
make
Builds on OpenBSD, Linux (glibc and musl), macOS, FreeBSD, NetBSD, and DragonFlyBSD.
make install
The default prefix is /usr/local. Override with:
make install PREFIX=/usr DESTDIR=/tmp/pkg
rcctl enable thinproxy
rcctl start thinproxysudo systemctl daemon-reload
sudo systemctl enable --now thinproxythinproxy [-dVv] [-b address] [-f config] [-p port] [-u user]
| Flag | Description |
|---|---|
-b address |
Bind address (default: 127.0.0.1) |
-d |
Daemonize and log to syslog |
-f config |
Configuration file (default: /etc/thinproxy.conf) |
-p port |
Listen port (default: 8080) |
-u user |
Drop privileges to user after bind |
-V |
Print version and exit |
-v |
Enable all log categories |
Command-line flags override configuration file values.
thinproxy # default settings
thinproxy -v -b 0.0.0.0 -p 3128 # all interfaces, verbose
thinproxy -d -u _thinproxy # daemon with privilege drop
thinproxy -f /etc/thinproxy/thinproxy.conf # custom config
curl -x http://127.0.0.1:8080 http://example.com
curl -x http://127.0.0.1:8080 https://example.comDefault path: /etc/thinproxy.conf (silently ignored if missing).
See thinproxy.conf.example for a full example.
| Directive | Description | Default |
|---|---|---|
listen |
Bind address | 127.0.0.1 |
port |
Listen port | 8080 |
user |
Drop privileges to user | none |
daemon |
Run as daemon (yes/no) |
no |
verbose |
Enable all log categories (yes/no) |
no |
log |
Log category (repeatable: requests, denied, wildcard) |
denied |
| Directive | Description | Default |
|---|---|---|
max_connections |
Max concurrent connections (1-512) | 512 |
max_connections_per_ip |
Max connections per source IP (1-512); IPv6 peers are aggregated by /64 | 32 |
idle_timeout |
Idle timeout in seconds (1-86400) | 300 |
| Directive | Description | Default |
|---|---|---|
deny_private |
Block private/reserved destinations (yes/no) |
yes |
nat64_prefix |
Additional NAT64 /96 prefix to unwrap (e.g. 2001:db8:64::/96) |
(well-known 64:ff9b::/96 only) |
connect_port |
Allowed CONNECT port (repeatable, 0 = wildcard) |
443 |
allow |
Allow source address/CIDR (whitelist mode) | |
deny |
Deny source address/CIDR (blacklist mode) |
Use allow or deny directives, but not both.
When allow is used, unlisted addresses are denied.
When deny is used, unlisted addresses are allowed.
Both IPv4 and IPv6 with optional CIDR prefix are supported.
thinproxy logs to stderr, or to syslog when run as a daemon with -d. A request is logged once for its disposition (never both an accepted and a denied line):
- Accepted (
requestscategory):<client> <method> <host>:<port>[<path>](path shown for plain HTTP) - Denied (
deniedcategory):<client> DENIED [<dest>] <reason>, where<dest>is shown when known (omitted forACLandPER_IP_LIMIT, which are decided before the request is read); reasons areACL,PER_IP_LIMIT,CONNECT_PORT,PRIVATE_ADDRESS - Resolve failure (always logged):
<client> RESOLVE_FAILED <dest> <reason>with reasonLOOKUP,CHILD_KILLED,SHORT_READ, orSETUP - Timeout (
requestscategory):<client> TIMEOUT [<dest>]when a connection exceedsidle_timeout
For plain HTTP, <dest> includes the path. When the wildcard category is enabled, a CONNECT whose port matched the wildcard rule is tagged with a trailing WILDCARD_PORT on whichever line it produces.
listen 0.0.0.0
port 3128
user _thinproxy
daemon yes
deny_private yes
connect_port 443
connect_port 8443
max_connections_per_ip 16
allow 192.168.1.0/24
allow 127.0.0.1
SIGTERM, SIGINT, and SIGHUP all trigger a clean shutdown.
The daemon does not reload its configuration on SIGHUP; restart it to
pick up changes. SIGPIPE and SIGCHLD are ignored.
Requests are rejected with 400 Bad Request if they contain:
- Control bytes (other than SP) anywhere in the request line
- Whitespace inside the request-target
- Control bytes other than HTAB inside any header line
- Whitespace between a header name and its colon
- Obsolete header line folding (obs-fold)
- Duplicate
Host,Content-Length, orTransfer-Encodingheaders - A missing
Hostheader on a forwarded (non-CONNECT) request - A
Transfer-Encodingvalue other thanchunked - Both
Transfer-EncodingandContent-Lengthtogether
- pledge(2) restricts syscalls to
stdio inet dns proc - unveil(2) restricts filesystem to
/etc/resolv.confand/etc/hosts - SO_SPLICE provides zero-copy kernel relay for CONNECT tunnels
- accept4(2) with SOCK_NONBLOCK avoids extra fcntl(2) per connection
- Native strlcpy(3), closefrom(2), and strtonum(3)
- seccomp-BPF restricts syscalls to an allowlist (I/O, networking, DNS, process forking); violations are logged with the blocked syscall number
- Supports x86_64 and aarch64
- POSIX-compatible fallbacks for BSD-specific functions
- Packages available as
.deb,.rpm,.apk, and static binaries
- POSIX-compatible fallbacks for BSD-specific functions
- Static binaries available for FreeBSD, NetBSD, and DragonFlyBSD
BSD 2-Clause. See LICENSE.