pub mod addr;
pub mod engine;
mod icmp;
pub mod packet;
pub mod route;
pub mod state;
pub mod tap;
mod transport;
use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use nix::unistd::Pid;
use crate::capture::{
derive_output_paths, CaptureMetadata, CaptureMode, CapturePlan, CaptureWriters,
RootfulEgressRewrite,
};
use crate::cli::{Cli, OutputView};
use crate::dns::DnsPlan;
use crate::flow_log::FlowLogger;
use crate::namespace;
use crate::proxy::ProxyPlan;
use crate::sandbox::SandboxPolicy;
use super::types::NetworkPlan;
pub struct ChildBootstrap {
tap: Option<tap::TapHandle>,
addr_plan: addr::AddressPlan,
}
impl ChildBootstrap {
pub fn prepare(plan: &NetworkPlan) -> Result<Self> {
let addr_plan = addr::AddressPlan::from_network_plan(plan);
Ok(Self {
tap: None,
addr_plan,
})
}
pub fn namespace_bootstrap(&self) -> namespace::ChildNetworkBootstrap {
namespace::ChildNetworkBootstrap::RootlessInternal(namespace::RootlessChildBootstrap {
tap_name: self.addr_plan.tap_name.clone(),
gateway_mac: self.addr_plan.gateway_mac,
child_ipv4: self.addr_plan.child_ipv4,
gateway_ipv4: self.addr_plan.gateway_ipv4,
child_ipv6: self.addr_plan.child_ipv6,
gateway_ipv6: self.addr_plan.gateway_ipv6,
child_ipv4_prefix_len: self.addr_plan.child_ipv4_prefix_len,
child_ipv6_prefix_len: self.addr_plan.child_ipv6_prefix_len,
})
}
pub fn addr_plan(&self) -> &addr::AddressPlan {
&self.addr_plan
}
pub fn set_tap(&mut self, tap: tap::TapHandle) {
self.tap = Some(tap);
}
pub fn take_tap(&mut self) -> tap::TapHandle {
self.tap
.take()
.expect("rootless tap handle must be populated before engine startup")
}
}
pub struct NetworkContext {
engine: engine::EngineHandle,
capture_plan: Option<CapturePlan>,
}
impl NetworkContext {
pub fn shutdown(self) -> Result<()> {
self.engine.shutdown()
}
pub fn leak_detected(&self) -> bool {
self.engine.leak_detected()
}
}
pub struct RootlessSetupParams<'a> {
pub dns_plan: &'a DnsPlan,
pub child_bootstrap: &'a mut ChildBootstrap,
pub proxy_plan: Option<&'a ProxyPlan>,
}
pub fn setup(
_plan: &NetworkPlan,
_run_id: &str,
_child_pid: Pid,
cli: &Cli,
_tproxy_port: Option<u16>,
params: RootlessSetupParams<'_>,
) -> Result<NetworkContext> {
let addr_plan = params.child_bootstrap.addr_plan().clone();
let sandbox_policy = SandboxPolicy::from_cli(cli);
let dns_upstream = if sandbox_policy.offline {
None
} else {
params.dns_plan.rootless_upstream()
};
let capture_plan = discover_rootless_capture_plan(cli)?;
let capture = match (cli.output.as_deref(), cli.output_view) {
(Some(output_path), OutputView::Child) => Some(
CaptureWriters::open_child_only(
output_path,
CaptureMetadata::new(
"child",
"rootless-internal",
"isolated",
addr_plan.tap_name.clone(),
),
)
.context("failed to open the rootless tap capture output")?,
),
(Some(output_path), OutputView::Egress) => Some(
CaptureWriters::open_synthetic_egress(
output_path,
discover_rootless_egress_rewrite(&addr_plan)?,
CaptureMetadata::new("egress", "rootless-internal", "logical", "logical-egress"),
)
.context("failed to open the rootless logical-egress capture output")?,
),
(Some(_), OutputView::WireEgress) => None,
(Some(output_path), OutputView::Both) => {
let (child_output_path, egress_output_path) =
derive_output_paths(output_path, cli.output_view)?;
Some(
CaptureWriters::open_child_and_synthetic_egress(
&child_output_path,
&egress_output_path,
discover_rootless_egress_rewrite(&addr_plan)?,
CaptureMetadata::new(
"child",
"rootless-internal",
"isolated",
addr_plan.tap_name.clone(),
),
CaptureMetadata::new(
"egress",
"rootless-internal",
"logical",
"logical-egress",
),
)
.context("failed to open the rootless child and logical-egress capture outputs")?,
)
}
(None, _) => None,
};
let flow_log = cli
.flow_log
.as_deref()
.map(FlowLogger::open)
.transpose()
.context("failed to open the rootless flow log output")?;
let engine = engine::EngineHandle::start(
params.child_bootstrap.take_tap(),
addr_plan.clone(),
engine::EngineConfig {
dns_upstream,
allow_ipv6_outbound: engine::detect_ipv6_outbound(),
sandbox_policy,
proxy_upstream: params
.proxy_plan
.and_then(ProxyPlan::rootless_upstream)
.cloned(),
capture,
flow_log,
},
)
.context("failed to start the rootless-internal userspace networking engine")?;
Ok(NetworkContext {
engine,
capture_plan,
})
}
impl NetworkContext {
pub fn capture_plan(&self) -> Option<CapturePlan> {
self.capture_plan.clone()
}
}
fn discover_rootless_egress_rewrite(addr_plan: &addr::AddressPlan) -> Result<RootfulEgressRewrite> {
let host_egress_ipv4 = Some(discover_route_get_src_v4().context(
"failed to determine the rootless IPv4 egress source address from the default route",
)?);
let host_egress_ipv6 = match discover_route_get_src_v6() {
Ok(value) => value,
Err(err) => {
crate::util::debug(format!(
"could not determine a rootless IPv6 egress source address: {err:#}. IPv6 logical-egress capture may be unavailable on this host"
));
None
}
};
Ok(RootfulEgressRewrite {
child_ipv4: addr_plan.child_ipv4,
child_ipv6: addr_plan.child_ipv6,
host_egress_ipv4,
host_egress_ipv6,
})
}
fn discover_route_get_src_v4() -> Result<std::net::Ipv4Addr> {
let output =
crate::util::run_command("ip", vec!["route".into(), "get".into(), "1.1.1.1".into()])
.context("failed to inspect IPv4 route-get output")?;
parse_route_get_src_v4(&output)
}
fn discover_rootless_capture_plan(cli: &Cli) -> Result<Option<CapturePlan>> {
let Some(output_path) = cli.output.as_ref() else {
return Ok(None);
};
match cli.output_view {
OutputView::WireEgress => {
discover_rootless_wire_egress_capture_plan(output_path.clone()).map(Some)
}
_ => Ok(None),
}
}
fn discover_rootless_wire_egress_capture_plan(output_path: PathBuf) -> Result<CapturePlan> {
let interface_name = discover_route_get_dev_v4()
.context("failed to determine the rootless wire-egress interface from the default route")?;
Ok(CapturePlan::ChildOnly {
mode: CaptureMode::AfPacket {
interface_name: interface_name.clone(),
},
output_path,
metadata: CaptureMetadata::new("wire-egress", "rootless-internal", "wire", interface_name),
})
}
fn discover_route_get_dev_v4() -> Result<String> {
let output =
crate::util::run_command("ip", vec!["route".into(), "get".into(), "1.1.1.1".into()])
.context("failed to inspect IPv4 route-get output")?;
parse_route_get_dev(&output)
}
fn discover_route_get_src_v6() -> Result<Option<std::net::Ipv6Addr>> {
let output = crate::util::run_command(
"ip",
vec![
"-6".into(),
"route".into(),
"get".into(),
"2606:4700:4700::1111".into(),
],
)
.context("failed to inspect IPv6 route-get output")?;
parse_route_get_src_v6(&output).map(Some)
}
fn parse_route_get_src_v4(output: &str) -> Result<std::net::Ipv4Addr> {
let tokens: Vec<&str> = output.split_whitespace().collect();
tokens
.windows(2)
.find(|pair| pair[0] == "src")
.ok_or_else(|| anyhow::anyhow!("no `src` token found in route-get output: {output}"))?[1]
.parse::<std::net::Ipv4Addr>()
.with_context(|| {
format!("failed to parse IPv4 `src` token from route-get output: {output}")
})
}
fn parse_route_get_src_v6(output: &str) -> Result<std::net::Ipv6Addr> {
let tokens: Vec<&str> = output.split_whitespace().collect();
tokens
.windows(2)
.find(|pair| pair[0] == "src")
.ok_or_else(|| anyhow::anyhow!("no `src` token found in IPv6 route-get output: {output}"))?
[1]
.parse::<std::net::Ipv6Addr>()
.with_context(|| format!("failed to parse IPv6 `src` token from route-get output: {output}"))
}
fn parse_route_get_dev(output: &str) -> Result<String> {
let tokens: Vec<&str> = output.split_whitespace().collect();
Ok(tokens
.windows(2)
.find(|pair| pair[0] == "dev")
.ok_or_else(|| anyhow!("no `dev` token found in route-get output: {output}"))?[1]
.to_string())
}