Files
iroh-forward/README.md
T
skoandClaude Opus 4.8 4257d13e88 docs: Build-Anleitung in README erweitern
Voraussetzungen (Rust-Toolchain), Release-Build, Binary-Pfad, ausführen,
cargo install und optionaler statischer musl-Build (inkl. musl-tools-Hinweis).

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

8.5 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

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