use std::future::Future;
use pb_mapper::{
Client, ClientConfig, ConnectOptions, ConnectRequest, DirectStage, PortMapping,
PortMappingStatus, RegisterOptions, RegisterRequest, Transport, TunnelDiagnostics,
TunnelEventKind, TunnelEvents,
};
use tokio::time::Instant;
const USAGE: &str = "usage: port_mapping share|connect <key> <local-addr> [auto|always|off]";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let (Some(role), Some(key), Some(local_addr)) = (args.next(), args.next(), args.next()) else {
usage();
};
let share = match role.as_str() {
"share" => true,
"connect" => false,
_ => usage(),
};
let port_mapping = match args.next().as_deref() {
None | Some("auto") => PortMapping::Auto,
Some("always") => PortMapping::Always,
Some("off") => PortMapping::Off,
Some(_) => usage(),
};
let client = Client::new(ClientConfig {
server: std::env::var("PB_MAPPER_SERVER")?,
credential: std::env::var("MSG_HEADER_KEY")?,
keep_alive: true,
namespace: None,
})?;
if share {
let registration = client
.register_with_options(
RegisterRequest {
key,
local_addr,
transport: Transport::Tcp,
codec: false,
force_namespace: false,
},
RegisterOptions {
port_mapping,
..Default::default()
},
)
.await?;
watch(
registration.subscribe_events(),
|| registration.diagnostics(),
registration.stop(),
)
.await
} else {
let connection = client
.connect_with_options(
ConnectRequest {
key,
local_addr,
transport: Transport::Tcp,
},
ConnectOptions {
port_mapping,
..Default::default()
},
)
.await?;
watch(
connection.subscribe_events(),
|| connection.diagnostics(),
connection.stop(),
)
.await
}
}
async fn watch(
mut events: TunnelEvents,
diagnostics: impl Fn() -> TunnelDiagnostics,
stop: impl Future<Output = pb_mapper::Result<()>>,
) -> Result<(), Box<dyn std::error::Error>> {
let started = Instant::now();
let stamp = || format!("{:7.1}s", started.elapsed().as_secs_f32());
if let Some(mapping) = diagnostics().port_mapping {
println!("{} mapping {}", stamp(), describe(&mapping.current));
}
let mut stop = std::pin::pin!(stop);
let mut stopping = false;
loop {
tokio::select! {
result = tokio::signal::ctrl_c(), if !stopping => {
result?;
stopping = true;
println!("{} stopping; the lease is released first", stamp());
stop.as_mut().await?;
}
event = events.recv() => match event {
Some(Ok(event)) => match event.kind {
TunnelEventKind::PortMappingChanged { mapping } => {
println!("{} mapping {}", stamp(), describe(&mapping));
}
TunnelEventKind::DirectEstablished { setup_ms } => {
println!("{} direct open, set up in {setup_ms} ms", stamp());
}
TunnelEventKind::DirectFallback { stage: DirectStage::Punching } => {
println!("{} relay the punch did not get through", stamp());
}
TunnelEventKind::DirectFallback { stage } => {
println!("{} relay direct gave way at {}", stamp(), stage.as_str());
}
TunnelEventKind::Stopped => println!("{} stopped", stamp()),
_ => {}
},
Some(Err(lagged)) => {
eprintln!("{lagged}");
if let Some(mapping) = diagnostics().port_mapping {
println!("{} mapping {}", stamp(), describe(&mapping.current));
}
}
None => return Ok(()),
},
}
}
}
fn describe(mapping: &PortMappingStatus) -> String {
let mut line = mapping.stage.as_str().to_string();
if let Some(protocol) = mapping.protocol {
line += &format!(" via {}", protocol.as_str());
}
if let Some(gateway) = mapping.gateway {
line += &format!(", router {gateway}");
}
if let (true, Some(external)) = (mapping.active, mapping.external_addr) {
let local = mapping
.local_port
.map(|port| format!(":{port} "))
.unwrap_or_default();
line += &format!(", {local}-> {external}");
if let Some(lease) = mapping.lease_lifetime_ms {
line += &format!(" for {} s", lease / 1000);
}
}
if let Some(error) = &mapping.error {
line += &format!(" ({error})");
}
line
}
fn usage() -> ! {
eprintln!("{USAGE}");
std::process::exit(2);
}