localproxy — Anleitung & Tipps

localproxy ist ein schlanker lokaler HTTP-Proxy für Browser-basierte Online-Tools — Download, Start, Konfiguration und Endpunkte, um externe URLs ohne CORS-Probleme abzurufen.

localproxy ist ein schlanker, sicherer lokaler HTTP-Proxy für Browser-basierte Online-Tools. Er bindet ausschließlich an 127.0.0.1 und erlaubt deinen Web-Anwendungen, externe URLs abzurufen — ohne an den CORS-Beschränkungen des Browsers zu scheitern.

Anleitung

Voraussetzungen

  • Eine vorgefertigte Binary für deine Plattform (Linux, macOS, Windows, FreeBSD, OpenBSD, NetBSD — jeweils gängige Architekturen)
  • Alternativ für den Build aus dem Quellcode: Go 1.27+ (öffnet in neuem Tab)

Binary herunterladen

Vorgefertigte Binaries für alle Plattformen liegen im Repository unter Releases (z. B. localproxy-linux-amd64, localproxy-macos-apple-silicon, localproxy-windows-amd64.exe).

Seit v1.2.0 werden die Binaries mit Go 1.27 gebaut und brauchen unter macOS deshalb macOS 13 Ventura oder neuer. Auf macOS 12 Monterey und älter bleibst du bei v1.1.1 (öffnet in neuem Tab) — sie ist funktional identisch und mit Go 1.26.6 gebaut. Überall sonst — auf Linux, Windows und ab macOS 13 — nimmst du mindestens v1.2.4: Sie ist mit Go 1.27.3 gebaut. Das bringt eine Sicherheitskorrektur für crypto/x509 (CVE-2026-111943): Go vertraut einer kleinen Gruppe von Zwischenzertifikaten nicht mehr, und Zertifikatsketten über eines davon gelten nicht mehr als gültig. Weil der Proxy das Zertifikat jedes Ziels prüft, endet eine solche Verbindung jetzt als tls_error; jede ältere Binary hätte sie akzeptiert. Dazu kommen die Korrekturen aus Go 1.27.2, unter anderem für crypto/tls und net/http, die seit v1.2.3 enthalten sind. Beides fehlt v1.1.1: Auf der 1.26-Linie kamen die Korrekturen erst mit Go 1.26.9 und 1.26.10, und eine neu gebaute 1.1.x-Version gibt es nicht.

Für die JPKCom Tools nimmst du v1.3.0 (gebaut mit demselben Go 1.27.3 wie v1.2.4). Deren Frontend nennt v1.3.0 als Mindestversion und zeigt bei einer älteren Binary einen Hinweis, sperrt sie aber nicht. Eine ältere Version funktioniert also weiter, nur Redirects nicht: Dort reicht /proxy den Location-Header noch an den Browser weiter, und der folgt dem 3xx selbst. Ein Redirect auf eine andere Site scheitert dann meist am CORS-Preflight („Failed to fetch"). Lässt das Ziel den Preflight aber durch, geht dabei dein X-Proxy-Token an das Ziel — diese Lücke schließt erst v1.3.0.

Ausführbar machen (macOS / Linux)

chmod +x ./localproxy-macos-apple-silicon
# macOS: Gatekeeper-Warnung der unsignierten Binary aufheben:
xattr -dr com.apple.quarantine ./localproxy-macos-apple-silicon

Starten

Im einfachsten Fall ohne weitere Konfiguration:

./localproxy

Beim Start zeigt ein Banner die wichtigen Werte an: die Adresse (http://127.0.0.1:PORT — ohne --port ein zufälliger freier Port), das bei jedem Start neu erzeugte Token, die verwendeten DNS-Server und die zugelassenen Origins.

Konfiguration

--port            int     TCP-Port (0 = zufälliger freier Port)                  [Standard: 0]
--origin          string  Erlaubte Origins, kommagetrennt
--timeout         int     Timeout für Upstream-Anfragen in Sekunden              [Standard: 30]
--max-mb          int64   Maximale Antwortgröße in MB (0 = unbegrenzt)           [Standard: 50]
--dns             string  DNS-Server, kommagetrennt, oder "system" für OS-DNS    [Standard: "1.1.1.1,8.8.8.8"]
--allow-any-host          Nicht-lokale Host-Header akzeptieren (nicht empfohlen)
--version                 Versionsinformation ausgeben und beenden

--port, --timeout und --max-mb werden beim Start validiert — ein Wert außerhalb des zulässigen Bereichs bricht mit einer Meldung ab, statt später verwirrend fehlzuschlagen.

Beispiele:

Terminal
# Produktion: nur ein bestimmtes Tool zulassen
./localproxy --origin https://deintool.example.com

# Eigene DNS-Server (Quad9 + Cloudflare)
./localproxy --dns "9.9.9.9,1.1.1.1"

# DNS-Resolver des Betriebssystems verwenden
./localproxy --dns system

Standardmäßig löst localproxy Namen über Cloudflare (1.1.1.1) und Google (8.8.8.8) auf — unabhängig von der DNS-Konfiguration des Hosts, was über Maschinen hinweg konsistente Ergebnisse liefert. Fehlt beim --dns-Wert der Port, wird :53 automatisch ergänzt.

--allow-any-host

Standardmäßig beantwortet localproxy nur Anfragen, deren Host-Header das Loopback-Interface benennt (127.0.0.0/8, localhost oder [::1], mit beliebigem Port). Das blockiert DNS-Rebinding: Ohne die Prüfung könnte eine Webseite einen von ihr kontrollierten Hostnamen auf 127.0.0.1 zeigen lassen und den Proxy aus deinem Browser heraus ansprechen.

--allow-any-host brauchst du nur, wenn du den Proxy unter einem anderen lokalen Namen erreichst — etwa über einen Container-Alias. Greif lieber zu --origin, statt die Prüfung abzuschalten.

Endpunkte

Endpunkt Auth Beschreibung
GET/POST/HEAD /proxy?url=... ja (X-Proxy-Token) Anfrage an die Ziel-URL weiterleiten (Body-Streaming, Metadaten-Header); &follow=1 (seit 1.3.0, nur GET/HEAD): der Proxy folgt Redirects selbst
GET /inspect?url=... ja Verbindungs-Metadaten als JSON (SSL, Timing, IP, Header, http3); &body=1 inkl. Body; seit 1.3.0 &get=1 (Header per GET statt HEAD) und &diag=1 (Fehler inline mit Zertifikatsdetails)
GET /page?url=... ja Vollständige Seitenanalyse: Redirect-Kette + Body + SSL + Timing + http3 in einem JSON; seit 1.3.0 &diag=1
`OPTIONS /proxy /inspect /page`
GET /ping nein Health-Check (gibt localproxy ok zurück)
GET /version nein Versionsinfo als JSON

Einbindung im Tool

  1. Nutzer gibt Adresse (http://127.0.0.1:PORT) und Token im Online-Tool ein.
  2. Das Tool prüft die Verbindung über /ping.
  3. Alle weiteren Anfragen laufen über /proxy?url=... mit dem Header X-Proxy-Token. Beispiel:
const response = await fetch(
  `${PROXY_BASE}/proxy?url=${encodeURIComponent(targetUrl)}&follow=1`,
  { headers: { "X-Proxy-Token": PROXY_TOKEN } }
);

Jede /proxy-Antwort liefert zusätzlich Upstream-Metadaten als Header: X-Upstream-Protocol, X-Upstream-IP, X-Upstream-Timing (z. B. dns=12;tcp=45;ssl=23;ttfb=156;total=234), X-Upstream-Content-Encoding, X-Upstream-Content-Length und — nur wenn der Body die --max-mb-Grenze gerissen hat und abgeschnitten wurde — X-Upstream-Truncated: 1. Seit 1.3.0 gilt das auch für einen komprimierten Body ohne Content-Length, den der Proxy vorher stillschweigend abschnitt; nur bei einem unkomprimierten Body ohne Content-Length fehlt der Header weiterhin, weil die Header schon verschickt sind, wenn die Grenze greift. Bis 1.2.4 konnte X-Upstream-IP bei Happy Eyeballs die Adresse eines unterlegenen Verbindungsversuchs nennen. Alle Upstream-Header sind per JavaScript lesbar.

Redirects: Seit 1.3.0 reicht /proxy den Location-Header des Ziels nie an den Browser weiter. fetch() wäre ihm sonst direkt zum Ziel gefolgt und hätte dabei X-Proxy-Token an einen Dritten geschickt, sobald dessen CORS-Regeln den Preflight durchließen. Ein 3xx kommt deshalb als normale Antwort an; das aufgelöste, absolute Ziel (nur http/https, ohne Userinfo) steht in X-Upstream-Location. Mit &follow=1 folgt der Proxy selbst: bei 301, 302, 303, 307 und 308, bis zu 5 Redirects, alle Hops teilen sich ein --timeout. GET und HEAD behalten ihre Methode, POST wird nie gefolgt. Jedes Ziel durchläuft die SSRF-Prüfung, ein privates endet mit 403 blocked_target. Fehlt Location, ist es unbrauchbar, kein http/https oder zeigt auf eine schon besuchte URL, kommt der 3xx unverändert zurück — ebenso der sechste. Mit follow=1 trägt die Antwort außerdem X-Upstream-Final-URL (woher der Inhalt stammt) und X-Upstream-Redirects (Anzahl der gefolgten Redirects, 0 ohne Redirect); alle anderen Metadaten-Header beschreiben dann den letzten Hop.

Fehler sind auswertbar: Alle 4xx-Antworten tragen CORS-Header und einen JSON-Body mit maschinenlesbarem Code (bad_request, forbidden, blocked_target, method_not_allowed) — ein falsches Token ist damit von einem nicht erreichbaren Proxy unterscheidbar. Nur abgelehnte Origins bekommen bewusst ein nacktes 403 ohne CORS-Header. Scheitert das Ziel, antworten /proxy und /inspect (ohne diag=1) mit 502 bzw. 504 und dns_error, tls_error, connection_error, upstream_error oder timeout; /page und /inspect?diag=1 liefern denselben Fehler stattdessen im Feld error einer HTTP-200-Antwort. Seit 1.3.0 trägt jeder Upstream-Fehler zusätzlich type, eine lesbare Klasse mit den Texten des PHP-Proxys der JPKCom Tools (z. B. Certificate expired, Could not resolve host). Ebenfalls seit 1.3.0 ist jeder Timeout ein timeout mit 504 — auch Timeouts beim Verbindungsaufbau, im TLS-Handshake, beim Warten auf die Antwort-Header und bei der DNS-Abfrage, die vorher als connection_error, upstream_error bzw. dns_error mit 502 ankamen. Seit 1.2.1 steht tls_error für jeden TLS-Fehler — nicht nur für Zertifikatsprobleme, sondern auch für gescheiterte Handshakes (Server nur mit TLS 1.0/1.1, keine gemeinsame Cipher-Suite, Alert der Gegenseite, Ziel spricht gar kein TLS). Vorher landeten diese Fälle bei upstream_error oder connection_error und waren von einem unerreichbaren Host nicht zu unterscheiden. Eine Lücke blieb bis 1.2.4: Antwortete hinter einer https://-URL ein reiner HTTP-Server, kam weiter upstream_error; seit 1.3.0 ist auch das ein tls_error (TLS protocol not supported). Ebenfalls erst seit 1.3.0 sind die genauen Zertifikatsmeldungen („TLS certificate hostname mismatch" usw.) erreichbar — vorher lautete jede „TLS certificate verification failed".

Diese Header gehen nicht ans Ziel: Cookie, Cookie2, Authorization, Proxy-Authorization, Origin, Referer und Sec-Fetch-* werden nie weitergereicht, damit Anmeldedaten und Identitätssignale des Aufrufers nicht beim Zielserver landen. Header, die du bewusst setzt — Accept, User-Agent, Accept-Language, eigene X-* — passieren weiterhin.

Tipps & Tricks

  • In Produktion Origins einschränken: Ohne --origin sind alle Origins erlaubt. Für den produktiven Einsatz immer das konkrete Tool per --origin https://deintool.example.com freigeben.
  • HTTPS-Seite ruft HTTP-localhost auf — kein Problem: Browser behandeln http://127.0.0.1 als „potentially trustworthy origin" (W3C Secure Contexts). Anfragen von einer HTTPS-Seite an den lokalen Proxy werden nicht als Mixed Content blockiert; Chromes Private-Network-Access-Preflight wird automatisch beantwortet.
  • /page spart Roundtrips: Der Endpunkt verfolgt die Redirect-Kette (bis zu 20 Redirects), lädt den Body, inspiziert SSL und misst Timings in einem einzigen JSON — er ersetzt mehrere /proxy- und /inspect-Aufrufe. Leitet die 21. Anfrage erneut weiter, stoppt die Kette seit 1.3.0 mit too_many_redirects; vorher wurde sie als Endseite geladen. Ebenso beendet seit 1.3.0 ein unbrauchbares Location die Kette (bad_request, bei einem Nicht-http(s)-Ziel blocked_target), statt dass die weiterleitende URL noch einmal als Endseite geholt wurde. error.error nennt seit 1.3.0 den echten Code des gescheiterten Hops (tls_error, dns_error, timeout, redirect_loop …, bei timeout mit status 504) statt pauschal upstream_error, und ip den Server, der gescheitert ist. Seit 1.3.0 tragen die Hops zusätzlich method (außer bei reinen Fehler-Einträgen ohne eigene Anfrage), bei einem 3xx location, bei einem Fehler errorCategory (bei tls_error auch errorType) und bei https eine Zertifikats-Kurzfassung (certCN, certExpiry, certDaysLeft); rawHeaders beginnt mit der Statuszeile (HTTP/2 200) und listet die Header nach Namen sortiert — vorher wechselte die Reihenfolge von Aufruf zu Aufruf. Seit 1.1.0 leitet er sein gesamtes Zeitbudget aus dem Request-Kontext des Clients ab: Bricht der Client ab, endet auch die Arbeit am Server. Vorher liefen Redirect-Hops und Encoding-Proben mit context.Background(), sodass 20 langsame Redirects einen Handler minutenlang am Leben halten konnten.
  • SSL-Details, die der Browser nicht hergibt: /inspect und /page liefern Zertifikatsinformationen (Version, Aussteller, Gültigkeit, SANs, Chain — inklusive IP-SANs), die über die Fetch-API nicht zugänglich sind. Seit 1.3.0 ist der Bericht (ssl) inhaltsgleich mit dem des PHP-Proxys der JPKCom Tools: die Chain (höchstens 10 Zertifikate) mit Label, Signaturprüfung, Fingerprints und PEM je Zertifikat, die Hostnamen-Prüfung hostMatch, eine Liste issues (u. a. abgelaufen, läuft innerhalb von 14 Tagen ab, Host nicht abgedeckt, selbstsigniert, schwacher Signatur-Algorithmus, RSA unter 2048 bit, fehlendes Zwischenzertifikat) sowie protocol und cipher in OpenSSL-Schreibweise. daysLeft wird seit 1.3.0 abgerundet und ist bei einem abgelaufenen Zertifikat negativ.
  • Kaputte Zertifikate untersuchen mit diag=1: Normalerweise liefert ein gescheiterter TLS-Aufbau nur den Fehler. Mit diag=1 (seit 1.3.0) öffnet der Proxy nach einem tls_error eine separate Verbindung nur für den Handshake und liest Chain, Protokoll und Cipher des Servers — auch bei abgelaufenen, selbstsignierten oder auf TLS 1.0 festhängenden Servern. Diese Probe ist die einzige Stelle ohne Zertifikatsprüfung: Sie geht an die IP, mit der die gescheiterte Anfrage schon verbunden war, durch denselben SSRF-Schutz, ist auf 10 s begrenzt und überträgt keine Anwendungsdaten. Das Urteil (verified, error, errorType) stammt weiter von der verifizierten Verbindung. Server, mit denen Go auch ohne Prüfung keinen Handshake schafft (u. a. SSLv3, RSA-Schlüssel unter 1024 bit), kann auch die Probe nicht beschreiben. Internationalisierte Hostnamen schickst du als Punycode (new URL(u).href), sonst ist hostMatch null und die Probe sendet kein SNI.
  • HEAD, GET und get=1: Ohne body=1 sendet /inspect upstream nur einen HEAD-Request (schneller, kein Body-Download). Lehnt der Server HEAD mit 405 oder 501 ab, fragt /inspect seit 1.3.0 per GET nach, ebenso jeder Hop von /page; der Body wird dabei ungelesen geschlossen. Mit get=1 holt /inspect die Header gleich per GET — für einen Security-Header-Check die bessere Wahl, denn manche Server, etwa www.google.com, senden Strict-Transport-Security nur bei GET. Welche Methode die Antwort beschreibt, steht in method.
  • HTTP/3 erkennen, ohne HTTP/3 zu sprechen: /inspect und /page liefern seit Version 1.1.0 ein Feld http3. Es beantwortet „unterstützt dieses Ziel HTTP/3?" — nicht „lief diese Anfrage über HTTP/3?". Die Antwort stammt aus dem Alt-Svc-Feld der Zielantwort (RFC 7838, ALPN-Token h3 nach RFC 9114); genau so entdecken Browser HTTP/3 auch, nämlich über TCP. Es ist also kein QUIC-Stack im Spiel. Entwurfs-Token wie h3-29 werden ignoriert, das rohe Alt-Svc-Feld bleibt in der headers-Map verfügbar. protocol sagt weiterhin, worüber diese eine Anfrage tatsächlich lief — die Kombination {"protocol": "HTTP/2.0", "http3": true} ist der Normalfall, kein Widerspruch.
  • Sicherheits-Defaults: 48-Zeichen-Session-Token bei jedem Start neu, SSRF-Schutz, TLS-Verifikation auf jeder Verbindung, die HTTP-Daten trägt (einzige Ausnahme: die Handshake-Probe von diag=1), der HTTP-Client folgt keinem Redirect von selbst und Location geht seit 1.3.0 nie an den Browser — gefolgt wird nur mit /proxy?follow=1 (max. 5) und in /page (max. 20), jeder Hop nur zu http/https und mit SSRF-Prüfung, Antwortgröße per --max-mb gedeckelt, Token-Vergleich in konstanter Zeit, X-Content-Type-Options: nosniff auf allen selbst erzeugten Antworten.
  • SSRF-Schutz greift erst im Dialer: Seit 1.1.0 wird die Zieladresse nach der DNS-Auflösung und vor dem Verbindungsaufbau geprüft. Die frühere Vorab-Prüfung löste den Namen getrennt vom HTTP-Client auf — eine vom Angreifer kontrollierte Zone mit kurzer TTL konnte der Prüfung eine öffentliche und der Verbindung eine Loopback-Adresse liefern. Die Vorab-Prüfung bleibt als schneller Abbruchpfad erhalten und ist auf 5 s begrenzt. Die Sperrliste deckt jetzt auch reservierten IPv4-Raum (0/8, 192.0.0.0/24, 198.18/15, Multicast, 240/4) und die IPv6-Formen mit eingebetteter IPv4-Adresse ab — IPv4-mapped, NAT64 (64:ff9b::/96) und 6to4 (2002::/16). 64:ff9b::7f00:1 erreicht in einem NAT64-Netz 127.0.0.1 und war einer rein v4-basierten Prüfung vorher unsichtbar.
  • Nebenläufigkeit & Kompression: Der Proxy bearbeitet Anfragen parallel (eine Goroutine pro Verbindung); jede Upstream-Anfrage nutzt eine frische Verbindung für genaues Timing. Brotli, gzip und deflate werden bei /proxy transparent durchgereicht — die Dekompression übernimmt der Browser. Bei /inspect mit body=1 und bei /page dekomprimiert der Proxy selbst; Content-Encoding: deflate versteht seit 1.1.0 sowohl zlib-verpackte Daten (RFC 1950) als auch rohes DEFLATE (RFC 1951) — vorher kam die zlib-Variante, die ein großer Teil der Server sendet, leer oder beschädigt zurück. Seit 1.3.0 landen keine Binärdaten mehr in body bzw. html: Eine Kodierung, die Go nicht dekodieren kann (br, zstd, compress …), gestapelte Kodierungen (gzip, br) oder ein kaputter Stream setzen decodeError: true, und es kommt nur, was sich dekodieren ließ — vorher standen dort die rohen komprimierten Bytes. Ein abgeschnittener Body endet seit 1.3.0 auf einer UTF-8-Zeichengrenze und trägt truncated: true.
  • Aus dem Quellcode bauen: Mit Go 1.27+ via go build -o localproxy . (die Releases werden mit Go 1.27.3 gebaut); Cross-Compiles über GOOS/GOARCH (z. B. GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o localproxy.exe .). Ein Git-Tag (git tag v1.3.0 && git push origin v1.3.0) stößt über GitHub Actions automatisch plattformübergreifende Release-Builds an.
  • Tests & Linting: go test -race ./... für den Race-Detector; das Projekt wird mit staticcheck (öffnet in neuem Tab) gelintet, dazu kommen govulncheck und eine gofmt-Prüfung (CI blockt den Release bei jedem Befund). Seit 1.1.0 läuft die CI bei jedem Push und Pull Request, nicht mehr nur auf Tags — ein kaputter Test fällt damit nicht erst zum Release auf. staticcheck ist reines Dev-Tooling und landet nie in der Binary. Lokal baust du es gepinnt in einem Wegwerf-Modul: d=$(mktemp -d) && cd "$d" && go mod init staticcheck-build && go get honnef.co/go/tools/cmd/staticcheck@v0.8.1 golang.org/x/tools@v0.51.0 && go install honnef.co/go/tools/cmd/staticcheck && cd - && rm -rf "$d". Ein schlichtes go install …@v0.8.1 reicht ab Go 1.27.2 nicht: staticcheck v0.8.1 (2026.2.1) verlangt noch golang.org/x/tools v0.44, das Export-Daten nur bis Version 4 liest, Go 1.27.2 und neuer schreiben aber Version 5 — jeder Stdlib-Import bricht dann mit export data version 5 is greater than maximum supported version 4 ab. go install paket@version kann keine Abhängigkeit überschreiben, deshalb das Wegwerf-Modul; die CI baut es genauso. Sobald ein staticcheck-Release x/tools v0.51.0 oder neuer verlangt, genügt wieder ein schlichtes go install.

Weiterführende Informationen