localproxy — Guide & Tips
localproxy is a lightweight local HTTP proxy for browser-based tools — download, start, configuration and endpoints to fetch URLs without CORS.
localproxy is a lightweight, secure local HTTP proxy for browser-based online tools. It binds exclusively to 127.0.0.1 and lets your web applications fetch external URLs — safely bypassing the browser's CORS restrictions.
Guide
Requirements
- A pre-built binary for your platform (Linux, macOS, Windows, FreeBSD, OpenBSD, NetBSD — common architectures each)
- Alternatively, to build from source: Go 1.26+
Download the binary
Pre-built binaries for all platforms are available under Releases in the repository (e.g. localproxy-linux-amd64, localproxy-macos-apple-silicon, localproxy-windows-amd64.exe).
Make it executable (macOS / Linux)
chmod +x ./localproxy-macos-apple-silicon
# macOS: clear the Gatekeeper warning for the unsigned binary:
xattr -dr com.apple.quarantine ./localproxy-macos-apple-siliconStart it
In the simplest case, with no further configuration:
./localproxyOn startup a banner shows the key values: the address (http://127.0.0.1:PORT — a random free port without --port), the token regenerated on every start, the DNS servers in use and the allowed origins.
Configuration
--port int TCP port to listen on (0 = random free port) [default: 0]
--origin string Allowed origins, comma-separated
--timeout int Upstream request timeout in seconds [default: 30]
--max-mb int64 Maximum upstream response size in MB (0 = unlimited) [default: 50]
--dns string DNS servers, comma-separated, or "system" for OS DNS [default: "1.1.1.1,8.8.8.8"]
--allow-any-host Accept a non-localhost Host header (not recommended)
--version Print version information and exit--port, --timeout and --max-mb are validated at startup — an out-of-range value aborts with a message instead of failing later in a confusing way.
Examples:
# Production: allow only one specific tool
./localproxy --origin https://yourtool.example.com
# Custom DNS servers (Quad9 + Cloudflare)
./localproxy --dns "9.9.9.9,1.1.1.1"
# Use the operating system's DNS resolver
./localproxy --dns systemBy default localproxy resolves names via Cloudflare (1.1.1.1) and Google (8.8.8.8) — independent of the host's DNS configuration, which keeps results consistent across machines. If the --dns value omits a port, :53 is appended automatically.
--allow-any-host
By default localproxy only answers requests whose Host header names the loopback interface (127.0.0.0/8, localhost or [::1], with any port). This blocks DNS rebinding: without the check, a web page could point a hostname it controls at 127.0.0.1 and address the proxy from your browser.
You only need --allow-any-host if you reach the proxy under some other local name — through a container alias, for example. Prefer --origin over disabling the check.
Endpoints
| Endpoint | Auth | Description |
|---|---|---|
GET/POST/HEAD /proxy?url=... | yes (X-Proxy-Token) | Forward the request to the target URL (body streaming, metadata headers) |
GET /inspect?url=... | yes | Connection metadata as JSON (SSL, timing, IP, headers, http3); &body=1 includes the body |
GET /page?url=... | yes | Full page analysis: redirect chain + body + SSL + timing + http3 in one JSON |
| `OPTIONS /proxy | /inspect | /page` |
GET /ping | no | Health check (returns localproxy ok) |
GET /version | no | Version info as JSON |
Integrating it in a tool
- The user enters the address (
http://127.0.0.1:PORT) and token in the online tool. - The tool verifies the connection via
/ping. - All further requests go through
/proxy?url=...with theX-Proxy-Tokenheader. Example:
const response = await fetch(
`${PROXY_BASE}/proxy?url=${encodeURIComponent(targetUrl)}`,
{ headers: { "X-Proxy-Token": PROXY_TOKEN } }
);Every /proxy response also carries upstream metadata as headers: X-Upstream-Protocol, X-Upstream-IP, X-Upstream-Timing (e.g. dns=12;tcp=45;ssl=23;ttfb=156;total=234), X-Upstream-Content-Encoding, X-Upstream-Content-Length and — only when the body exceeded the --max-mb limit and was cut short — X-Upstream-Truncated: 1. All upstream headers are readable from JavaScript.
Errors are machine-readable: All 4xx responses carry CORS headers and a JSON body with a machine-readable code (bad_request, forbidden, blocked_target, method_not_allowed) — so a wrong token is distinguishable from an unreachable proxy. Only rejected origins get a bare 403 with no CORS headers, by design.
These headers never reach the target: Cookie, Cookie2, Authorization, Proxy-Authorization, Origin, Referer and Sec-Fetch-* are never forwarded, so the caller's credentials and identity signals do not end up at the target server. Headers you set deliberately — Accept, User-Agent, Accept-Language, custom X-* — still pass through.
Tips & Tricks
- Restrict origins in production: Without
--originall origins are allowed. For production use, always allow the specific tool via--origin https://yourtool.example.com. - HTTPS page calling HTTP localhost — no problem: Browsers treat
http://127.0.0.1as a "potentially trustworthy origin" (W3C Secure Contexts). Requests from an HTTPS page to the local proxy are not blocked as mixed content; Chrome's Private Network Access preflight is answered automatically. /pagesaves round-trips: The endpoint traces the redirect chain (up to 20 hops), downloads the body, inspects SSL and measures timing in a single JSON — replacing multiple/proxyand/inspectcalls. Since 1.1.0 it derives its whole time budget from the client's request context: if the client disconnects, the server-side work stops too. Redirect hops and encoding probes previously ran oncontext.Background(), so 20 slow redirects could keep a handler alive for minutes.- SSL details the browser won't give you:
/inspectreturns certificate information (version, issuer, validity, SANs including IP SANs, chain) that the Fetch API can't access. Withoutbody=1it sends only a HEAD request upstream (faster, no body download). - Detect HTTP/3 without speaking HTTP/3: Since version 1.1.0
/inspectand/pagereturn anhttp3field. It answers "does this target support HTTP/3?" — not "was this request made over HTTP/3?". The answer comes from the target'sAlt-Svcfield (RFC 7838, ALPN tokenh3per RFC 9114), which is exactly how browsers discover HTTP/3 as well: over TCP. No QUIC stack is involved. Draft tokens likeh3-29are ignored, and the rawAlt-Svcfield stays available in theheadersmap.protocolstill reports what this one request actually used — the combination{"protocol": "HTTP/2.0", "http3": true}is the normal case, not a contradiction. - Security defaults: 48-character session token regenerated on each start, SSRF protection, upstream TLS verification always on, redirects are not followed automatically (the client decides), response size capped via
--max-mb, constant-time token comparison,X-Content-Type-Options: nosniffon every proxy-generated response. - SSRF protection now enforced in the dialer: Since 1.1.0 the target address is checked after DNS resolution and before connect. The earlier pre-check resolved the name separately from the HTTP client, so an attacker-controlled zone with a short TTL could answer with a public address for the check and a loopback address for the connection. The pre-check remains as a fast-fail path and is bounded to 5 s. The blocklist now also covers reserved IPv4 space (
0/8,192.0.0.0/24,198.18/15, multicast,240/4) and the IPv6 forms that embed an IPv4 address — IPv4-mapped, NAT64 (64:ff9b::/96) and 6to4 (2002::/16).64:ff9b::7f00:1reaches127.0.0.1on a NAT64 network and was invisible to a v4-only check before. - Concurrency & compression: The proxy handles requests concurrently (one goroutine per connection); each upstream request uses a fresh connection for accurate timing. Brotli, gzip and deflate pass through transparently on
/proxy— the browser handles decompression. On/inspectwithbody=1the proxy decompresses itself; since 1.1.0Content-Encoding: deflatehandles both zlib-wrapped data (RFC 1950) and raw DEFLATE (RFC 1951) — the zlib variant, which a large share of servers send, previously came back empty or corrupt. - Build from source: With Go 1.26+ via
go build -o localproxy .(releases are built with Go 1.26.5); cross-compile withGOOS/GOARCH(e.g.GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o localproxy.exe .). A Git tag (git tag v1.1.0 && git push origin v1.1.0) triggers cross-platform release builds automatically via GitHub Actions. - Tests & linting:
go test -race ./...for the race detector; the project is linted with staticcheck, plusgovulncheckand agofmtcheck (CI blocks the release on any finding). Since 1.1.0 CI runs on every push and pull request, not only on tags — a broken test no longer surfaces first at release time. staticcheck is dev tooling only and never ships with the binary.