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-siliconStarten
Im einfachsten Fall ohne weitere Konfiguration:
./localproxyBeim 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:
# 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 systemStandardmäß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
- Nutzer gibt Adresse (
http://127.0.0.1:PORT) und Token im Online-Tool ein. - Das Tool prüft die Verbindung über
/ping. - Alle weiteren Anfragen laufen über
/proxy?url=...mit dem HeaderX-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
--originsind alle Origins erlaubt. Für den produktiven Einsatz immer das konkrete Tool per--origin https://deintool.example.comfreigeben. - HTTPS-Seite ruft HTTP-localhost auf — kein Problem: Browser behandeln
http://127.0.0.1als „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. /pagespart 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 mittoo_many_redirects; vorher wurde sie als Endseite geladen. Ebenso beendet seit 1.3.0 ein unbrauchbaresLocationdie Kette (bad_request, bei einem Nicht-http(s)-Zielblocked_target), statt dass die weiterleitende URL noch einmal als Endseite geholt wurde.error.errornennt seit 1.3.0 den echten Code des gescheiterten Hops (tls_error,dns_error,timeout,redirect_loop…, beitimeoutmitstatus504) statt pauschalupstream_error, undipden Server, der gescheitert ist. Seit 1.3.0 tragen die Hops zusätzlichmethod(außer bei reinen Fehler-Einträgen ohne eigene Anfrage), bei einem 3xxlocation, bei einem FehlererrorCategory(beitls_erroraucherrorType) und bei https eine Zertifikats-Kurzfassung (certCN,certExpiry,certDaysLeft);rawHeadersbeginnt 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 mitcontext.Background(), sodass 20 langsame Redirects einen Handler minutenlang am Leben halten konnten.- SSL-Details, die der Browser nicht hergibt:
/inspectund/pageliefern 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üfunghostMatch, eine Listeissues(u. a. abgelaufen, läuft innerhalb von 14 Tagen ab, Host nicht abgedeckt, selbstsigniert, schwacher Signatur-Algorithmus, RSA unter 2048 bit, fehlendes Zwischenzertifikat) sowieprotocolundcipherin OpenSSL-Schreibweise.daysLeftwird 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. Mitdiag=1(seit 1.3.0) öffnet der Proxy nach einemtls_erroreine 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 isthostMatchnullund die Probe sendet kein SNI. - HEAD, GET und
get=1: Ohnebody=1sendet/inspectupstream nur einen HEAD-Request (schneller, kein Body-Download). Lehnt der Server HEAD mit 405 oder 501 ab, fragt/inspectseit 1.3.0 per GET nach, ebenso jeder Hop von/page; der Body wird dabei ungelesen geschlossen. Mitget=1holt/inspectdie Header gleich per GET — für einen Security-Header-Check die bessere Wahl, denn manche Server, etwawww.google.com, sendenStrict-Transport-Securitynur bei GET. Welche Methode die Antwort beschreibt, steht inmethod. - HTTP/3 erkennen, ohne HTTP/3 zu sprechen:
/inspectund/pageliefern seit Version 1.1.0 ein Feldhttp3. Es beantwortet „unterstützt dieses Ziel HTTP/3?" — nicht „lief diese Anfrage über HTTP/3?". Die Antwort stammt aus demAlt-Svc-Feld der Zielantwort (RFC 7838, ALPN-Tokenh3nach RFC 9114); genau so entdecken Browser HTTP/3 auch, nämlich über TCP. Es ist also kein QUIC-Stack im Spiel. Entwurfs-Token wieh3-29werden ignoriert, das roheAlt-Svc-Feld bleibt in derheaders-Map verfügbar.protocolsagt 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 undLocationgeht 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-mbgedeckelt, Token-Vergleich in konstanter Zeit,X-Content-Type-Options: nosniffauf 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:1erreicht in einem NAT64-Netz127.0.0.1und 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
/proxytransparent durchgereicht — die Dekompression übernimmt der Browser. Bei/inspectmitbody=1und bei/pagedekomprimiert der Proxy selbst;Content-Encoding: deflateversteht 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 inbodybzw.html: Eine Kodierung, die Go nicht dekodieren kann (br,zstd,compress…), gestapelte Kodierungen (gzip, br) oder ein kaputter Stream setzendecodeError: 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ägttruncated: 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 überGOOS/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 kommengovulncheckund einegofmt-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 schlichtesgo install …@v0.8.1reicht ab Go 1.27.2 nicht: staticcheck v0.8.1 (2026.2.1) verlangt nochgolang.org/x/toolsv0.44, das Export-Daten nur bis Version 4 liest, Go 1.27.2 und neuer schreiben aber Version 5 — jeder Stdlib-Import bricht dann mitexport data version 5 is greater than maximum supported version 4ab.go install paket@versionkann 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 schlichtesgo install.