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-silicon

Start it

In the simplest case, with no further configuration:

./localproxy

On 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 system

By 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

  1. The user enters the address (http://127.0.0.1:PORT) and token in the online tool.
  2. The tool verifies the connection via /ping.
  3. All further requests go through /proxy?url=... with the X-Proxy-Token header. 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 --origin all 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.1 as 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.
  • /page saves 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 /proxy and /inspect calls. 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 on context.Background(), so 20 slow redirects could keep a handler alive for minutes.
  • SSL details the browser won't give you: /inspect returns certificate information (version, issuer, validity, SANs including IP SANs, chain) that the Fetch API can't access. Without body=1 it sends only a HEAD request upstream (faster, no body download).
  • Detect HTTP/3 without speaking HTTP/3: Since version 1.1.0 /inspect and /page return an http3 field. It answers "does this target support HTTP/3?"not "was this request made over HTTP/3?". The answer comes from the target's Alt-Svc field (RFC 7838, ALPN token h3 per RFC 9114), which is exactly how browsers discover HTTP/3 as well: over TCP. No QUIC stack is involved. Draft tokens like h3-29 are ignored, and the raw Alt-Svc field stays available in the headers map. protocol still 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: nosniff on 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:1 reaches 127.0.0.1 on 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 /inspect with body=1 the proxy decompresses itself; since 1.1.0 Content-Encoding: deflate handles 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 with GOOS/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, plus govulncheck and a gofmt check (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.

Further reading