burrow/burrow/src/main.rs
JettChenT d1638726ca Add proxy subscription runtime support
Add daemon RPCs, Apple and GTK import flows, packet proxy runtime support, diagnostics, and BEPs for proxy subscription handling.

Redact subscription URL secrets from fetch errors before they reach logs or UI surfaces.
2026-06-01 15:23:17 +08:00

1684 lines
55 KiB
Rust

use anyhow::Result;
use clap::{Args, Parser, Subcommand};
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod control;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod daemon;
mod proxy_runtime;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod proxy_subscription;
pub(crate) mod tracing;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod wireguard;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
mod auth;
#[cfg(target_os = "linux")]
mod tor;
#[cfg(target_os = "linux")]
mod usernet;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
use daemon::{DaemonClient, DaemonCommand};
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
use crate::daemon::DaemonResponseData;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
pub mod database;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
use crate::daemon::rpc::{grpc_defs::Empty, BurrowClient};
#[derive(Parser)]
#[command(name = "Burrow")]
#[command(author = "Hack Club <team@hackclub.com>")]
#[command(version = "0.1")]
#[command(
about = "Burrow is a tool for burrowing through firewalls, built by teenagers at Hack Club.",
long_about = "Burrow is a 🚀 blazingly fast 🚀 tool designed to penetrate unnecessarily restrictive firewalls, providing teenagers worldwide with secure, less-filtered, and safe access to the internet!
It's being built by teenagers from Hack Club, in public! Check it out: https://github.com/hackclub/burrow
Spotted a bug? Please open an issue! https://github.com/hackclub/burrow/issues/new"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Start Burrow
Start(StartArgs),
/// Stop Burrow daemon
Stop,
/// Start Burrow daemon
Daemon(DaemonArgs),
/// Server Info
ServerInfo,
/// Server config
ServerConfig,
/// Reload Config
ReloadConfig(ReloadConfigArgs),
/// Authentication server
AuthServer,
/// Server Status
ServerStatus,
/// Tunnel Config
TunnelConfig,
/// Add Network
NetworkAdd(NetworkAddArgs),
/// List Networks
NetworkList,
/// Reorder Network
NetworkReorder(NetworkReorderArgs),
/// Delete Network
NetworkDelete(NetworkDeleteArgs),
/// Discover a Tailnet authority through the daemon
TailnetDiscover(TailnetDiscoverArgs),
/// Probe a Tailnet authority through the daemon
TailnetProbe(TailnetProbeArgs),
/// Send an ICMP echo probe through the active Tailnet tunnel over daemon packet streaming
TailnetPing(TailnetPingArgs),
/// Send a UDP echo probe through the active Tailnet tunnel over daemon packet streaming
TailnetUdpEcho(TailnetUdpEchoArgs),
/// Send an HTTP probe through the active proxy tunnel over daemon packet streaming
ProxyTcpProbe(ProxyTcpProbeArgs),
/// Send an HTTPS probe through the active proxy tunnel over daemon packet streaming
ProxyHttpsProbe(ProxyHttpsProbeArgs),
/// Select a proxy subscription node through the daemon
ProxySubscriptionSelect(ProxySubscriptionSelectArgs),
/// Preview proxy nodes from a subscription URL
ProxySubscriptionPreview(ProxySubscriptionPreviewArgs),
#[cfg(target_os = "linux")]
/// Run a command in an unshared Linux namespace using a Burrow backend
Exec(ExecArgs),
#[cfg(target_os = "linux")]
/// Run a command in a Linux user namespace with Tor-backed networking
TorExec(TorExecArgs),
}
#[derive(Args)]
struct ReloadConfigArgs {
#[clap(long, short)]
interface_id: String,
}
#[derive(Args)]
struct StartArgs {}
#[derive(Args)]
struct DaemonArgs {}
#[derive(Args)]
struct NetworkAddArgs {
id: i32,
network_type: i32,
payload_path: String,
}
#[derive(Args)]
struct NetworkReorderArgs {
id: i32,
index: i32,
}
#[derive(Args)]
struct NetworkDeleteArgs {
id: i32,
}
#[derive(Args)]
struct TailnetDiscoverArgs {
email: String,
}
#[derive(Args)]
struct TailnetProbeArgs {
authority: String,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
#[derive(Args)]
struct TailnetPingArgs {
remote: String,
#[arg(long, default_value = "burrow-tailnet-smoke")]
payload: String,
#[arg(long, default_value_t = 5000)]
timeout_ms: u64,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
#[derive(Args)]
struct TailnetUdpEchoArgs {
remote: String,
#[arg(long, default_value = "burrow-tailnet-smoke")]
message: String,
#[arg(long, default_value_t = 5000)]
timeout_ms: u64,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
#[derive(Args)]
struct ProxyTcpProbeArgs {
#[arg(long, default_value = "1.1.1.1:80")]
remote: String,
#[arg(long, default_value = "one.one.one.one")]
host: String,
#[arg(long)]
dns_name: Option<String>,
#[arg(long, default_value = "100.64.0.2:53")]
dns_server: String,
#[arg(long, default_value_t = 8000)]
timeout_ms: u64,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
#[derive(Args)]
struct ProxyHttpsProbeArgs {
#[arg(long, default_value = "1.1.1.1:443")]
remote: String,
#[arg(long)]
host: Option<String>,
#[arg(long)]
dns_name: Option<String>,
#[arg(long, default_value = "100.64.0.2:53")]
dns_server: String,
#[arg(long, default_value_t = 15000)]
timeout_ms: u64,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
#[derive(Args)]
struct ProxySubscriptionSelectArgs {
id: i32,
selected_ordinal: i32,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
#[derive(Args)]
struct ProxySubscriptionPreviewArgs {
url: String,
}
#[cfg(target_os = "linux")]
#[derive(Args)]
struct TorExecArgs {
payload_path: String,
#[arg(required = true, num_args = 1.., trailing_var_arg = true)]
command: Vec<String>,
}
#[cfg(target_os = "linux")]
#[derive(Args)]
struct ExecArgs {
#[arg(long, value_enum)]
backend: usernet::ExecBackendKind,
#[arg(long)]
payload: Option<String>,
#[arg(required = true, num_args = 1.., trailing_var_arg = true)]
command: Vec<String>,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_start() -> Result<()> {
let mut client = BurrowClient::from_uds().await?;
let res = client.tunnel_client.tunnel_start(Empty {}).await?;
println!("Got results! {:?}", res);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_stop() -> Result<()> {
let mut client = BurrowClient::from_uds().await?;
let res = client.tunnel_client.tunnel_stop(Empty {}).await?;
println!("Got results! {:?}", res);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_serverstatus() -> Result<()> {
let mut client = BurrowClient::from_uds().await?;
let mut res = client
.tunnel_client
.tunnel_status(Empty {})
.await?
.into_inner();
if let Some(st) = res.message().await? {
println!("Server Status: {:?}", st);
} else {
println!("Server Status is None");
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_tun_config() -> Result<()> {
let mut client = BurrowClient::from_uds().await?;
let mut res = client
.tunnel_client
.tunnel_configuration(Empty {})
.await?
.into_inner();
if let Some(config) = res.message().await? {
println!("Tunnel Config: {:?}", config);
} else {
println!("Tunnel Config is None");
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_network_add(id: i32, network_type: i32, payload_path: &str) -> Result<()> {
use tokio::{fs::File, io::AsyncReadExt};
use crate::daemon::rpc::grpc_defs::Network;
let mut file = File::open(payload_path).await?;
let mut payload = Vec::new();
file.read_to_end(&mut payload).await?;
let mut client = BurrowClient::from_uds().await?;
let network = Network {
id,
r#type: network_type,
payload,
};
let res = client.networks_client.network_add(network).await?;
println!("Network Add Response: {:?}", res);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_network_list() -> Result<()> {
let mut client = BurrowClient::from_uds().await?;
let mut res = client
.networks_client
.network_list(Empty {})
.await?
.into_inner();
while let Some(network_list) = res.message().await? {
println!("Network List: {:?}", network_list);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_network_reorder(id: i32, index: i32) -> Result<()> {
use crate::daemon::rpc::grpc_defs::NetworkReorderRequest;
let mut client = BurrowClient::from_uds().await?;
let reorder_request = NetworkReorderRequest { id, index };
let res = client
.networks_client
.network_reorder(reorder_request)
.await?;
println!("Network Reorder Response: {:?}", res);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_network_delete(id: i32) -> Result<()> {
use crate::daemon::rpc::grpc_defs::NetworkDeleteRequest;
let mut client = BurrowClient::from_uds().await?;
let delete_request = NetworkDeleteRequest { id };
let res = client
.networks_client
.network_delete(delete_request)
.await?;
println!("Network Delete Response: {:?}", res);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_tailnet_discover(email: &str) -> Result<()> {
let mut client = BurrowClient::from_uds().await?;
let response = client
.tailnet_client
.discover(crate::daemon::rpc::grpc_defs::TailnetDiscoverRequest { email: email.to_owned() })
.await?
.into_inner();
println!("Tailnet Discover Response: {:?}", response);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_tailnet_probe(authority: &str) -> Result<()> {
let mut client = BurrowClient::from_uds().await?;
let response = client
.tailnet_client
.probe(crate::daemon::rpc::grpc_defs::TailnetProbeRequest {
authority: authority.to_owned(),
})
.await?
.into_inner();
println!("Tailnet Probe Response: {:?}", response);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_tailnet_ping(remote: &str, payload: &str, timeout_ms: u64) -> Result<()> {
use std::net::IpAddr;
use anyhow::Context;
use rand::Rng;
use tokio::{
sync::mpsc,
time::{timeout, Duration},
};
use tokio_stream::wrappers::ReceiverStream;
use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket};
let remote_ip: IpAddr = remote
.parse()
.with_context(|| format!("invalid remote IP address {remote}"))?;
let message = payload.as_bytes().to_vec();
let mut client = BurrowClient::from_uds().await?;
client.tunnel_client.tunnel_start(Empty {}).await?;
let mut config_stream = client
.tunnel_client
.tunnel_configuration(Empty {})
.await?
.into_inner();
let config = config_stream
.message()
.await?
.context("tunnel configuration stream ended before yielding a config")?;
let local_ip = select_tailnet_local_ip(&config.addresses, remote_ip)?;
let identifier = rand::thread_rng().gen::<u16>();
let sequence = 1_u16;
let packet = build_icmp_echo_request(local_ip, remote_ip, identifier, sequence, &message)?;
let (outbound_tx, outbound_rx) = mpsc::channel::<TunnelPacket>(128);
let mut tunnel_packets = client
.tunnel_client
.tunnel_packets(ReceiverStream::new(outbound_rx))
.await?
.into_inner();
outbound_tx
.send(TunnelPacket { payload: packet })
.await
.context("failed to send ICMP echo probe into daemon packet stream")?;
log::debug!(
"tailnet ping probe queued from {local_ip} to {remote_ip} identifier={identifier} sequence={sequence}"
);
drop(outbound_tx);
let reply = timeout(Duration::from_millis(timeout_ms), async {
loop {
let packet = tunnel_packets
.message()
.await
.context("failed to read packet from daemon packet stream")?
.context("daemon packet stream ended before returning a reply")?;
log::debug!(
"tailnet ping received {} bytes from daemon packet stream",
packet.payload.len()
);
if let Some(reply) =
parse_icmp_echo_reply(&packet.payload, local_ip, remote_ip, identifier, sequence)?
{
break Ok::<_, anyhow::Error>(reply);
}
}
})
.await
.with_context(|| format!("timed out waiting for ICMP echo reply from {remote_ip}"))??;
println!("Tailnet Ping Source: {}", reply.source);
println!("Tailnet Ping Destination: {}", reply.destination);
println!(
"Tailnet Ping Payload: {}",
String::from_utf8_lossy(&reply.payload)
);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_tailnet_udp_echo(remote: &str, message: &str, timeout_ms: u64) -> Result<()> {
use std::net::SocketAddr;
use anyhow::{bail, Context};
use futures::{SinkExt, StreamExt};
use netstack_smoltcp::StackBuilder;
use tokio::{
sync::mpsc,
time::{timeout, Duration},
};
use tokio_stream::wrappers::ReceiverStream;
use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket};
let remote_addr: SocketAddr = remote
.parse()
.with_context(|| format!("invalid remote socket address {remote}"))?;
let mut client = BurrowClient::from_uds().await?;
client.tunnel_client.tunnel_start(Empty {}).await?;
let mut config_stream = client
.tunnel_client
.tunnel_configuration(Empty {})
.await?
.into_inner();
let config = config_stream
.message()
.await?
.context("tunnel configuration stream ended before yielding a config")?;
let local_addr = select_tailnet_local_socket(&config.addresses, remote_addr.ip())?;
let (stack, runner, udp_socket, _) = StackBuilder::default()
.enable_udp(true)
.enable_tcp(true)
.build()
.context("failed to build userspace UDP stack")?;
let runner = runner.context("userspace UDP stack runner unavailable")?;
let udp_socket = udp_socket.context("userspace UDP stack socket unavailable")?;
let (mut stack_sink, mut stack_stream) = stack.split();
let (mut udp_reader, mut udp_writer) = udp_socket.split();
let (outbound_tx, outbound_rx) = mpsc::channel::<TunnelPacket>(128);
let mut tunnel_packets = client
.tunnel_client
.tunnel_packets(ReceiverStream::new(outbound_rx))
.await?
.into_inner();
let ingress_task = tokio::spawn(async move {
loop {
match tunnel_packets.message().await? {
Some(packet) => {
log::debug!(
"tailnet udp echo received {} bytes from daemon packet stream",
packet.payload.len()
);
stack_sink
.send(packet.payload)
.await
.context("failed to feed inbound tailnet packet into userspace stack")?;
}
None => break,
}
}
Result::<()>::Ok(())
});
let egress_task = tokio::spawn(async move {
while let Some(packet) = stack_stream.next().await {
let payload = packet.context("failed to read outbound packet from userspace stack")?;
log::debug!(
"tailnet udp echo sending {} bytes into daemon packet stream",
payload.len()
);
outbound_tx
.send(TunnelPacket { payload })
.await
.context("failed to forward outbound tailnet packet to daemon")?;
}
Result::<()>::Ok(())
});
let runner_task = tokio::spawn(async move { runner.await.map_err(anyhow::Error::from) });
udp_writer
.send((message.as_bytes().to_vec(), local_addr, remote_addr))
.await
.context("failed to send UDP echo probe into userspace stack")?;
log::debug!("tailnet udp echo probe queued from {local_addr} to {remote_addr}");
let response = timeout(Duration::from_millis(timeout_ms), udp_reader.next())
.await
.with_context(|| format!("timed out waiting for UDP echo from {remote_addr}"))?
.context("userspace UDP stack ended before returning a reply")?;
let (payload, reply_source, reply_destination) = response;
let response_text = String::from_utf8_lossy(&payload);
ingress_task.abort();
egress_task.abort();
runner_task.abort();
if reply_source != remote_addr {
bail!("received UDP reply from unexpected source {reply_source}");
}
if reply_destination != local_addr {
bail!("received UDP reply for unexpected local socket {reply_destination}");
}
if payload != message.as_bytes() {
bail!("UDP echo payload mismatch");
}
println!("Tailnet UDP Echo Source: {reply_source}");
println!("Tailnet UDP Echo Destination: {reply_destination}");
println!("Tailnet UDP Echo Payload: {response_text}");
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_proxy_subscription_preview(url: &str) -> Result<()> {
let body = proxy_subscription::fetch_subscription(url).await?;
let preview = proxy_subscription::parse_subscription_body(&body, Some(url));
println!("Proxy subscription nodes: {}", preview.nodes.len());
println!("Rejected entries: {}", preview.rejected.len());
println!("Detected format: {}", preview.detected_format.as_str());
for node in preview.nodes.iter() {
println!(
"{}: {} {}:{} ({})",
node.ordinal,
node.name,
node.server,
node.port,
node.protocol.as_str()
);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn proxy_dns_probe(
dns_name: &str,
dns_server: &str,
timeout_ms: u64,
) -> Result<std::net::Ipv4Addr> {
use std::net::{IpAddr, SocketAddr};
use anyhow::{bail, Context};
use futures::{SinkExt, StreamExt};
use netstack_smoltcp::StackBuilder;
use rand::Rng;
use tokio::{
sync::mpsc,
time::{timeout, Duration},
};
use tokio_stream::wrappers::ReceiverStream;
use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket};
let dns_server_addr: SocketAddr = dns_server
.parse()
.with_context(|| format!("invalid DNS server socket address {dns_server}"))?;
let mut query = proxy_runtime::build_dns_query(dns_name, 1)?;
let query_id = rand::thread_rng().gen::<u16>();
query[0..2].copy_from_slice(&query_id.to_be_bytes());
let mut client = BurrowClient::from_uds().await?;
client.tunnel_client.tunnel_start(Empty {}).await?;
let mut config_stream = client
.tunnel_client
.tunnel_configuration(Empty {})
.await?
.into_inner();
let config = config_stream
.message()
.await?
.context("tunnel configuration stream ended before yielding a config")?;
let local_addr = select_tailnet_local_socket(&config.addresses, dns_server_addr.ip())?;
let (stack, runner, udp_socket, _) = StackBuilder::default()
.enable_udp(true)
.enable_tcp(true)
.build()
.context("failed to build userspace DNS probe stack")?;
let runner = runner.context("userspace DNS probe stack runner unavailable")?;
let udp_socket = udp_socket.context("userspace DNS probe socket unavailable")?;
let (mut stack_sink, mut stack_stream) = stack.split();
let (mut udp_reader, mut udp_writer) = udp_socket.split();
let (outbound_tx, outbound_rx) = mpsc::channel::<TunnelPacket>(128);
let mut tunnel_packets = client
.tunnel_client
.tunnel_packets(ReceiverStream::new(outbound_rx))
.await?
.into_inner();
let ingress_task = tokio::spawn(async move {
while let Some(packet) = tunnel_packets.message().await? {
stack_sink
.send(packet.payload)
.await
.context("failed to feed inbound DNS probe packet into userspace stack")?;
}
Result::<()>::Ok(())
});
let egress_task = tokio::spawn(async move {
while let Some(packet) = stack_stream.next().await {
let payload = packet.context("failed to read outbound DNS probe packet")?;
outbound_tx
.send(TunnelPacket { payload })
.await
.context("failed to forward outbound DNS probe packet to daemon")?;
}
Result::<()>::Ok(())
});
let runner_task = tokio::spawn(async move { runner.await.map_err(anyhow::Error::from) });
udp_writer
.send((query, local_addr, dns_server_addr))
.await
.context("failed to send DNS probe query")?;
let response = timeout(Duration::from_millis(timeout_ms), udp_reader.next())
.await
.with_context(|| format!("timed out waiting for DNS response from {dns_server_addr}"))?
.context("userspace DNS probe stack ended before returning a reply")?;
let (payload, reply_source, _reply_destination) = response;
ingress_task.abort();
egress_task.abort();
runner_task.abort();
if reply_source != dns_server_addr {
bail!("received DNS response from unexpected source {reply_source}");
}
for ip in proxy_runtime::parse_dns_response(&payload, query_id)? {
if let IpAddr::V4(ip) = ip {
return Ok(ip);
}
}
bail!("DNS response for {dns_name} did not include an A record")
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_proxy_tcp_probe(
remote: &str,
host: &str,
dns_name: Option<&str>,
dns_server: &str,
timeout_ms: u64,
) -> Result<()> {
use std::net::{IpAddr, SocketAddr};
use anyhow::{bail, Context};
use rand::Rng;
use tokio::{
sync::mpsc,
time::{timeout, Duration},
};
use tokio_stream::wrappers::ReceiverStream;
use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket};
let mut remote_addr: SocketAddr = remote
.parse()
.with_context(|| format!("invalid remote socket address {remote}"))?;
let mut http_host = host.to_owned();
if let Some(dns_name) = dns_name {
let resolved_ip = proxy_dns_probe(dns_name, dns_server, timeout_ms).await?;
remote_addr = SocketAddr::new(IpAddr::V4(resolved_ip), remote_addr.port());
if host == "one.one.one.one" {
http_host = dns_name.to_owned();
}
println!("Proxy TCP Probe DNS Name: {dns_name}");
println!("Proxy TCP Probe DNS Resolved: {resolved_ip}");
}
let remote_ip = match remote_addr.ip() {
IpAddr::V4(ip) => ip,
IpAddr::V6(_) => bail!("proxy tcp probe currently supports IPv4 targets only"),
};
let mut client = BurrowClient::from_uds().await?;
client.tunnel_client.tunnel_start(Empty {}).await?;
let mut config_stream = client
.tunnel_client
.tunnel_configuration(Empty {})
.await?
.into_inner();
let config = config_stream
.message()
.await?
.context("tunnel configuration stream ended before yielding a config")?;
let local_addr = select_tailnet_local_socket(&config.addresses, remote_addr.ip())?;
let local_ip = match local_addr.ip() {
IpAddr::V4(ip) => ip,
IpAddr::V6(_) => bail!("proxy tcp probe selected an IPv6 local address"),
};
let local_port = local_addr.port();
let (outbound_tx, outbound_rx) = mpsc::channel::<TunnelPacket>(128);
let mut tunnel_packets = client
.tunnel_client
.tunnel_packets(ReceiverStream::new(outbound_rx))
.await?
.into_inner();
let initial_seq = rand::thread_rng().gen::<u32>();
let syn = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
initial_seq,
0,
TCP_SYN,
&[],
);
outbound_tx
.send(TunnelPacket { payload: syn })
.await
.context("failed to send proxy tcp SYN into daemon packet stream")?;
let syn_ack = timeout(Duration::from_millis(timeout_ms), async {
loop {
let packet = tunnel_packets
.message()
.await
.context("failed to read packet from daemon packet stream")?
.context("daemon packet stream ended before SYN-ACK")?;
if let Some(segment) = parse_tcp_ipv4_segment(
&packet.payload,
local_ip,
remote_ip,
local_port,
remote_addr.port(),
)? {
if segment.flags & TCP_SYN != 0 && segment.flags & TCP_ACK != 0 {
break Ok::<_, anyhow::Error>(segment);
}
if segment.flags & TCP_RST != 0 {
bail!("proxy tcp probe received RST before SYN-ACK");
}
}
}
})
.await
.with_context(|| format!("timed out waiting for proxy tcp SYN-ACK from {remote_addr}"))??;
let mut client_seq = initial_seq.wrapping_add(1);
let mut peer_ack = syn_ack.sequence.wrapping_add(1);
let ack = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
client_seq,
peer_ack,
TCP_ACK,
&[],
);
outbound_tx
.send(TunnelPacket { payload: ack })
.await
.context("failed to send proxy tcp handshake ACK")?;
let request = format!(
"GET / HTTP/1.1\r\nHost: {http_host}\r\nConnection: close\r\nUser-Agent: burrow-proxy-tcp-probe\r\n\r\n"
)
.into_bytes();
let request_packet = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
client_seq,
peer_ack,
TCP_PSH | TCP_ACK,
&request,
);
outbound_tx
.send(TunnelPacket { payload: request_packet })
.await
.context("failed to send proxy tcp HTTP request")?;
client_seq = client_seq.wrapping_add(request.len() as u32);
let response = timeout(Duration::from_millis(timeout_ms), async {
loop {
let packet = tunnel_packets
.message()
.await
.context("failed to read packet from daemon packet stream")?
.context("daemon packet stream ended before HTTP response")?;
let Some(segment) = parse_tcp_ipv4_segment(
&packet.payload,
local_ip,
remote_ip,
local_port,
remote_addr.port(),
)?
else {
continue;
};
if segment.flags & TCP_RST != 0 {
bail!("proxy tcp probe received RST before HTTP response");
}
if !segment.payload.is_empty() {
peer_ack = segment.sequence.wrapping_add(segment.payload.len() as u32);
let ack = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
client_seq,
peer_ack,
TCP_ACK,
&[],
);
outbound_tx
.send(TunnelPacket { payload: ack })
.await
.context("failed to ACK proxy tcp response")?;
break Ok::<_, anyhow::Error>(segment.payload);
}
if segment.flags & TCP_FIN != 0 {
bail!("proxy tcp probe received FIN before HTTP response bytes");
}
}
})
.await
.with_context(|| {
format!("timed out waiting for HTTP response through proxy to {remote_addr}")
})??;
let first_line = String::from_utf8_lossy(&response)
.lines()
.next()
.unwrap_or("")
.to_owned();
println!("Proxy TCP Probe Local: {local_addr}");
println!("Proxy TCP Probe Remote: {remote_addr}");
println!("Proxy TCP Probe Response Bytes: {}", response.len());
println!("Proxy TCP Probe First Line: {first_line}");
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn open_proxy_packet_tcp_stream(
remote_addr: std::net::SocketAddr,
timeout_ms: u64,
) -> Result<(tokio::io::DuplexStream, std::net::SocketAddr)> {
use std::net::IpAddr;
use anyhow::{bail, Context};
use rand::Rng;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
sync::mpsc,
time::{timeout, Duration},
};
use tokio_stream::wrappers::ReceiverStream;
use crate::daemon::rpc::grpc_defs::{Empty, TunnelPacket};
let remote_ip = match remote_addr.ip() {
IpAddr::V4(ip) => ip,
IpAddr::V6(_) => bail!("proxy packet tcp stream currently supports IPv4 targets only"),
};
let mut client = BurrowClient::from_uds().await?;
client.tunnel_client.tunnel_start(Empty {}).await?;
let mut config_stream = client
.tunnel_client
.tunnel_configuration(Empty {})
.await?
.into_inner();
let config = config_stream
.message()
.await?
.context("tunnel configuration stream ended before yielding a config")?;
let local_addr = select_tailnet_local_socket(&config.addresses, remote_addr.ip())?;
let local_ip = match local_addr.ip() {
IpAddr::V4(ip) => ip,
IpAddr::V6(_) => bail!("proxy packet tcp stream selected an IPv6 local address"),
};
let local_port = local_addr.port();
let (outbound_tx, outbound_rx) = mpsc::channel::<TunnelPacket>(128);
let mut tunnel_packets = client
.tunnel_client
.tunnel_packets(ReceiverStream::new(outbound_rx))
.await?
.into_inner();
let initial_seq = rand::thread_rng().gen::<u32>();
let syn = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
initial_seq,
0,
TCP_SYN,
&[],
);
outbound_tx
.send(TunnelPacket { payload: syn })
.await
.context("failed to send proxy packet TCP SYN into daemon packet stream")?;
let syn_ack = timeout(Duration::from_millis(timeout_ms), async {
loop {
let packet = tunnel_packets
.message()
.await
.context("failed to read packet from daemon packet stream")?
.context("daemon packet stream ended before SYN-ACK")?;
if let Some(segment) = parse_tcp_ipv4_segment(
&packet.payload,
local_ip,
remote_ip,
local_port,
remote_addr.port(),
)? {
if segment.flags & TCP_SYN != 0 && segment.flags & TCP_ACK != 0 {
break Ok::<_, anyhow::Error>(segment);
}
if segment.flags & TCP_RST != 0 {
bail!("proxy packet TCP stream received RST before SYN-ACK");
}
}
}
})
.await
.with_context(|| format!("timed out waiting for proxy TCP SYN-ACK from {remote_addr}"))??;
let mut client_seq = initial_seq.wrapping_add(1);
let mut peer_ack = syn_ack.sequence.wrapping_add(1);
let ack = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
client_seq,
peer_ack,
TCP_ACK,
&[],
);
outbound_tx
.send(TunnelPacket { payload: ack })
.await
.context("failed to send proxy packet TCP handshake ACK")?;
let (client_stream, manager_stream) = tokio::io::duplex(128 * 1024);
let (mut app_reader, mut app_writer) = tokio::io::split(manager_stream);
tokio::spawn(async move {
let mut app_buf = vec![0u8; 8192];
loop {
tokio::select! {
read = app_reader.read(&mut app_buf) => {
let read = match read {
Ok(read) => read,
Err(error) => {
::tracing::debug!(%error, "proxy packet TCP stream app read failed");
break;
}
};
if read == 0 {
let fin = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
client_seq,
peer_ack,
TCP_FIN | TCP_ACK,
&[],
);
let _ = outbound_tx.send(TunnelPacket { payload: fin }).await;
break;
}
for chunk in app_buf[..read].chunks(1200) {
let packet = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
client_seq,
peer_ack,
TCP_PSH | TCP_ACK,
chunk,
);
if let Err(error) = outbound_tx.send(TunnelPacket { payload: packet }).await {
::tracing::debug!(%error, "proxy packet TCP stream outbound send failed");
return;
}
client_seq = client_seq.wrapping_add(chunk.len() as u32);
}
}
packet = tunnel_packets.message() => {
let packet = match packet {
Ok(Some(packet)) => packet,
Ok(None) => break,
Err(error) => {
::tracing::debug!(%error, "proxy packet TCP stream inbound read failed");
break;
}
};
let segment = match parse_tcp_ipv4_segment(
&packet.payload,
local_ip,
remote_ip,
local_port,
remote_addr.port(),
) {
Ok(Some(segment)) => segment,
Ok(None) => continue,
Err(error) => {
::tracing::debug!(%error, "proxy packet TCP stream segment parse failed");
break;
}
};
if segment.flags & TCP_RST != 0 {
::tracing::debug!("proxy packet TCP stream received RST");
break;
}
let mut ack_increment = segment.payload.len() as u32;
if segment.flags & TCP_FIN != 0 {
ack_increment = ack_increment.wrapping_add(1);
}
if ack_increment > 0 {
peer_ack = segment.sequence.wrapping_add(ack_increment);
let ack = build_tcp_ipv4_packet(
local_ip,
remote_ip,
local_port,
remote_addr.port(),
client_seq,
peer_ack,
TCP_ACK,
&[],
);
if let Err(error) = outbound_tx.send(TunnelPacket { payload: ack }).await {
::tracing::debug!(%error, "proxy packet TCP stream ACK send failed");
return;
}
}
if !segment.payload.is_empty() {
if let Err(error) = app_writer.write_all(&segment.payload).await {
::tracing::debug!(%error, "proxy packet TCP stream app write failed");
break;
}
}
if segment.flags & TCP_FIN != 0 {
let _ = app_writer.shutdown().await;
break;
}
}
}
}
});
Ok((client_stream, local_addr))
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_proxy_https_probe(
remote: &str,
host: Option<&str>,
dns_name: Option<&str>,
dns_server: &str,
timeout_ms: u64,
) -> Result<()> {
use std::{
net::{IpAddr, SocketAddr},
sync::{Arc, Once},
};
use anyhow::{bail, Context};
use rustls::{pki_types::ServerName, ClientConfig, RootCertStore};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
time::{timeout, Duration},
};
use tokio_rustls::TlsConnector;
static INSTALL_RUSTLS_PROVIDER: Once = Once::new();
INSTALL_RUSTLS_PROVIDER.call_once(|| {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
});
let mut remote_addr: SocketAddr = remote
.parse()
.with_context(|| format!("invalid remote socket address {remote}"))?;
let tls_host = host
.or(dns_name)
.context("proxy HTTPS probe requires --host or --dns-name for TLS SNI")?
.to_owned();
if let Some(dns_name) = dns_name {
let resolved_ip = proxy_dns_probe(dns_name, dns_server, timeout_ms).await?;
remote_addr = SocketAddr::new(IpAddr::V4(resolved_ip), remote_addr.port());
println!("Proxy HTTPS Probe DNS Name: {dns_name}");
println!("Proxy HTTPS Probe DNS Resolved: {resolved_ip}");
}
if !remote_addr.is_ipv4() {
bail!("proxy HTTPS probe currently supports IPv4 targets only");
}
let mut root_store = RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let mut tls_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
tls_config.alpn_protocols = vec![b"http/1.1".to_vec()];
let connector = TlsConnector::from(Arc::new(tls_config));
let server_name = ServerName::try_from(tls_host.clone())
.with_context(|| format!("invalid TLS server name {tls_host}"))?;
let (stream, local_addr) = open_proxy_packet_tcp_stream(remote_addr, timeout_ms).await?;
let mut tls_stream = timeout(
Duration::from_millis(timeout_ms),
connector.connect(server_name, stream),
)
.await
.with_context(|| format!("timed out waiting for TLS handshake with {tls_host}"))?
.with_context(|| format!("TLS handshake with {tls_host} failed"))?;
let request = format!(
"GET / HTTP/1.1\r\nHost: {tls_host}\r\nConnection: close\r\nUser-Agent: burrow-proxy-https-probe\r\n\r\n"
);
timeout(
Duration::from_millis(timeout_ms),
tls_stream.write_all(request.as_bytes()),
)
.await
.with_context(|| format!("timed out sending HTTPS request to {tls_host}"))?
.with_context(|| format!("failed to send HTTPS request to {tls_host}"))?;
let response = timeout(Duration::from_millis(timeout_ms), async {
let mut response = Vec::new();
let mut buf = [0u8; 8192];
loop {
let read = tls_stream
.read(&mut buf)
.await
.context("failed to read HTTPS response")?;
if read == 0 {
break;
}
response.extend_from_slice(&buf[..read]);
if response.iter().any(|byte| *byte == b'\n') {
break;
}
}
if response.is_empty() {
bail!("HTTPS response ended before returning bytes");
}
Ok::<_, anyhow::Error>(response)
})
.await
.with_context(|| format!("timed out waiting for HTTPS response from {tls_host}"))??;
let first_line = String::from_utf8_lossy(&response)
.lines()
.next()
.unwrap_or("")
.to_owned();
println!("Proxy HTTPS Probe Local: {local_addr}");
println!("Proxy HTTPS Probe Remote: {remote_addr}");
println!("Proxy HTTPS Probe Host: {tls_host}");
println!("Proxy HTTPS Probe Response Bytes: {}", response.len());
println!("Proxy HTTPS Probe First Line: {first_line}");
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_proxy_subscription_select(id: i32, selected_ordinal: i32) -> Result<()> {
use crate::daemon::rpc::grpc_defs::ProxySubscriptionSelectRequest;
let mut client = BurrowClient::from_uds().await?;
let response = client
.proxy_subscription_client
.select_node(ProxySubscriptionSelectRequest { id, selected_ordinal })
.await?
.into_inner();
println!(
"Proxy Subscription Selected: id={} selected_ordinal={}",
response.id, response.selected_ordinal
);
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn select_tailnet_local_ip(
addresses: &[String],
remote_ip: std::net::IpAddr,
) -> Result<std::net::IpAddr> {
use anyhow::Context;
let family_is_v4 = remote_ip.is_ipv4();
addresses
.iter()
.filter_map(|cidr| cidr.split('/').next())
.filter_map(|ip| ip.parse::<std::net::IpAddr>().ok())
.find(|ip| ip.is_ipv4() == family_is_v4)
.with_context(|| {
format!(
"no local {} tailnet address found in daemon config {:?}",
if family_is_v4 { "IPv4" } else { "IPv6" },
addresses
)
})
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn select_tailnet_local_socket(
addresses: &[String],
remote_ip: std::net::IpAddr,
) -> Result<std::net::SocketAddr> {
use rand::Rng;
let local_ip = select_tailnet_local_ip(addresses, remote_ip)?;
let port = rand::thread_rng().gen_range(40000..50000);
Ok(std::net::SocketAddr::new(local_ip, port))
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
struct IcmpEchoReply {
source: std::net::IpAddr,
destination: std::net::IpAddr,
payload: Vec<u8>,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
const TCP_FIN: u16 = 0x01;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
const TCP_SYN: u16 = 0x02;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
const TCP_RST: u16 = 0x04;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
const TCP_PSH: u16 = 0x08;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
const TCP_ACK: u16 = 0x10;
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
struct TcpSegment {
sequence: u32,
flags: u16,
payload: Vec<u8>,
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn build_icmp_echo_request(
source: std::net::IpAddr,
destination: std::net::IpAddr,
identifier: u16,
sequence: u16,
payload: &[u8],
) -> Result<Vec<u8>> {
use anyhow::bail;
let (source, destination) = match (source, destination) {
(std::net::IpAddr::V4(source), std::net::IpAddr::V4(destination)) => (source, destination),
_ => bail!("tailnet ping currently supports IPv4 only"),
};
let mut icmp = Vec::with_capacity(8 + payload.len());
icmp.push(8);
icmp.push(0);
icmp.extend_from_slice(&[0, 0]);
icmp.extend_from_slice(&identifier.to_be_bytes());
icmp.extend_from_slice(&sequence.to_be_bytes());
icmp.extend_from_slice(payload);
let icmp_checksum = internet_checksum(&icmp);
icmp[2..4].copy_from_slice(&icmp_checksum.to_be_bytes());
let total_len = 20 + icmp.len();
let mut packet = Vec::with_capacity(total_len);
packet.push(0x45);
packet.push(0);
packet.extend_from_slice(&(total_len as u16).to_be_bytes());
packet.extend_from_slice(&0u16.to_be_bytes());
packet.extend_from_slice(&0u16.to_be_bytes());
packet.push(64);
packet.push(1);
packet.extend_from_slice(&[0, 0]);
packet.extend_from_slice(&source.octets());
packet.extend_from_slice(&destination.octets());
let header_checksum = internet_checksum(&packet);
packet[10..12].copy_from_slice(&header_checksum.to_be_bytes());
packet.extend_from_slice(&icmp);
Ok(packet)
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn parse_icmp_echo_reply(
packet: &[u8],
local_ip: std::net::IpAddr,
remote_ip: std::net::IpAddr,
identifier: u16,
sequence: u16,
) -> Result<Option<IcmpEchoReply>> {
use anyhow::bail;
let (local_ip, remote_ip) = match (local_ip, remote_ip) {
(std::net::IpAddr::V4(local_ip), std::net::IpAddr::V4(remote_ip)) => (local_ip, remote_ip),
_ => bail!("tailnet ping currently supports IPv4 only"),
};
if packet.len() < 20 {
return Ok(None);
}
let version = packet[0] >> 4;
if version != 4 {
return Ok(None);
}
let ihl = (packet[0] & 0x0f) as usize * 4;
if packet.len() < ihl + 8 {
return Ok(None);
}
if packet[9] != 1 {
return Ok(None);
}
let source = std::net::Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]);
let destination = std::net::Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]);
if source != remote_ip || destination != local_ip {
return Ok(None);
}
let icmp = &packet[ihl..];
if icmp[0] != 0 || icmp[1] != 0 {
return Ok(None);
}
let reply_identifier = u16::from_be_bytes([icmp[4], icmp[5]]);
let reply_sequence = u16::from_be_bytes([icmp[6], icmp[7]]);
if reply_identifier != identifier || reply_sequence != sequence {
return Ok(None);
}
Ok(Some(IcmpEchoReply {
source: std::net::IpAddr::V4(source),
destination: std::net::IpAddr::V4(destination),
payload: icmp[8..].to_vec(),
}))
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn build_tcp_ipv4_packet(
source: std::net::Ipv4Addr,
destination: std::net::Ipv4Addr,
source_port: u16,
destination_port: u16,
sequence: u32,
ack: u32,
flags: u16,
payload: &[u8],
) -> Vec<u8> {
let tcp_len = 20 + payload.len();
let total_len = 20 + tcp_len;
let mut packet = Vec::with_capacity(total_len);
packet.push(0x45);
packet.push(0);
packet.extend_from_slice(&(total_len as u16).to_be_bytes());
packet.extend_from_slice(&0u16.to_be_bytes());
packet.extend_from_slice(&0x4000u16.to_be_bytes());
packet.push(64);
packet.push(6);
packet.extend_from_slice(&[0, 0]);
packet.extend_from_slice(&source.octets());
packet.extend_from_slice(&destination.octets());
let header_checksum = internet_checksum(&packet);
packet[10..12].copy_from_slice(&header_checksum.to_be_bytes());
let tcp_start = packet.len();
packet.extend_from_slice(&source_port.to_be_bytes());
packet.extend_from_slice(&destination_port.to_be_bytes());
packet.extend_from_slice(&sequence.to_be_bytes());
packet.extend_from_slice(&ack.to_be_bytes());
packet.push(5 << 4);
packet.push((flags & 0xff) as u8);
packet.extend_from_slice(&64240u16.to_be_bytes());
packet.extend_from_slice(&[0, 0]);
packet.extend_from_slice(&0u16.to_be_bytes());
packet.extend_from_slice(payload);
let checksum = tcp_ipv4_checksum(source, destination, &packet[tcp_start..]);
packet[tcp_start + 16..tcp_start + 18].copy_from_slice(&checksum.to_be_bytes());
packet
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn parse_tcp_ipv4_segment(
packet: &[u8],
local_ip: std::net::Ipv4Addr,
remote_ip: std::net::Ipv4Addr,
local_port: u16,
remote_port: u16,
) -> Result<Option<TcpSegment>> {
use anyhow::bail;
if packet.len() < 40 {
return Ok(None);
}
let version = packet[0] >> 4;
if version != 4 {
return Ok(None);
}
let ihl = (packet[0] & 0x0f) as usize * 4;
if packet.len() < ihl + 20 || packet[9] != 6 {
return Ok(None);
}
let source = std::net::Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]);
let destination = std::net::Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]);
if source != remote_ip || destination != local_ip {
return Ok(None);
}
let tcp = &packet[ihl..];
let source_port = u16::from_be_bytes([tcp[0], tcp[1]]);
let destination_port = u16::from_be_bytes([tcp[2], tcp[3]]);
if source_port != remote_port || destination_port != local_port {
return Ok(None);
}
let data_offset = ((tcp[12] >> 4) as usize) * 4;
if data_offset < 20 || tcp.len() < data_offset {
bail!("invalid TCP data offset in proxy tcp probe response");
}
let sequence = u32::from_be_bytes([tcp[4], tcp[5], tcp[6], tcp[7]]);
let flags = (tcp[13] as u16) & 0x3f;
Ok(Some(TcpSegment {
sequence,
flags,
payload: tcp[data_offset..].to_vec(),
}))
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn tcp_ipv4_checksum(
source: std::net::Ipv4Addr,
destination: std::net::Ipv4Addr,
tcp_segment: &[u8],
) -> u16 {
let mut pseudo = Vec::with_capacity(12 + tcp_segment.len());
pseudo.extend_from_slice(&source.octets());
pseudo.extend_from_slice(&destination.octets());
pseudo.push(0);
pseudo.push(6);
pseudo.extend_from_slice(&(tcp_segment.len() as u16).to_be_bytes());
pseudo.extend_from_slice(tcp_segment);
internet_checksum(&pseudo)
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn internet_checksum(bytes: &[u8]) -> u16 {
let mut sum = 0u32;
let mut chunks = bytes.chunks_exact(2);
for chunk in &mut chunks {
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
}
if let Some(&last) = chunks.remainder().first() {
sum += (last as u32) << 8;
}
while (sum >> 16) != 0 {
sum = (sum & 0xffff) + (sum >> 16);
}
!(sum as u16)
}
#[cfg(target_os = "linux")]
async fn try_tor_exec(payload_path: &str, command: Vec<String>) -> Result<()> {
let exit_code = usernet::run_exec(usernet::ExecInvocation {
backend: usernet::ExecBackendKind::Tor,
payload_path: Some(payload_path.into()),
command,
})
.await?;
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
#[cfg(target_os = "linux")]
async fn try_exec(
backend: usernet::ExecBackendKind,
payload: Option<String>,
command: Vec<String>,
) -> Result<()> {
let exit_code = usernet::run_exec(usernet::ExecInvocation {
backend,
payload_path: payload.map(Into::into),
command,
})
.await?;
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
fn handle_unexpected(res: Result<DaemonResponseData, String>) {
match res {
Ok(DaemonResponseData::None) => {
println!("Server not started.")
}
Ok(res) => {
println!("Unexpected Response: {:?}", res)
}
Err(e) => {
println!("Error when retrieving from server: {}", e)
}
}
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_serverinfo() -> Result<()> {
let mut client = DaemonClient::new().await?;
let res = client.send_command(DaemonCommand::ServerInfo).await?;
if let Ok(DaemonResponseData::ServerInfo(si)) = res.result {
println!("Got Result! {:?}", si);
} else {
handle_unexpected(res.result);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_serverconfig() -> Result<()> {
let mut client = DaemonClient::new().await?;
let res = client.send_command(DaemonCommand::ServerConfig).await?;
if let Ok(DaemonResponseData::ServerConfig(cfig)) = res.result {
println!("Got Result! {:?}", cfig);
} else {
handle_unexpected(res.result);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
async fn try_reloadconfig(interface_id: String) -> Result<()> {
let mut client = DaemonClient::new().await?;
let res = client
.send_command(DaemonCommand::ReloadConfig(interface_id))
.await?;
if let Ok(DaemonResponseData::ServerConfig(cfig)) = res.result {
println!("Got Result! {:?}", cfig);
} else {
handle_unexpected(res.result);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_vendor = "apple"))]
#[tokio::main]
async fn main() -> Result<()> {
tracing::initialize();
dotenv::dotenv().ok();
let cli = Cli::parse();
match &cli.command {
Commands::Start(..) => try_start().await?,
Commands::Stop => try_stop().await?,
Commands::Daemon(_) => daemon::daemon_main(None, None, None).await?,
Commands::ServerInfo => try_serverinfo().await?,
Commands::ServerConfig => try_serverconfig().await?,
Commands::ReloadConfig(args) => try_reloadconfig(args.interface_id.clone()).await?,
Commands::AuthServer => crate::auth::server::serve().await?,
Commands::ServerStatus => try_serverstatus().await?,
Commands::TunnelConfig => try_tun_config().await?,
Commands::NetworkAdd(args) => {
try_network_add(args.id, args.network_type, &args.payload_path).await?
}
Commands::NetworkList => try_network_list().await?,
Commands::NetworkReorder(args) => try_network_reorder(args.id, args.index).await?,
Commands::NetworkDelete(args) => try_network_delete(args.id).await?,
Commands::TailnetDiscover(args) => try_tailnet_discover(&args.email).await?,
Commands::TailnetProbe(args) => try_tailnet_probe(&args.authority).await?,
Commands::TailnetPing(args) => {
try_tailnet_ping(&args.remote, &args.payload, args.timeout_ms).await?
}
Commands::TailnetUdpEcho(args) => {
try_tailnet_udp_echo(&args.remote, &args.message, args.timeout_ms).await?
}
Commands::ProxyTcpProbe(args) => {
try_proxy_tcp_probe(
&args.remote,
&args.host,
args.dns_name.as_deref(),
&args.dns_server,
args.timeout_ms,
)
.await?
}
Commands::ProxyHttpsProbe(args) => {
try_proxy_https_probe(
&args.remote,
args.host.as_deref(),
args.dns_name.as_deref(),
&args.dns_server,
args.timeout_ms,
)
.await?
}
Commands::ProxySubscriptionSelect(args) => {
try_proxy_subscription_select(args.id, args.selected_ordinal).await?
}
Commands::ProxySubscriptionPreview(args) => {
try_proxy_subscription_preview(&args.url).await?
}
#[cfg(target_os = "linux")]
Commands::Exec(args) => {
try_exec(
args.backend.clone(),
args.payload.clone(),
args.command.clone(),
)
.await?
}
#[cfg(target_os = "linux")]
Commands::TorExec(args) => try_tor_exec(&args.payload_path, args.command.clone()).await?,
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_vendor = "apple")))]
pub fn main() {
eprintln!("This platform is not supported")
}