A transparent proxy that routes traffic through upstream proxies based on hostname and IP matching. Supports HTTP, HTTPS, WebSocket, WSS, and SOCKS5 without terminating TLS.
-
Transparent proxying — HTTP, HTTPS, WebSocket, and WSS
-
SOCKS5 inbound — accepts SOCKS5 clients on the same port, auto-detected alongside HTTP
-
No TLS termination — extracts SNI from the ClientHello for routing, then tunnels raw bytes
-
HTTP CONNECT — works as an explicit proxy for clients that support it
-
Flexible routing — exact hostnames, domain suffixes, glob patterns, and CIDR subnets
-
Upstream proxy support — HTTP CONNECT, SOCKS5, and WireGuard upstreams
-
Connection pooling — reuses upstream connections across HTTP requests
-
HTTP keep-alive — serves multiple requests per client connection
-
Hot reload — config changes (including included files) are detected and applied without restart
-
Config includes — split config across multiple files with glob support
-
Live log streaming — stream colored logs from a running server with level filtering
-
Health checks — TCP health checks for all upstreams via the status command
-
Internal pages — link dashboards at
pages.subspace.puband statistics atstats.subspace.pub -
Statistics — live metrics, upstream health, and historical charts with persistent SQLite storage
-
Styled error pages — DNS failures and connection errors show helpful error pages instead of bare 502s
brew install davidolrik/tap/subspaceTo run subspace as a background service that starts automatically at login:
brew services start subspaceRequires Go 1.26 or later.
go install go.olrik.dev/subspace@latestOr clone and build:
git clone https://github.com/davidolrik/subspace.git
cd subspace
go build -o subspace .Create a config file at ~/.config/subspace/config.kdl:
listen "127.0.0.1:8118"
upstream "corporate" {
type "http"
address "proxy.corp.com:3128"
username "user"
password "pass"
}
upstream "tunnel" {
type "socks5"
address "socks.example.com:1080"
}
route ".corp.internal" via="corporate"
route "specific.host.com" via="tunnel"Start the proxy:
subspace serveUse it:
# HTTP proxy
curl -x http://localhost:8118 http://example.com
# HTTPS (via CONNECT)
curl -x http://localhost:8118 https://example.com
# SOCKS5 (same port)
curl --socks5-hostname localhost:8118 https://example.com
git -c http.proxy=socks5h://localhost:8118 clone https://github.com/org/repoSubspace uses KDL for configuration. The default config path is ~/.config/subspace/config.kdl (respects $XDG_CONFIG_HOME).
The address to listen on.
listen "127.0.0.1:8118"Path to the Unix control socket used by status and logs commands. Defaults to ~/.config/subspace/control.sock.
control_socket "/tmp/subspace.sock"Defines a named upstream proxy. Supported types: http (HTTP CONNECT), socks5, and wireguard.
upstream "corporate" {
type "http"
address "proxy.corp.com:3128"
username "user" // optional
password "secret" // optional
}
upstream "tunnel" {
type "socks5"
address "127.0.0.1:1080"
}
upstream "home" {
type "wireguard"
endpoint "vpn.example.com:51820"
private-key "base64-encoded-private-key"
public-key "base64-encoded-peer-public-key"
address "10.0.0.2/32"
dns "1.1.1.1" // optional
}Routes traffic matching a pattern through a named upstream. Rules are evaluated in order and the last match wins.
// Exact hostname
route "specific.host.com" via="myproxy"
// Domain suffix — matches any subdomain of example.com
route ".example.com" via="myproxy"
// Glob pattern — matches IP ranges
route "192.168.*.*" via="lan-proxy"
// CIDR subnet — matches IP addresses in the network
route "10.0.0.0/8" via="internal"
// Direct — bypass all upstreams for this pattern
route "bypass.corp.com" via="direct"Pattern types:
| Pattern | Example | Matches |
|---|---|---|
| Exact | "example.com" |
example.com only |
| Domain suffix | ".example.com" |
foo.example.com, bar.baz.example.com |
| Glob | "192.168.*.*" |
192.168.1.1, 192.168.0.255 |
| CIDR | "10.0.0.0/8" |
any IP in the 10.0.0.0/8 subnet (IPv4 and IPv6) |
The built-in upstream direct can be used to bypass proxying for specific patterns, which is useful when a broader rule would otherwise match.
Unmatched traffic always connects directly.
Includes other config files, resolved relative to the file containing the include statement. Supports glob patterns.
include "upstreams/*.kdl"
include "routes/corporate.kdl"Nested includes are supported. Circular includes are detected and rejected. Glob patterns that match no files are silently ignored; exact paths that don't exist produce an error.
New files added to an already-included directory are picked up automatically on the next config change.
Defines an internal page served at pages.subspace.pub/{name}/. The page name is derived from the filename by default, or set explicitly with name=. An optional alias= adds a second path. p.subspace.pub is a shorthand for pages.subspace.pub.
page "dev.kdl"
page "ops.kdl"
page "my-page.kdl" name="internal" alias="int"This creates pages at pages.subspace.pub/dev/, pages.subspace.pub/ops/, and pages.subspace.pub/internal/ (also p.subspace.pub/int/). Each page is configured in its own KDL file:
title "Development"
footer "Acme Corp"
list "Repositories" {
link "GitHub" url="https://github.com/org" icon="si-github" description="Source code"
link "CI/CD" url="https://ci.example.com" icon="fa-rocket"
}Icons are embedded in the binary (no external requests). Use si-* for Simple Icons and fa-* for Font Awesome.
All configured pages and the statistics page appear in a shared navigation menu.
- Statistics — always available at
stats.subspace.pub(orstatistics.subspace.pub). Shows live metrics (connections, active, upstream health), and historical charts (connections over time, traffic by upstream, protocol breakdown). Statistics are persisted to a SQLite database and retained for one year with automatic downsampling. - Fallback — when subspace is not running, an external redirect server sends visitors to the documentation site at
https://subspace.pub/. - Error pages — DNS failures and connection errors show styled error pages instead of bare HTTP 502 responses.
The statistics page is always available at stats.subspace.pub regardless of page configuration. p.subspace.pub is a shorthand for pages.subspace.pub.
All config files (main and included) are watched for changes. When any file is modified, Subspace re-parses the entire config tree, validates it, and applies the new routing if valid. Invalid configs are rejected with a warning and the current routing stays active.
Settings that require a restart: listen, control_socket.
Starts the proxy server.
subspace serve
subspace serve --config /path/to/config.kdlShows health and status of upstream proxies. Performs a TCP health check against each upstream.
subspace status # colored terminal output
subspace status -J # raw JSON outputShows which route and upstream would handle a given URL.
$ subspace resolve https://app.corp.internal/apiStreams logs from a running server.
subspace logs # last 10 info+ lines
subspace logs -N 50 # last 50 lines
subspace logs -L error # only errors
subspace logs -L debug -F # all levels, follow live| Flag | Description | Default |
|---|---|---|
-N, --lines |
Number of historical lines | 10 |
-L, --level |
Minimum level: debug, info, warn, error |
info |
-F, --follow |
Stream live output after history | false |
Prints the version and exits.
subspace versionWhen a connection arrives, Subspace peeks at the first byte to classify the protocol:
0x05(SOCKS5) — Performs the SOCKS5 handshake to extract the target hostname and port. Routes through the same upstream selection as HTTP CONNECT, then relays bidirectionally.0x16(TLS) — Parses the ClientHello to extract the SNI hostname, reads the full record length for reliable extraction. Routes based on SNI, then tunnels the raw TLS bytes without decryption.- HTTP CONNECT — Responds with
200 Connection Establishedand relays bytes bidirectionally. - Plain HTTP — Reads the
Hostheader for routing. DetectsUpgrade: websocketfor WebSocket upgrade. Supports HTTP/1.1 keep-alive for multiple requests per connection.
All protocols share the same routing rules and upstream configuration. SOCKS5 clients (git, ssh, curl) can use the same port as HTTP proxy clients.
HTTP requests reuse upstream connections when possible. After a response is fully read, the upstream connection is returned to a per-host pool instead of being closed. The next request to the same upstream and target address gets the pooled connection, avoiding a fresh TCP + proxy handshake.
Pool defaults: 4 idle connections per host, 90 second idle timeout. The pool is drained on config reload.
CONNECT and TLS connections use bidirectional relay and are not pooled.
For tunneled connections (TLS, CONNECT, WebSocket), Subspace unwraps the buffered reader before entering the relay loop. This allows the kernel to use splice/sendfile for zero-copy data transfer between sockets.
Subspace clears proxy-related environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, ALL_PROXY) on startup. Routing is controlled exclusively by the config file.
Respects $XDG_CONFIG_HOME for the default config directory. Respects $NO_COLOR to disable colored output.
MIT