Created: 2026-01-11
Last Updated: 2026-01-17
Status: Approved
Author: AI Assistant + Project Owner
This document outlines the phased implementation plan for tinyproxy-zig, a Zig implementation of the tinyproxy HTTP/HTTPS proxy daemon using the zio async I/O framework.
| Module | Status | Description |
|---|---|---|
main.zig |
✅ Done | CLI parsing, config load, runtime, signal setup |
child.zig |
✅ Done | Listen socket, accept loop, ACL checks, signal handling |
request.zig |
✅ Done | HTTP parsing, CONNECT tunnel, upstream handling (HTTP/SOCKS) |
relay.zig |
✅ Done | Bidirectional data relay |
buffer.zig |
✅ Done | Line reader |
config.zig |
✅ Done | Runtime config struct + helpers |
conf.zig |
✅ Done | Config parser (ReverseMagic deferred) |
log.zig |
✅ Done | File/stderr/syslog logging + rotation |
headers.zig |
✅ Done | Hop-by-hop removal + Via handling |
anonymous.zig |
✅ Done | Anonymous header whitelist filtering |
acl.zig |
✅ Done | ACL rules + integration |
auth.zig |
✅ Done | Basic auth + 407 response |
connect_ports.zig |
✅ Done | CONNECT port restrictions |
filter.zig |
✅ Done | URL/domain filtering with fnmatch patterns |
upstream.zig |
✅ Done | Upstream parsing + NoUpstream matching |
socks.zig |
✅ Done | SOCKS4a/SOCKS5 handshake + tests |
signals.zig |
✅ Done | SIGHUP/SIGUSR1/SIGTERM/SIGINT handlers + reload wiring |
daemon.zig |
✅ Done | Daemonize, PID file, privilege dropping |
stats.zig |
✅ Done | Statistics counters + HTML stats page |
html_error.zig |
✅ Done | HTML error pages with templates |
reverse.zig |
✅ Done | Reverse proxy path mapping + URL rewriting |
transparent.zig |
✅ Done | Transparent proxy SO_ORIGINAL_DST (Linux) |
socket.zig |
✅ Done | Socket helpers |
network.zig |
✅ Done | POSIX line reader helper |
proxy.zig |
✅ Done | Forward proxy test harness |
pool.zig |
✅ Done | Pool utility (imported) |
connection.zig |
✅ Done | Connection struct |
darwin.zig |
✅ Done | Darwin socket flags shim |
- HTTP Forward Proxy - Parse absolute URLs, forward requests
- HTTPS CONNECT Tunnel - Establish TCP tunnels with port restrictions
- HTTP Header Handling - Hop-by-hop removal + Via
- Anonymous Mode Filtering - Whitelist-only headers
- Access Control - ACL + Basic Auth
- Upstream Proxy Chains - HTTP + SOCKS4a/SOCKS5 + NoUpstream rules
Goal: Implement tinyproxy-compatible configuration file parsing with runtime reload support.
Data Structure:
pub const Config = struct {
// Network configuration
listen_addrs: std.ArrayList([]const u8),
port: u16 = 8888,
bind_addrs: ?std.ArrayList([]const u8) = null,
// Connection control
max_clients: u32 = 100,
idle_timeout: u32 = 600,
// Logging
log_file: ?[]const u8 = null,
log_level: LogLevel = .info,
use_syslog: bool = false,
// User/Group (daemon mode)
user: ?[]const u8 = null,
group: ?[]const u8 = null,
// Via header
via_proxy_name: ?[]const u8 = null,
disable_via_header: bool = false,
// Extensible fields for Phase 2-4
// filter, acl, upstream, reverse, auth...
pub fn load(allocator: Allocator, path: []const u8) !Config { ... }
pub fn deinit(self: *Config) void { ... }
};Config File Format (tinyproxy compatible):
Port 8888
Listen 127.0.0.1
MaxClients 100
Timeout 600
LogFile "/var/log/tinyproxy.log"
Tasks:
- 1.1.1 Define Config struct with all fields
- 1.1.2 Implement config file tokenizer
- 1.1.3 Implement directive parser
- 1.1.4 Add config reload support (SIGHUP wiring in
config.zig)
Goal: Unified logging interface supporting file, syslog, and stderr.
pub const LogLevel = enum { err, warning, notice, info, debug };
pub fn init(config: *const Config) !void { ... }
pub fn log(level: LogLevel, comptime fmt: []const u8, args: anytype) void { ... }
pub fn deinit() void { ... }Tasks:
- 1.2.1 Define LogLevel enum
- 1.2.2 Implement file logging
- 1.2.3 Implement stderr fallback
- 1.2.4 Add log rotation support (SIGUSR1)
- 1.2.5 Implement syslog backend (use_syslog)
Goal: Complete HTTP parsing with chunked encoding and Content-Length handling.
pub const HttpMessage = struct {
headers: std.StringHashMap([]const u8),
content_length: ?usize = null,
is_chunked: bool = false,
pub fn parse(allocator: Allocator, reader: anytype) !HttpMessage { ... }
pub fn getHeader(self: *const HttpMessage, name: []const u8) ?[]const u8 { ... }
pub fn deinit(self: *HttpMessage) void { ... }
};
pub const RequestLine = struct {
method: []const u8,
uri: []const u8,
version: HttpVersion,
};
pub const HttpVersion = enum { http10, http11 };Tasks:
- 2.1.1 Implement HttpMessage struct
- 2.1.2 Parse request/status line
- 2.1.3 Handle chunked transfer encoding
- 2.1.4 Handle Content-Length body reading
Goal: Handle hop-by-hop headers, add/remove/modify headers.
const hop_by_hop_headers = [_][]const u8{
"Connection", "Keep-Alive", "Proxy-Authenticate",
"Proxy-Authorization", "TE", "Trailers",
"Transfer-Encoding", "Upgrade", "Proxy-Connection",
};
pub fn removeHopByHop(headers: *std.StringHashMap([]const u8)) void { ... }
pub fn addViaHeader(headers: *std.StringHashMap([]const u8), config: *const Config) !void { ... }
pub fn processClientHeaders(headers: *std.StringHashMap([]const u8), config: *const Config) !void { ... }
pub fn processServerHeaders(headers: *std.StringHashMap([]const u8), config: *const Config) !void { ... }Tasks:
- 2.2.1 Implement hop-by-hop header removal
- 2.2.2 Implement Via header addition
- 2.2.3 Implement Connection header parsing
- 2.2.4 Add AddHeader config directive support (requires config system)
Goal: Forward only whitelisted headers to hide client information.
pub const AnonymousConfig = struct {
allowed_headers: std.StringHashMap(void),
pub fn init(allocator: Allocator) AnonymousConfig { ... }
pub fn allow(self: *AnonymousConfig, header: []const u8) !void { ... }
pub fn isAllowed(self: *const AnonymousConfig, header: []const u8) bool { ... }
};
pub fn filterHeaders(headers: *std.StringHashMap([]const u8), config: *const AnonymousConfig) void { ... }Tasks:
- 2.3.1 Implement AnonymousConfig struct
- 2.3.2 Add Anonymous config directive (requires config file parser)
- 2.3.3 Integrate into request processing pipeline
Goal: IP address/subnet-based Allow/Deny rules.
pub const AclAction = enum { allow, deny };
pub const AclEntry = struct {
action: AclAction,
spec: HostSpec,
};
pub const HostSpec = union(enum) {
ip4: std.net.Ip4Address,
ip4_cidr: struct { addr: std.net.Ip4Address, prefix_len: u5 },
ip6: std.net.Ip6Address,
ip6_cidr: struct { addr: std.net.Ip6Address, prefix_len: u7 },
hostname: []const u8,
};
pub const Acl = struct {
entries: std.ArrayList(AclEntry),
pub fn init(allocator: Allocator) Acl { ... }
pub fn addRule(self: *Acl, rule: []const u8, action: AclAction) !void { ... }
pub fn check(self: *const Acl, client_addr: std.net.Address) AclAction { ... }
pub fn deinit(self: *Acl) void { ... }
};Config Example:
Allow 127.0.0.1
Allow 192.168.0.0/16
Deny 0.0.0.0/0
Tasks:
- 3.1.1 Implement HostSpec union parsing
- 3.1.2 Implement CIDR matching
- 3.1.3 Implement Acl.check() logic
- 3.1.4 Integrate into connection acceptance
Goal: HTTP Basic Authentication with multiple users.
pub const BasicAuth = struct {
credentials: std.StringHashMap([]const u8),
realm: []const u8 = "tinyproxy",
pub fn init(allocator: Allocator) BasicAuth { ... }
pub fn addUser(self: *BasicAuth, user: []const u8, pass: []const u8) !void { ... }
pub fn verify(self: *const BasicAuth, auth_header: ?[]const u8) bool { ... }
pub fn deinit(self: *BasicAuth) void { ... }
};
pub fn sendAuthRequired(stream: *zio.net.Stream, rt: *zio.Runtime, realm: []const u8) !void { ... }Tasks:
- 3.2.1 Implement Base64 decode
- 3.2.2 Implement credential verification
- 3.2.3 Send 407 response
- 3.2.4 Add BasicAuth config directive
Goal: Restrict ports accessible via CONNECT method.
pub const ConnectPorts = struct {
allowed: std.ArrayList(PortRange),
pub const PortRange = struct { min: u16, max: u16 };
pub fn init(allocator: Allocator) ConnectPorts { ... }
pub fn allow(self: *ConnectPorts, port_spec: []const u8) !void { ... }
pub fn isAllowed(self: *const ConnectPorts, port: u16) bool { ... }
};Config Example:
ConnectPort 443
ConnectPort 563
ConnectPort 8000-9000
Tasks:
- 3.3.1 Implement port range parsing
- 3.3.2 Implement port check
- 3.3.3 Integrate into CONNECT handling
Goal: Regex/fnmatch-based URL filtering with blacklist/whitelist modes.
pub const FilterMode = enum { default_allow, default_deny };
pub const FilterType = enum { regex, fnmatch };
pub const Filter = struct {
patterns: std.ArrayList(Pattern),
mode: FilterMode = .default_allow,
filter_type: FilterType = .regex,
case_sensitive: bool = false,
pub fn init(allocator: Allocator) Filter { ... }
pub fn loadFromFile(self: *Filter, path: []const u8) !void { ... }
pub fn check(self: *const Filter, url: []const u8) bool { ... }
pub fn deinit(self: *Filter) void { ... }
};Tasks:
- 3.4.1 Implement pattern file parsing
- 3.4.2 Implement fnmatch matching
- 3.4.3 Implement regex matching (use std regex or simple glob) - Deferred, fnmatch covers most use cases
- 3.4.4 Integrate into request processing
Goal: Support HTTP/SOCKS4/SOCKS5 upstream proxies.
pub const ProxyType = enum { none, http, socks4, socks5 };
pub const UpstreamProxy = struct {
host: []const u8,
port: u16,
proxy_type: ProxyType,
user: ?[]const u8 = null,
pass: ?[]const u8 = null,
target: ?HostSpec = null,
};
pub const UpstreamManager = struct {
proxies: std.ArrayList(UpstreamProxy),
no_upstream: std.ArrayList(HostSpec),
pub fn init(allocator: Allocator) UpstreamManager { ... }
pub fn addUpstream(self: *UpstreamManager, spec: []const u8) !void { ... }
pub fn addNoUpstream(self: *UpstreamManager, domain: []const u8) !void { ... }
pub fn getUpstream(self: *const UpstreamManager, host: []const u8) ?*const UpstreamProxy { ... }
pub fn deinit(self: *UpstreamManager) void { ... }
};Config Example:
Upstream http 192.168.1.1:8080
Upstream socks5 user:pass@proxy.example.com:1080 ".onion"
NoUpstream "192.168.0.0/16"
NoUpstream ".local"
Tasks:
- 4.1.1 Implement upstream config parsing
- 4.1.2 Implement HTTP CONNECT to upstream
- 4.1.3 Implement SOCKS4a protocol
- 4.1.4 Implement SOCKS5 protocol with auth
- 4.1.5 Implement NoUpstream matching
Goal: Map URL paths to backend servers.
pub const ReversePath = struct {
path: []const u8,
upstream_url: []const u8,
};
pub const ReverseProxy = struct {
paths: std.ArrayList(ReversePath),
only_reverse: bool = false,
magic_cookie: bool = false,
base_url: ?[]const u8 = null,
pub fn init(allocator: Allocator) ReverseProxy { ... }
pub fn addPath(self: *ReverseProxy, path: []const u8, url: []const u8) !void { ... }
pub fn rewriteUrl(self: *const ReverseProxy, request_url: []const u8) ?RewriteResult { ... }
pub fn deinit(self: *ReverseProxy) void { ... }
};
pub const RewriteResult = struct {
new_host: []const u8,
new_port: u16,
new_path: []const u8,
};Config Example:
ReversePath "/api" "http://api-server:8080/"
ReversePath "/static" "http://cdn:80/"
ReverseOnly Yes
Tasks:
- 4.2.1 Implement path matching
- 4.2.2 Implement URL rewriting
- 4.2.3 Handle ReverseOnly mode
- 4.2.4 Implement magic cookie tracking (ReverseMagic)
Goal: Get original destination from socket (requires SO_ORIGINAL_DST).
pub const TransparentProxy = struct {
enabled: bool = false,
pub fn getOriginalDest(client_fd: std.posix.fd_t) !?std.net.Address { ... }
};Tasks:
- 4.3.1 Implement Linux SO_ORIGINAL_DST
- 4.3.2 Implement BSD pf support (optional) - Deferred, complex integration required
- 4.3.3 Integrate into request processing
Goal: Display runtime statistics via special URL.
pub const Stats = struct {
connections_opened: std.atomic.Value(u64) = .{ .raw = 0 },
connections_closed: std.atomic.Value(u64) = .{ .raw = 0 },
connections_refused: std.atomic.Value(u64) = .{ .raw = 0 },
connections_denied: std.atomic.Value(u64) = .{ .raw = 0 },
bytes_sent: std.atomic.Value(u64) = .{ .raw = 0 },
bytes_received: std.atomic.Value(u64) = .{ .raw = 0 },
start_time: i64,
pub fn init() Stats { ... }
pub fn record(self: *Stats, event: StatEvent) void { ... }
pub fn renderHtml(self: *const Stats, allocator: Allocator) ![]const u8 { ... }
};Tasks:
- 5.1.1 Implement Stats struct with atomics
- 5.1.2 Implement HTML rendering
- 5.1.3 Add StatHost config directive
Goal: Graceful shutdown, config reload, log rotation.
pub const SignalHandler = struct {
should_quit: std.atomic.Value(bool) = .{ .raw = false },
should_reload: std.atomic.Value(bool) = .{ .raw = false },
pub fn init() !SignalHandler { ... }
pub fn install(self: *SignalHandler) !void { ... }
};Tasks:
- 5.2.1 Install SIGTERM/SIGINT handlers
- 5.2.2 Install SIGHUP for config reload
- 5.2.3 Install SIGUSR1 for log rotation
Note: SIGHUP handler is installed, but actual config reload logic remains pending.
Goal: Background execution, PID file, privilege dropping.
pub fn daemonize() !void { ... }
pub fn writePidFile(path: []const u8) !void { ... }
pub fn dropPrivileges(user: ?[]const u8, group: ?[]const u8) !void { ... }Tasks:
- 5.3.1 Implement daemonize (fork, setsid)
- 5.3.2 Implement PID file management
- 5.3.3 Implement setuid/setgid
Goal: Return friendly HTML error pages.
pub const HttpError = enum(u16) {
bad_request = 400,
unauthorized = 401,
forbidden = 403,
not_found = 404,
proxy_auth_required = 407,
request_timeout = 408,
bad_gateway = 502,
service_unavailable = 503,
gateway_timeout = 504,
};
pub fn sendError(rt: *zio.Runtime, stream: *zio.net.Stream, err: HttpError, config: *const Config) !void { ... }Tasks:
- 5.4.1 Implement default error templates
- 5.4.2 Add ErrorFile config directive
- 5.4.3 Implement template variable substitution
tinyproxy-zig/
├── build.zig
├── build.zig.zon
├── AGENTS.md
├── README.md
├── docs/
│ ├── roadmap.md
│ └── plans/
│ ├── 2026-01-11-http-parsing-design.md
│ ├── 2026-01-15-config-cli-wiring.md
│ ├── 2026-01-17-socks-upstream-design.md
│ └── 2026-01-17-socks-upstream-implementation-plan.md
├── openspec/
│ ├── AGENTS.md
│ ├── project.md
│ ├── specs/
│ └── changes/
└── src/
├── main.zig
├── config.zig
├── conf.zig
├── log.zig
├── signals.zig
│
├── child.zig
├── request.zig
├── http.zig
├── headers.zig
├── anonymous.zig
├── buffer.zig
├── relay.zig
│
├── acl.zig
├── auth.zig
├── connect_ports.zig
├── filter.zig
│
├── upstream.zig
├── socks.zig
├── transparent.zig
│
│── # Utilities
├── connection.zig
├── socket.zig
├── network.zig
├── pool.zig
├── proxy.zig
├── text.zig
└── darwin.zig
- Zig Standard Library Style: Use
error union,optional, avoid global state - Minimal Testing: Core functionality only, manual curl testing
- Modular Design: One feature per file, clear interfaces
- Configuration-Driven: All features controllable via config file
- tinyproxy Compatibility: Same config format, same behavior
- Core tinyproxy config directives supported (Bind, XTinyproxy, ReverseMagic)
- Forward proxy works with curl and browsers
- CONNECT tunnel works for HTTPS
- ACL correctly blocks/allows by IP
- Upstream proxy chains work (HTTP + SOCKS4a/SOCKS5)
- Stats page accessible
- Graceful shutdown on SIGTERM
- Config reload on SIGHUP