Files
iroh-forward/README.md
T
skoandClaude Opus 4.8 e510d57239 docs: Beispiel-Upstream-IP im Mermaid-Diagramm angepasst
10.0.0.5 -> 192.168.254.111

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 04:29:55 +02:00

9.7 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.

Architektur-Vergleich

Klassischer Nginx-Reverse-Proxy

Nginx leitet per proxy_pass an eine geroutete IP des Serving Host weiter. Der Serving Host muss dafür über das Netz erreichbar sein — also feste IP bzw. ein VPN-Tunnel und ein eingehender Port. Standortwechsel, CGNAT oder dynamische IPs brechen das Setup.

flowchart LR
    Client([Client]) -->|HTTPS| OPN[OPNsense]
    OPN --> NGX[Nginx]
    subgraph NH [Nginx-Host]
        NGX
    end
    NGX -->|"proxy_pass<br/>http://192.168.254.111:3000<br/>(geroutetes Netz / VPN)"| APP
    subgraph SH ["Serving Host — feste IP / VPN + eingehender Port nötig"]
        APP[App :3000]
    end

Mit iroh-forward

Nginx zeigt auf einen lokalen Port; der iroh-forward client baut die Verbindung über das Iroh-Netz (QUIC, adressiert per Public Key) zum iroh-forward server auf. Beide Seiten verbinden sich ausgehend (über n0-Relay oder direktes Hole-Punching) — der Serving Host braucht keinen eingehenden Port und darf hinter CGNAT / mit dynamischer IP an beliebigem Standort laufen.

flowchart LR
    Client([Client]) -->|HTTPS| OPN[OPNsense]
    OPN --> NGX[Nginx]
    subgraph NH [Nginx-Host]
        NGX -->|"proxy_pass<br/>http://127.0.0.1:9080"| IFC[iroh-forward client]
    end
    IFC <-->|"QUIC / Iroh · TLS 1.3<br/>dial by Public Key<br/>n0-Relay oder Hole-Punch"| IFS
    subgraph SH ["Serving Host — kein eingehender Port · CGNAT / dyn. IP ok"]
        IFS[iroh-forward server] -->|"127.0.0.1:3000"| APP[App :3000]
    end

QUIC multiplext beliebig viele Streams über eine Connection → parallele Requests skalieren ohne Tricks.

Build

Voraussetzung: eine Rust-Toolchain (Rust 1.82+). Falls noch nicht vorhanden:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Release-Binary bauen:

cargo build --release

Das fertige Binary liegt unter ./target/release/iroh-forward. Direkt ausführen:

./target/release/iroh-forward --help
# oder ohne expliziten Build:
cargo run --release -- --help

Systemweit installieren (nach ~/.cargo/bin):

cargo install --path .

Statisches Binary fürs Deployment (optional)

Für ein weitgehend abhängigkeitsfreies Binary, das auf jedem Linux-Host ohne passende glibc läuft, gegen musl bauen. Da iroh standardmäßig ring (C-Crypto) nutzt, wird die musl-C-Toolchain benötigt:

# Debian/Ubuntu:
sudo apt install musl-tools
rustup target add x86_64-unknown-linux-musl

cargo build --release --target x86_64-unknown-linux-musl
# Binary: ./target/x86_64-unknown-linux-musl/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 Forward-Ziel als NAME=ADDR, mehrfach angebbar. Bloßes ADDR ⇒ Name default
--allow (server) erlaubte Client-EndpointId; mehrfach angebbar. Ohne Angabe: alle erlaubt
--listen (client) 127.0.0.1:9080 Listener als NAME=ADDR, mehrfach angebbar. NAME muss einem --to-Namen des Servers entsprechen
--peer (client) EndpointId des Serving Host (Pflicht)
--relay (beide) self-hosted Relay-URL (z.B. https://relay.example.com). Ohne Angabe: n0-Default-Relays
--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.

Multi-Port: mehrere Dienste über einen Tunnel

Server und Client können mehrere benannte Forwards bedienen. Jeder Client-Listener sendet seinen Service-Namen als Stream-Vorspann; der Server ordnet ihn dem passenden --to-Ziel zu. Alle Forwards laufen über eine geteilte QUIC-Connection (Multiplexing).

# Serving Host — z.B. Web-App + SSH:
iroh-forward server --key-file node.key \
  --to web=127.0.0.1:3000 \
  --to ssh=127.0.0.1:22 \
  --allow <client-id>

# Nginx-Host — lokale Ports, je einem Service zugeordnet:
iroh-forward client --peer <server-id> --key-file client.key \
  --listen web=127.0.0.1:9080 \
  --listen ssh=127.0.0.1:2222

Dann nginx → 127.0.0.1:9080 (web) und ssh -p 2222 user@127.0.0.1 (ssh). Ein Listener mit einem Service-Namen, den der Server nicht kennt, wird serverseitig verworfen.

Es funktioniert jedes TCP-Protokoll (SSH, MySQL, PostgreSQL, Redis, …). Der einfache Fall ohne NAME= nutzt automatisch den Service default auf beiden Seiten.

Self-hosted Relay

Standardmäßig werden die öffentlichen n0-Relays genutzt. Mit --relay <URL> (auf beiden Seiten) wird stattdessen ein eigener iroh-relay verwendet; die EndpointId-Discovery läuft weiterhin über n0-DNS.

iroh-forward server --relay https://relay.example.com ...
iroh-forward client --relay https://relay.example.com ...

Der Relay sieht nur verschlüsselte Bytes. Praktisch wird er meist nur fürs initiale Rendezvous und bei symmetrischem NAT gebraucht — die meisten Verbindungen laufen nach dem Hole-Punching direkt P2P.

Sicherheit & Grenzen

  • Vertrauensanker: siehe Schlüsselmodell. Ohne --allow ist die EndpointId faktisch ein Zugangs-Token → geheim halten; Key-Dateien 0600.
  • Nur TCP: reine UDP-Protokolle werden nicht getunnelt.
  • Client-IP: das Ziel sieht als Quelle immer den server-Prozess (127.0.0.1), nicht die echte Client-IP — X-Forwarded-For ggf. davor an der Edge setzen.

Geplante Erweiterungen

  • systemd-Units für beide Rollen.
  • Allowlist/Service-Zuordnung pro Client (welcher Client welche Services nutzen darf).