Ersetzt das ASCII-Diagramm durch zwei Mermaid-Diagramme: klassischer Nginx-Reverse-Proxy (Serving Host braucht geroutete IP/VPN + eingehenden Port) vs. iroh-forward (ausgehend per Public Key, kein eingehender Port, CGNAT ok). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
258 lines
9.7 KiB
Markdown
258 lines
9.7 KiB
Markdown
# 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<br/>http://10.0.0.5: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.
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
```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 <PATH>`: 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 <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:
|
|
|
|
```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 <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).
|
|
|
|
```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 <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`](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).
|