use std::{error::Error, net::SocketAddr, path::PathBuf, time::Duration};
use clap::Parser;
use tailscale::tsnet::Server;
use tokio::task::spawn;
use tracing_subscriber::filter::LevelFilter;
#[derive(clap::Parser)]
#[command(version, about)]
struct Args {
#[arg(short = 'd', long, default_value = "tsnet_echo_state")]
dir: PathBuf,
#[arg(short = 'k', long, env = "TS_AUTH_KEY")]
auth_key: Option<String>,
#[arg(short = 'H', long, default_value = "tsnet_echo_example")]
hostname: Option<String>,
#[arg(long, env = "TS_CONTROL_URL")]
control_url: Option<url::Url>,
#[clap(short, long, default_value_t = 1234)]
listen_port: u16,
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
let args = Args::parse();
if args.listen_port == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"--listen-port must be between 1 and 65535",
)
.into());
}
let mut srv = Server::new();
srv.hostname = args.hostname;
srv.auth_key = args.auth_key;
srv.control_url = args.control_url.map(|u| u.to_string());
if !args.dir.as_os_str().is_empty() {
srv.dir = Some(args.dir);
}
srv.up(Some(Duration::from_secs(60))).await?;
let (ipv4, _ipv6) = srv.tailscale_ips().await?;
let listener = srv.listen("tcp", &format!(":{}", args.listen_port)).await?;
let listening_addr = SocketAddr::from((ipv4, args.listen_port));
tracing::info!(%listening_addr, "listening on the tailnet (press Ctrl-C to shut down)");
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
tracing::info!("Ctrl-C received; shutting down");
break;
}
accepted = listener.accept() => {
let conn = accepted?;
spawn(async move {
let remote_ep = conn.remote_addr();
tracing::info!(%remote_ep, "accepted connection");
let (mut reader, mut writer) = tokio::io::split(conn);
if let Err(e) = tokio::io::copy(&mut reader, &mut writer).await {
tracing::error!(%remote_ep, error = %e);
} else {
tracing::info!(%remote_ep, "remote hung up");
}
});
}
}
}
drop(listener);
let closed_cleanly = srv.close(Some(Duration::from_secs(5))).await;
tracing::info!(closed_cleanly, "server closed");
Ok(())
}