Add multi-port forwarding and functional self-hosted relay

- Multi-Port: server akzeptiert mehrere `--to NAME=ADDR`, client mehrere
  `--listen NAME=ADDR`. Pro Bi-Stream wird ein Service-Name-Vorspann gesendet,
  den der Server einem Ziel zuordnet; alle Forwards teilen eine QUIC-Connection.
  Bloßes ADDR ohne NAME ⇒ Service `default` (Backward-Compat).
- `--relay <URL>`: ersetzt den n0-Relay durch einen self-hosted iroh-relay,
  n0-DNS-Discovery bleibt erhalten. Neuer gemeinsamer Endpoint-Builder in net.rs.
- ALPN auf iroh-forward/1 erhöht (Wire-Protokoll um Service-Header erweitert).
- Stream-Protokoll (write_service/read_service) in proxy.rs.
- README: Abschnitte zu Multi-Port und Self-hosted Relay, Optionen aktualisiert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sko
2026-06-21 14:32:43 +00:00
co-authored by Claude Opus 4.8
parent ec7c96b092
commit 241e05c578
6 changed files with 242 additions and 79 deletions
+45 -10
View File
@@ -67,10 +67,11 @@ location / {
| Flag | Default | Beschreibung | | Flag | Default | Beschreibung |
| ---- | ------- | ------------ | | ---- | ------- | ------------ |
| `--to` (server) | `127.0.0.1:3000` | lokales Forward-Ziel | | `--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 | | `--allow` (server) | — | erlaubte Client-EndpointId; mehrfach angebbar. Ohne Angabe: alle erlaubt |
| `--listen` (client) | `127.0.0.1:9080` | lokaler TCP-Listen-Port | | `--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) | | `--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) | | `--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 Zusätzlicher Subcommand `keygen --key-file <PATH>`: erzeugt (falls nötig) eine Key-Datei und gibt
@@ -139,20 +140,54 @@ iroh-forward server --to 127.0.0.1:3000 --key-file node.key \
Besonders wichtig, sobald sensible Dienste (SSH, DB) statt nur eines HTTP-Upstreams getunnelt Besonders wichtig, sobald sensible Dienste (SSH, DB) statt nur eines HTTP-Upstreams getunnelt
werden. werden.
## Sicherheit & Grenzen (v1) ## 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 - **Vertrauensanker:** siehe [Schlüsselmodell](#schlüsselmodell-endpointid-vs-secretkey). Ohne
`--allow` ist die EndpointId faktisch ein Zugangs-Token → geheim halten; Key-Dateien `0600`. `--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 - **Nur TCP:** reine UDP-Protokolle werden nicht getunnelt.
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 - **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. echte Client-IP — `X-Forwarded-For` ggf. davor an der Edge setzen.
### Geplante Erweiterungen ### Geplante Erweiterungen
- Self-hosted `iroh-relay` statt n0-Relays (`--relay` funktionsfähig machen).
- Mehrere Port-Mappings pro Prozess.
- systemd-Units für beide Rollen. - systemd-Units für beide Rollen.
- Allowlist/Service-Zuordnung pro Client (welcher Client welche Services nutzen darf).
+55 -28
View File
@@ -1,55 +1,80 @@
//! Client-Rolle (Nginx-Host): lauscht auf einem lokalen TCP-Port und leitet //! Client-Rolle (Nginx-Host): lauscht auf lokalen TCP-Ports und leitet jede
//! jede Verbindung über einen QUIC-Stream zum Peer (Serving Host) weiter. //! Verbindung über einen QUIC-Stream zum Peer (Serving Host) weiter. Pro Listener
//! wird ein Service-Name mitgesendet, den der Server einem Ziel zuordnet.
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use iroh::endpoint::{presets, Connection}; use iroh::endpoint::Connection;
use iroh::{Endpoint, EndpointId}; use iroh::{Endpoint, EndpointId};
use tokio::net::TcpListener; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::task::JoinSet;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::key::load_or_create_secret_key; use crate::key::load_or_create_secret_key;
use crate::proxy::splice; use crate::net::build_endpoint;
use crate::ALPN; use crate::proxy::{splice, write_service};
use crate::{NamedAddr, ALPN};
pub async fn run( pub async fn run(
listen: String, listen: Vec<NamedAddr>,
peer: EndpointId, peer: EndpointId,
key_file: PathBuf, key_file: PathBuf,
relay: Option<String>, relay: Option<String>,
) -> Result<()> { ) -> Result<()> {
let secret_key = load_or_create_secret_key(&key_file)?; let secret_key = load_or_create_secret_key(&key_file)?;
let endpoint = build_endpoint(secret_key, relay).await?;
if relay.is_some() { // Eine wiederverwendete Iroh-Connection; QUIC multiplext alle Streams (auch
warn!("--relay wird in v1 noch nicht ausgewertet; es werden die n0-Default-Relays genutzt"); // über mehrere Listener hinweg) darüber.
}
// presets::N0 = n0-Default-Relays + DNS-Discovery + Crypto-Provider.
let endpoint = Endpoint::builder(presets::N0)
.secret_key(secret_key)
.alpns(vec![ALPN.to_vec()])
.bind()
.await
.context("Iroh-Endpoint binden")?;
let listener = TcpListener::bind(&listen)
.await
.with_context(|| format!("TCP-Listener binden: {listen}"))?;
info!(%listen, %peer, "Client läuft, leitet an Peer weiter");
// Eine wiederverwendete Iroh-Connection; QUIC multiplext alle Streams darüber.
let conn: Arc<Mutex<Option<Connection>>> = Arc::new(Mutex::new(None)); let conn: Arc<Mutex<Option<Connection>>> = Arc::new(Mutex::new(None));
// Alle Listener vorab binden, damit Bind-Fehler sofort auffallen.
let mut listeners = Vec::new();
for m in &listen {
let l = TcpListener::bind(&m.addr)
.await
.with_context(|| format!("TCP-Listener binden: {} (service {})", m.addr, m.name))?;
info!(service = %m.name, listen = %m.addr, %peer, "Listener aktiv");
listeners.push((m.name.clone(), l));
}
let mut tasks = JoinSet::new();
for (service, listener) in listeners {
let endpoint = endpoint.clone();
let conn = conn.clone();
tasks.spawn(accept_loop(endpoint, conn, peer, service, listener));
}
// Läuft normalerweise endlos; bricht ab, sobald ein Listener einen Fehler liefert.
while let Some(res) = tasks.join_next().await {
match res {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(e),
Err(e) => return Err(anyhow::anyhow!("Listener-Task abgebrochen: {e}")),
}
}
Ok(())
}
async fn accept_loop(
endpoint: Endpoint,
conn: Arc<Mutex<Option<Connection>>>,
peer: EndpointId,
service: String,
listener: TcpListener,
) -> Result<()> {
loop { loop {
let (tcp, addr) = listener.accept().await.context("TCP accept")?; let (tcp, addr) = listener.accept().await.context("TCP accept")?;
let endpoint = endpoint.clone(); let endpoint = endpoint.clone();
let conn = conn.clone(); let conn = conn.clone();
let service = service.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = forward(endpoint, conn, peer, tcp).await { if let Err(e) = forward(endpoint, conn, peer, &service, tcp).await {
warn!("Forward für {addr} fehlgeschlagen: {e:#}"); warn!(%service, "Forward für {addr} fehlgeschlagen: {e:#}");
} }
}); });
} }
@@ -59,10 +84,12 @@ async fn forward(
endpoint: Endpoint, endpoint: Endpoint,
conn: Arc<Mutex<Option<Connection>>>, conn: Arc<Mutex<Option<Connection>>>,
peer: EndpointId, peer: EndpointId,
tcp: tokio::net::TcpStream, service: &str,
tcp: TcpStream,
) -> Result<()> { ) -> Result<()> {
let connection = get_or_connect(&endpoint, &conn, peer).await?; let connection = get_or_connect(&endpoint, &conn, peer).await?;
let (send, recv) = connection.open_bi().await.context("QUIC-Stream öffnen")?; let (mut send, recv) = connection.open_bi().await.context("QUIC-Stream öffnen")?;
write_service(&mut send, service).await?;
splice(tcp, send, recv).await splice(tcp, send, recv).await
} }
+40 -13
View File
@@ -2,12 +2,13 @@
//! //!
//! Zwei Rollen: //! Zwei Rollen:
//! * `server` läuft auf dem Serving Host und leitet eingehende QUIC-Streams an //! * `server` läuft auf dem Serving Host und leitet eingehende QUIC-Streams an
//! einen lokalen TCP-Port weiter. //! lokale TCP-Ports weiter (ein oder mehrere benannte Ziele).
//! * `client` läuft auf dem Nginx-Host, lauscht auf einem lokalen TCP-Port und //! * `client` läuft auf dem Nginx-Host, lauscht auf lokalen TCP-Ports und öffnet
//! öffnet pro Verbindung einen QUIC-Stream zum Serving Host. //! pro Verbindung einen QUIC-Stream zum Serving Host.
mod client; mod client;
mod key; mod key;
mod net;
mod proxy; mod proxy;
mod server; mod server;
@@ -18,7 +19,33 @@ use clap::{Parser, Subcommand};
use iroh::EndpointId; use iroh::EndpointId;
/// ALPN-Kennung für dieses Protokoll; Server und Client müssen übereinstimmen. /// ALPN-Kennung für dieses Protokoll; Server und Client müssen übereinstimmen.
pub const ALPN: &[u8] = b"iroh-forward/0"; /// `/1`: pro Bi-Stream wird ein Service-Name-Vorspann gesendet (Multi-Port).
pub const ALPN: &[u8] = b"iroh-forward/1";
/// Ein benannter Adress-Eintrag aus der CLI: `NAME=ADDR` oder bloß `ADDR` (Name = `default`).
#[derive(Clone, Debug)]
pub struct NamedAddr {
pub name: String,
pub addr: String,
}
fn parse_named_addr(s: &str) -> Result<NamedAddr, String> {
match s.split_once('=') {
Some((name, addr)) => {
if name.is_empty() || addr.is_empty() {
return Err("Format: NAME=ADDR (z.B. web=127.0.0.1:3000)".to_string());
}
Ok(NamedAddr {
name: name.to_string(),
addr: addr.to_string(),
})
}
None => Ok(NamedAddr {
name: "default".to_string(),
addr: s.to_string(),
}),
}
}
#[derive(Parser)] #[derive(Parser)]
#[command(name = "iroh-forward", about, version)] #[command(name = "iroh-forward", about, version)]
@@ -29,33 +56,33 @@ struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Command { enum Command {
/// Serving Host: nimmt Iroh-Verbindungen an und leitet sie an einen lokalen Port weiter. /// Serving Host: nimmt Iroh-Verbindungen an und leitet sie an lokale Ziele weiter.
Server { Server {
/// Lokales Ziel, an das weitergeleitet wird. /// Forward-Ziel als `NAME=ADDR` (mehrfach für mehrere Dienste). Bloßes `ADDR` = Name `default`.
#[arg(long, default_value = "127.0.0.1:3000")] #[arg(long = "to", value_parser = parse_named_addr, default_value = "127.0.0.1:3000")]
to: String, to: Vec<NamedAddr>,
/// Datei mit dem persistierten SecretKey (bestimmt die EndpointId/Adresse). /// Datei mit dem persistierten SecretKey (bestimmt die EndpointId/Adresse).
#[arg(long, default_value = "./node.key")] #[arg(long, default_value = "./node.key")]
key_file: PathBuf, key_file: PathBuf,
/// Erlaubte Client-EndpointId (mehrfach angebbar). Ohne Angabe: alle erlaubt. /// Erlaubte Client-EndpointId (mehrfach angebbar). Ohne Angabe: alle erlaubt.
#[arg(long = "allow")] #[arg(long = "allow")]
allow: Vec<EndpointId>, allow: Vec<EndpointId>,
/// Optionale Custom-Relay-URL (v1: noch nicht ausgewertet). /// Self-hosted Relay-URL (z.B. https://relay.example.com). Ohne Angabe: n0-Default-Relays.
#[arg(long)] #[arg(long)]
relay: Option<String>, relay: Option<String>,
}, },
/// Nginx-Host: lauscht lokal auf TCP und leitet über Iroh zum Peer weiter. /// Nginx-Host: lauscht lokal auf TCP und leitet über Iroh zum Peer weiter.
Client { Client {
/// Lokale TCP-Listen-Adresse (Nginx zeigt per proxy_pass hierauf). /// Listener als `NAME=ADDR` (mehrfach). `NAME` muss einem `--to`-Namen des Servers entsprechen.
#[arg(long, default_value = "127.0.0.1:9080")] #[arg(long = "listen", value_parser = parse_named_addr, default_value = "127.0.0.1:9080")]
listen: String, listen: Vec<NamedAddr>,
/// EndpointId (Public Key) des Serving Host. /// EndpointId (Public Key) des Serving Host.
#[arg(long)] #[arg(long)]
peer: EndpointId, peer: EndpointId,
/// Datei mit dem persistierten SecretKey des Clients. /// Datei mit dem persistierten SecretKey des Clients.
#[arg(long, default_value = "./client.key")] #[arg(long, default_value = "./client.key")]
key_file: PathBuf, key_file: PathBuf,
/// Optionale Custom-Relay-URL (v1: noch nicht ausgewertet). /// Self-hosted Relay-URL (z.B. https://relay.example.com). Ohne Angabe: n0-Default-Relays.
#[arg(long)] #[arg(long)]
relay: Option<String>, relay: Option<String>,
}, },
+26
View File
@@ -0,0 +1,26 @@
//! Gemeinsamer Endpoint-Aufbau für Server und Client (inkl. optionalem Custom-Relay).
use anyhow::{Context, Result};
use iroh::endpoint::presets;
use iroh::{Endpoint, RelayMap, RelayMode, RelayUrl, SecretKey};
use crate::ALPN;
/// Baut einen Iroh-Endpoint.
///
/// Standard: `presets::N0` = n0-Default-Relays + DNS-Discovery + Crypto-Provider.
/// Mit `relay` wird der Relay-Transport durch einen self-hosted Relay ersetzt; die
/// n0-DNS-Discovery (EndpointId → aktuelle Adresse) bleibt erhalten.
pub async fn build_endpoint(secret_key: SecretKey, relay: Option<String>) -> Result<Endpoint> {
let mut builder = Endpoint::builder(presets::N0)
.secret_key(secret_key)
.alpns(vec![ALPN.to_vec()]);
if let Some(url) = relay {
let url: RelayUrl = url.parse().context("ungültige Relay-URL (erwartet z.B. https://relay.example.com)")?;
// relay_mode() ersetzt den vom Preset gesetzten Relay-Transport.
builder = builder.relay_mode(RelayMode::Custom(RelayMap::from(url)));
}
builder.bind().await.context("Iroh-Endpoint binden")
}
+34 -2
View File
@@ -1,10 +1,42 @@
//! Bidirektionales Kopieren zwischen einem TCP-Stream und einem QUIC-Bi-Stream. //! Bidirektionales Kopieren zwischen einem TCP-Stream und einem QUIC-Bi-Stream,
//! plus ein kleines Stream-Vorspann-Protokoll zur Auswahl des Ziel-Service.
use anyhow::Result; use anyhow::{bail, Context, Result};
use iroh::endpoint::{RecvStream, SendStream}; use iroh::endpoint::{RecvStream, SendStream};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream; use tokio::net::TcpStream;
/// Schreibt den Service-Namen als Stream-Vorspann: `[u8 Länge][Name-Bytes]`.
///
/// Der Client sendet das zu Beginn jedes Bi-Streams, damit der Server weiß, an
/// welches der konfigurierten Ziele weitergeleitet werden soll.
pub async fn write_service(send: &mut SendStream, name: &str) -> Result<()> {
let bytes = name.as_bytes();
if bytes.len() > u8::MAX as usize {
bail!("Service-Name zu lang (max 255 Bytes): {name}");
}
send.write_all(&[bytes.len() as u8])
.await
.context("Service-Header schreiben")?;
send.write_all(bytes)
.await
.context("Service-Name schreiben")?;
Ok(())
}
/// Liest den vom Client gesendeten Service-Namen (Gegenstück zu [`write_service`]).
pub async fn read_service(recv: &mut RecvStream) -> Result<String> {
let mut len = [0u8; 1];
recv.read_exact(&mut len)
.await
.context("Service-Header lesen")?;
let mut buf = vec![0u8; len[0] as usize];
recv.read_exact(&mut buf)
.await
.context("Service-Name lesen")?;
String::from_utf8(buf).context("Service-Name ist kein gültiges UTF-8")
}
/// Pumpt Daten in beide Richtungen, bis eine Seite EOF liefert oder ein Fehler auftritt. /// Pumpt Daten in beide Richtungen, bis eine Seite EOF liefert oder ein Fehler auftritt.
/// ///
/// `tcp` <-> (`quic_send`, `quic_recv`). Beendet beide Richtungen sauber. /// `tcp` <-> (`quic_send`, `quic_recv`). Beendet beide Richtungen sauber.
+42 -26
View File
@@ -1,44 +1,48 @@
//! Server-Rolle (Serving Host): nimmt Iroh-Verbindungen an und leitet jeden //! Server-Rolle (Serving Host): nimmt Iroh-Verbindungen an und leitet jeden
//! QUIC-Stream an einen lokalen TCP-Port weiter. //! QUIC-Stream an das per Service-Name gewählte lokale TCP-Ziel weiter.
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use iroh::endpoint::{presets, VarInt}; use iroh::endpoint::VarInt;
use iroh::{Endpoint, EndpointId}; use iroh::EndpointId;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use crate::key::load_or_create_secret_key; use crate::key::load_or_create_secret_key;
use crate::proxy::splice; use crate::net::build_endpoint;
use crate::ALPN; use crate::proxy::{read_service, splice};
use crate::NamedAddr;
/// QUIC-Close-Code, mit dem nicht erlaubte Clients abgewiesen werden. /// QUIC-Close-Code, mit dem nicht erlaubte Clients abgewiesen werden.
const CLOSE_NOT_ALLOWED: u32 = 1; const CLOSE_NOT_ALLOWED: u32 = 1;
pub async fn serve( pub async fn serve(
target: String, to: Vec<NamedAddr>,
key_file: PathBuf, key_file: PathBuf,
allow: Vec<EndpointId>, allow: Vec<EndpointId>,
relay: Option<String>, relay: Option<String>,
) -> Result<()> { ) -> Result<()> {
let secret_key = load_or_create_secret_key(&key_file)?; // Service-Name -> Ziel-Adresse; doppelte Namen sind ein Konfigurationsfehler.
let mut targets: HashMap<String, String> = HashMap::new();
if relay.is_some() { for m in to {
warn!("--relay wird in v1 noch nicht ausgewertet; es werden die n0-Default-Relays genutzt"); if targets.insert(m.name.clone(), m.addr.clone()).is_some() {
bail!("doppelter --to-Name: {}", m.name);
}
} }
// presets::N0 = n0-Default-Relays + DNS-Discovery + Crypto-Provider. let targets = Arc::new(targets);
let endpoint = Endpoint::builder(presets::N0)
.secret_key(secret_key) let secret_key = load_or_create_secret_key(&key_file)?;
.alpns(vec![ALPN.to_vec()]) let endpoint = build_endpoint(secret_key, relay).await?;
.bind()
.await
.context("Iroh-Endpoint binden")?;
let id = endpoint.id(); let id = endpoint.id();
info!(%id, %target, "Server läuft. Diesen Public Key an den Client geben:"); info!(%id, "Server läuft. Diesen Public Key an den Client geben:");
info!(" --peer {id}"); info!(" --peer {id}");
for (name, addr) in targets.iter() {
info!(service = %name, target = %addr, "Forward-Ziel");
}
let allow: Arc<[EndpointId]> = allow.into(); let allow: Arc<[EndpointId]> = allow.into();
if allow.is_empty() { if allow.is_empty() {
@@ -48,10 +52,10 @@ pub async fn serve(
} }
while let Some(incoming) = endpoint.accept().await { while let Some(incoming) = endpoint.accept().await {
let target = target.clone(); let targets = targets.clone();
let allow = allow.clone(); let allow = allow.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = handle_connection(incoming, &target, &allow).await { if let Err(e) = handle_connection(incoming, targets, &allow).await {
warn!("Verbindung beendet: {e:#}"); warn!("Verbindung beendet: {e:#}");
} }
}); });
@@ -61,7 +65,7 @@ pub async fn serve(
async fn handle_connection( async fn handle_connection(
incoming: iroh::endpoint::Incoming, incoming: iroh::endpoint::Incoming,
target: &str, targets: Arc<HashMap<String, String>>,
allow: &[EndpointId], allow: &[EndpointId],
) -> Result<()> { ) -> Result<()> {
let connection = incoming.await.context("Connection-Handshake")?; let connection = incoming.await.context("Connection-Handshake")?;
@@ -77,22 +81,34 @@ async fn handle_connection(
loop { loop {
// Ein Bi-Stream pro weitergeleiteter TCP-Verbindung. // Ein Bi-Stream pro weitergeleiteter TCP-Verbindung.
let (send, recv) = match connection.accept_bi().await { let (send, mut recv) = match connection.accept_bi().await {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
info!("Verbindung geschlossen: {e}"); info!("Verbindung geschlossen: {e}");
return Ok(()); return Ok(());
} }
}; };
let target = target.to_string(); let targets = targets.clone();
tokio::spawn(async move { tokio::spawn(async move {
match TcpStream::connect(&target).await { // Service-Name-Vorspann lesen und passendes Ziel bestimmen.
let name = match read_service(&mut recv).await {
Ok(n) => n,
Err(e) => {
warn!("Service-Header ungültig: {e:#}");
return;
}
};
let Some(target) = targets.get(&name) else {
warn!(service = %name, "unbekannter Service angefragt — Stream verworfen");
return;
};
match TcpStream::connect(target).await {
Ok(tcp) => { Ok(tcp) => {
if let Err(e) = splice(tcp, send, recv).await { if let Err(e) = splice(tcp, send, recv).await {
warn!("Stream-Forward-Fehler: {e:#}"); warn!("Stream-Forward-Fehler: {e:#}");
} }
} }
Err(e) => error!("Ziel {target} nicht erreichbar: {e}"), Err(e) => error!(service = %name, %target, "Ziel nicht erreichbar: {e}"),
} }
}); });
} }