# iroh-forward TCP-Forwarding über das [Iroh](https://www.iroh.computer/)-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. ```mermaid flowchart LR Client([Client]) -->|HTTPS| OPN[OPNsense] OPN --> NGX[Nginx] subgraph NH [Nginx-Host] NGX end NGX -->|"proxy_pass
http://10.0.0.5:3000
(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. ```mermaid flowchart LR Client([Client]) -->|HTTPS| OPN[OPNsense] OPN --> NGX[Nginx] subgraph NH [Nginx-Host] NGX -->|"proxy_pass
http://127.0.0.1:9080"| IFC[iroh-forward client] end IFC <-->|"QUIC / Iroh · TLS 1.3
dial by Public Key
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: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` Release-Binary bauen: ```bash cargo build --release ``` Das fertige Binary liegt unter `./target/release/iroh-forward`. Direkt ausführen: ```bash ./target/release/iroh-forward --help # oder ohne expliziten Build: cargo run --release -- --help ``` Systemweit installieren (nach `~/.cargo/bin`): ```bash 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: ```bash # 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) ```bash 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) ```bash iroh-forward client \ --listen 127.0.0.1:9080 \ --peer 7f2bba1e... \ --key-file /etc/iroh/client.key ``` Nginx-vhost: ```nginx 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 `: erzeugt (falls nötig) eine Key-Datei und gibt die EndpointId aus, ohne den Dienst zu starten — siehe [Schlüssel vorab erzeugen](#schlüssel-vorab-erzeugen-verteilung-vereinfachen). 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 ` 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: ```bash 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: ```bash # 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 ``` 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). ```bash # 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 # Nginx-Host — lokale Ports, je einem Service zugeordnet: iroh-forward client --peer --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 ` (auf beiden Seiten) wird stattdessen ein eigener [`iroh-relay`](https://www.iroh.computer/docs) verwendet; die EndpointId-Discovery läuft weiterhin über n0-DNS. ```bash 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](#schlüsselmodell-endpointid-vs-secretkey). 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).