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:
@@ -67,10 +67,11 @@ location / {
|
||||
|
||||
| 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 |
|
||||
| `--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) |
|
||||
| `--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
|
||||
@@ -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
|
||||
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
|
||||
`--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.
|
||||
- **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
|
||||
|
||||
- Self-hosted `iroh-relay` statt n0-Relays (`--relay` funktionsfähig machen).
|
||||
- Mehrere Port-Mappings pro Prozess.
|
||||
- systemd-Units für beide Rollen.
|
||||
- Allowlist/Service-Zuordnung pro Client (welcher Client welche Services nutzen darf).
|
||||
|
||||
+55
-28
@@ -1,55 +1,80 @@
|
||||
//! Client-Rolle (Nginx-Host): lauscht auf einem lokalen TCP-Port und leitet
|
||||
//! jede Verbindung über einen QUIC-Stream zum Peer (Serving Host) weiter.
|
||||
//! Client-Rolle (Nginx-Host): lauscht auf lokalen TCP-Ports und leitet jede
|
||||
//! 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::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use iroh::endpoint::{presets, Connection};
|
||||
use iroh::endpoint::Connection;
|
||||
use iroh::{Endpoint, EndpointId};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::key::load_or_create_secret_key;
|
||||
use crate::proxy::splice;
|
||||
use crate::ALPN;
|
||||
use crate::net::build_endpoint;
|
||||
use crate::proxy::{splice, write_service};
|
||||
use crate::{NamedAddr, ALPN};
|
||||
|
||||
pub async fn run(
|
||||
listen: String,
|
||||
listen: Vec<NamedAddr>,
|
||||
peer: EndpointId,
|
||||
key_file: PathBuf,
|
||||
relay: Option<String>,
|
||||
) -> Result<()> {
|
||||
let secret_key = load_or_create_secret_key(&key_file)?;
|
||||
let endpoint = build_endpoint(secret_key, relay).await?;
|
||||
|
||||
if relay.is_some() {
|
||||
warn!("--relay wird in v1 noch nicht ausgewertet; es werden die n0-Default-Relays genutzt");
|
||||
}
|
||||
// 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.
|
||||
// Eine wiederverwendete Iroh-Connection; QUIC multiplext alle Streams (auch
|
||||
// über mehrere Listener hinweg) darüber.
|
||||
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 {
|
||||
let (tcp, addr) = listener.accept().await.context("TCP accept")?;
|
||||
let endpoint = endpoint.clone();
|
||||
let conn = conn.clone();
|
||||
let service = service.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = forward(endpoint, conn, peer, tcp).await {
|
||||
warn!("Forward für {addr} fehlgeschlagen: {e:#}");
|
||||
if let Err(e) = forward(endpoint, conn, peer, &service, tcp).await {
|
||||
warn!(%service, "Forward für {addr} fehlgeschlagen: {e:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -59,10 +84,12 @@ async fn forward(
|
||||
endpoint: Endpoint,
|
||||
conn: Arc<Mutex<Option<Connection>>>,
|
||||
peer: EndpointId,
|
||||
tcp: tokio::net::TcpStream,
|
||||
service: &str,
|
||||
tcp: TcpStream,
|
||||
) -> Result<()> {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
+40
-13
@@ -2,12 +2,13 @@
|
||||
//!
|
||||
//! Zwei Rollen:
|
||||
//! * `server` läuft auf dem Serving Host und leitet eingehende QUIC-Streams an
|
||||
//! einen lokalen TCP-Port weiter.
|
||||
//! * `client` läuft auf dem Nginx-Host, lauscht auf einem lokalen TCP-Port und
|
||||
//! öffnet pro Verbindung einen QUIC-Stream zum Serving Host.
|
||||
//! lokale TCP-Ports weiter (ein oder mehrere benannte Ziele).
|
||||
//! * `client` läuft auf dem Nginx-Host, lauscht auf lokalen TCP-Ports und öffnet
|
||||
//! pro Verbindung einen QUIC-Stream zum Serving Host.
|
||||
|
||||
mod client;
|
||||
mod key;
|
||||
mod net;
|
||||
mod proxy;
|
||||
mod server;
|
||||
|
||||
@@ -18,7 +19,33 @@ use clap::{Parser, Subcommand};
|
||||
use iroh::EndpointId;
|
||||
|
||||
/// 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)]
|
||||
#[command(name = "iroh-forward", about, version)]
|
||||
@@ -29,33 +56,33 @@ struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
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 {
|
||||
/// Lokales Ziel, an das weitergeleitet wird.
|
||||
#[arg(long, default_value = "127.0.0.1:3000")]
|
||||
to: String,
|
||||
/// Forward-Ziel als `NAME=ADDR` (mehrfach für mehrere Dienste). Bloßes `ADDR` = Name `default`.
|
||||
#[arg(long = "to", value_parser = parse_named_addr, default_value = "127.0.0.1:3000")]
|
||||
to: Vec<NamedAddr>,
|
||||
/// Datei mit dem persistierten SecretKey (bestimmt die EndpointId/Adresse).
|
||||
#[arg(long, default_value = "./node.key")]
|
||||
key_file: PathBuf,
|
||||
/// Erlaubte Client-EndpointId (mehrfach angebbar). Ohne Angabe: alle erlaubt.
|
||||
#[arg(long = "allow")]
|
||||
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)]
|
||||
relay: Option<String>,
|
||||
},
|
||||
/// Nginx-Host: lauscht lokal auf TCP und leitet über Iroh zum Peer weiter.
|
||||
Client {
|
||||
/// Lokale TCP-Listen-Adresse (Nginx zeigt per proxy_pass hierauf).
|
||||
#[arg(long, default_value = "127.0.0.1:9080")]
|
||||
listen: String,
|
||||
/// Listener als `NAME=ADDR` (mehrfach). `NAME` muss einem `--to`-Namen des Servers entsprechen.
|
||||
#[arg(long = "listen", value_parser = parse_named_addr, default_value = "127.0.0.1:9080")]
|
||||
listen: Vec<NamedAddr>,
|
||||
/// EndpointId (Public Key) des Serving Host.
|
||||
#[arg(long)]
|
||||
peer: EndpointId,
|
||||
/// Datei mit dem persistierten SecretKey des Clients.
|
||||
#[arg(long, default_value = "./client.key")]
|
||||
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)]
|
||||
relay: Option<String>,
|
||||
},
|
||||
|
||||
+26
@@ -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
@@ -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 tokio::io::AsyncWriteExt;
|
||||
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.
|
||||
///
|
||||
/// `tcp` <-> (`quic_send`, `quic_recv`). Beendet beide Richtungen sauber.
|
||||
|
||||
+42
-26
@@ -1,44 +1,48 @@
|
||||
//! 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::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use iroh::endpoint::{presets, VarInt};
|
||||
use iroh::{Endpoint, EndpointId};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use iroh::endpoint::VarInt;
|
||||
use iroh::EndpointId;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::key::load_or_create_secret_key;
|
||||
use crate::proxy::splice;
|
||||
use crate::ALPN;
|
||||
use crate::net::build_endpoint;
|
||||
use crate::proxy::{read_service, splice};
|
||||
use crate::NamedAddr;
|
||||
|
||||
/// QUIC-Close-Code, mit dem nicht erlaubte Clients abgewiesen werden.
|
||||
const CLOSE_NOT_ALLOWED: u32 = 1;
|
||||
|
||||
pub async fn serve(
|
||||
target: String,
|
||||
to: Vec<NamedAddr>,
|
||||
key_file: PathBuf,
|
||||
allow: Vec<EndpointId>,
|
||||
relay: Option<String>,
|
||||
) -> Result<()> {
|
||||
let secret_key = load_or_create_secret_key(&key_file)?;
|
||||
|
||||
if relay.is_some() {
|
||||
warn!("--relay wird in v1 noch nicht ausgewertet; es werden die n0-Default-Relays genutzt");
|
||||
// Service-Name -> Ziel-Adresse; doppelte Namen sind ein Konfigurationsfehler.
|
||||
let mut targets: HashMap<String, String> = HashMap::new();
|
||||
for m in to {
|
||||
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 endpoint = Endpoint::builder(presets::N0)
|
||||
.secret_key(secret_key)
|
||||
.alpns(vec![ALPN.to_vec()])
|
||||
.bind()
|
||||
.await
|
||||
.context("Iroh-Endpoint binden")?;
|
||||
let targets = Arc::new(targets);
|
||||
|
||||
let secret_key = load_or_create_secret_key(&key_file)?;
|
||||
let endpoint = build_endpoint(secret_key, relay).await?;
|
||||
|
||||
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}");
|
||||
for (name, addr) in targets.iter() {
|
||||
info!(service = %name, target = %addr, "Forward-Ziel");
|
||||
}
|
||||
|
||||
let allow: Arc<[EndpointId]> = allow.into();
|
||||
if allow.is_empty() {
|
||||
@@ -48,10 +52,10 @@ pub async fn serve(
|
||||
}
|
||||
|
||||
while let Some(incoming) = endpoint.accept().await {
|
||||
let target = target.clone();
|
||||
let targets = targets.clone();
|
||||
let allow = allow.clone();
|
||||
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:#}");
|
||||
}
|
||||
});
|
||||
@@ -61,7 +65,7 @@ pub async fn serve(
|
||||
|
||||
async fn handle_connection(
|
||||
incoming: iroh::endpoint::Incoming,
|
||||
target: &str,
|
||||
targets: Arc<HashMap<String, String>>,
|
||||
allow: &[EndpointId],
|
||||
) -> Result<()> {
|
||||
let connection = incoming.await.context("Connection-Handshake")?;
|
||||
@@ -77,22 +81,34 @@ async fn handle_connection(
|
||||
|
||||
loop {
|
||||
// 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,
|
||||
Err(e) => {
|
||||
info!("Verbindung geschlossen: {e}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let target = target.to_string();
|
||||
let targets = targets.clone();
|
||||
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) => {
|
||||
if let Err(e) = splice(tcp, send, recv).await {
|
||||
warn!("Stream-Forward-Fehler: {e:#}");
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Ziel {target} nicht erreichbar: {e}"),
|
||||
Err(e) => error!(service = %name, %target, "Ziel nicht erreichbar: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user