Initial commit: iroh-forward TCP-over-Iroh forwarder
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>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# 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`.
|
||||
|
||||
```
|
||||
[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
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
# Binary: ./target/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` | 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](#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.
|
||||
|
||||
## Sicherheit & Grenzen (v1)
|
||||
|
||||
- **Vertrauensanker:** siehe [Schlüsselmodell](#schlüsselmodell-endpointid-vs-secretkey). Ohne
|
||||
`--allow` ist die EndpointId faktisch ein Zugangs-Token → geheim halten; Key-Dateien `0600`.
|
||||
- **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-For` ggf. davor an der Edge setzen.
|
||||
|
||||
### Geplante Erweiterungen
|
||||
|
||||
- Self-hosted `iroh-relay` statt n0-Relays (`--relay` funktionsfähig machen).
|
||||
- Mehrere Port-Mappings pro Prozess.
|
||||
- systemd-Units für beide Rollen.
|
||||
Reference in New Issue
Block a user