TCP-Forwarding über das Iroh-Netz (QUIC), adressiert per Public Key statt IP (Umsetzung von Option B aus recherche-vpn-vs-p2p-netzwerk.md). - server: nimmt Iroh-Verbindungen an, leitet QUIC-Streams an lokalen Port weiter - client: lauscht lokal auf TCP, öffnet pro Verbindung einen QUIC-Stream zum Peer - keygen: erzeugt Key-Datei und gibt EndpointId aus (Vorab-Verteilung) - Allowlist (--allow) erlaubter Client-EndpointIds - QUIC-Multiplexing, Reconnect mit Backoff, n0-Default-Relays + DNS-Discovery Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.1 KiB
iroh-forward
TCP-Forwarding über das Iroh-Netz (QUIC), adressiert per Public Key statt IP. Macht einen Serving Host standortunabhängig erreichbar (auch hinter CGNAT / dynamischer IP), ohne VPN-Daemon und ohne Änderung an OPNsense/Nginx.
Umsetzung von Option B aus recherche-vpn-vs-p2p-netzwerk.md.
[Nginx] proxy_pass http://127.0.0.1:9080
│ (lokales TCP)
▼
iroh-forward client --listen 127.0.0.1:9080 --peer <SERVER_ENDPOINT_ID>
│ QUIC / Iroh (TLS 1.3, n0-Relay oder Hole-Punch); 1 QUIC-Stream pro TCP-Verbindung
▼
iroh-forward server --to 127.0.0.1:3000
│
▼
[App :3000]
QUIC multiplext beliebig viele Streams über eine Connection → parallele Requests skalieren ohne Tricks.
Build
cargo build --release
# Binary: ./target/release/iroh-forward
Nutzung
Serving Host (Ziel)
iroh-forward server --to 127.0.0.1:3000 --key-file /etc/iroh/node.key
Beim Start wird die EndpointId (= Public Key = Adresse) geloggt:
Server läuft ... id=7f2bba1e... --peer 7f2bba1e...
Diese EndpointId einmalig out-of-band (Vault, Ansible-Secret, …) an den Nginx-Host übergeben.
Der --key-file persistiert die Identität → nach Neustart bleibt die EndpointId gleich.
Nginx-Host (Client)
iroh-forward client \
--listen 127.0.0.1:9080 \
--peer 7f2bba1e... \
--key-file /etc/iroh/client.key
Nginx-vhost:
location / {
proxy_pass http://127.0.0.1:9080;
}
Optionen
| Flag | Default | Beschreibung |
|---|---|---|
--to (server) |
127.0.0.1:3000 |
lokales Forward-Ziel |
--allow (server) |
— | erlaubte Client-EndpointId; mehrfach angebbar. Ohne Angabe: alle erlaubt |
--listen (client) |
127.0.0.1:9080 |
lokaler TCP-Listen-Port |
--peer (client) |
— | EndpointId des Serving Host (Pflicht) |
--key-file |
./node.key / ./client.key |
persistierter SecretKey (Datei wird mit 0600 angelegt) |
Zusätzlicher Subcommand keygen --key-file <PATH>: erzeugt (falls nötig) eine Key-Datei und gibt
die EndpointId aus, ohne den Dienst zu starten — siehe
Schlüssel vorab erzeugen.
Logging über RUST_LOG, z.B. RUST_LOG=iroh_forward=info,iroh=warn.
Schlüsselmodell: EndpointId vs. SecretKey
Häufiges Missverständnis: Die EndpointId ist die Adresse, nicht das Secret. Es sind zwei verschiedene Dinge (asymmetrische Kryptografie wie bei SSH):
| Was | Geheim? | Rolle | |
|---|---|---|---|
SecretKey (*.key-Datei) |
32-Byte Ed25519-Privatschlüssel | JA — niemals teilen | Beweist „ich bin dieser Endpoint" |
| EndpointId | der daraus abgeleitete Public Key | Nein — öffentlich | Adresse, an die man dialt (--peer) |
Die EndpointId ist mathematisch der Public Key zum SecretKey (SecretKey::public()). Sie zu
kennen erlaubt nicht, den Server zu imitieren — dafür braucht man die Key-Datei. Im Gegenteil:
Wenn der Client --peer <EndpointId> dialt, verifiziert QUIC/TLS kryptografisch, dass die
Gegenstelle wirklich den passenden SecretKey besitzt. Genau das ist „dial by key" und macht MITM
unmöglich. Die EndpointId darf bedenkenlos ins Config-Repo committet werden.
Warum sie sich trotzdem wie ein Secret „anfühlt": Ohne Allowlist ist die EndpointId das
einzige Tor, um sich überhaupt zu verbinden — jeder, der sie kennt, darf eine Verbindung
aufbauen. Sie ist dann kein kryptografisches Secret, aber faktisch ein Zugangs-Token. Mit
gesetzter --allow-Allowlist verliert die EndpointId diese Sonderrolle und ist nur noch eine
harmlose Adresse.
Schlüssel vorab erzeugen (Verteilung vereinfachen)
Man kann sich keine beliebige Wunsch-ID aussuchen (sie ist der Public Key des Keypairs), aber man kann die Keypairs vorab erzeugen, sodass die EndpointId schon vor dem ersten Start bekannt ist — kein „booten → Log lesen → ID kopieren" nötig:
iroh-forward keygen --key-file serving-host.key
# → gibt die EndpointId auf stdout aus, legt serving-host.key (0600) an
Dann im Provisioning:
serving-host.key(das Secret) → per Vault/Ansible-Secret nur auf den Serving Host,0600- die EndpointId (öffentlich) → direkt in die Client-Config / ins Git-Repo
Die Discovery (n0-DNS) löst die EndpointId zur jeweils aktuellen Netzwerkposition auf — es werden
nie IPs verteilt, nur einmalig die öffentliche EndpointId (wie ein gepinnter SSH-Hostkey).
keygen ist idempotent: bei vorhandener Datei wird die bestehende EndpointId ausgegeben.
Zugriffskontrolle (Allowlist)
Ohne --allow darf jeder, der die Server-EndpointId kennt, sich verbinden. Mit einer oder mehreren
--allow-Angaben akzeptiert der Server nur die genannten Client-EndpointIds; alle anderen
werden direkt nach dem Handshake abgewiesen:
# Client-Key vorab erzeugen, dessen EndpointId notieren:
iroh-forward keygen --key-file client.key # -> 33c17794...
# Server nur für diesen Client öffnen (mehrfach für mehrere Clients):
iroh-forward server --to 127.0.0.1:3000 --key-file node.key \
--allow 33c17794... --allow <weitere-client-id>
Besonders wichtig, sobald sensible Dienste (SSH, DB) statt nur eines HTTP-Upstreams getunnelt werden.
Sicherheit & Grenzen (v1)
- Vertrauensanker: siehe Schlüsselmodell. Ohne
--allowist die EndpointId faktisch ein Zugangs-Token → geheim halten; Key-Dateien0600. - Relay: es werden die öffentlichen n0-Relays genutzt (
presets::N0). Der Relay sieht nur verschlüsselte Bytes. Das--relay-Flag ist als Platzhalter vorhanden, aber noch nicht ausgewertet. - Nur TCP: reine UDP-Protokolle werden nicht getunnelt. Eine Instanz forwardet genau einen Port auf genau ein Ziel.
- Client-IP: das Ziel sieht als Quelle immer den
server-Prozess (127.0.0.1), nicht die echte Client-IP —X-Forwarded-Forggf. davor an der Edge setzen.
Geplante Erweiterungen
- Self-hosted
iroh-relaystatt n0-Relays (--relayfunktionsfähig machen). - Mehrere Port-Mappings pro Prozess.
- systemd-Units für beide Rollen.