Files
EasyTier/easytier/src/instance/runtime_host/tun_desktop.rs
T
KKRainbow ad795429c6 Add shared virtual NIC core with per-member routing
Share a named TUN across instances while keeping unnamed devices
dedicated. Track member addresses, routes, and MTU, and reject
overlapping destinations except the common Magic DNS host route.

Dispatch packets between the physical TUN and member tunnels. Keep
IPv4 and ordinary IPv6 source translation for mobile wrong-source
traffic. Update platform configuration and runtime host integration,
with unit and network namespace coverage for routing and lifecycle.

Use smoltcp for packet parsing and checksum updates. Keep core CI
matrix jobs independent.

Reserve native inbound flows before source translations allocate their
wire tuples. Keep active reservations under table pressure and leave
forwarded subnet traffic outside the local flow table.

Complete interface claim updates on the shared runtime and retain member
registrations until the transaction commits. Keep address, route, and
MTU cleanup ordered after cancelled callers, including device
initialization.

Project mobile VPN addresses, routes, and DNS from admitted native
members. Prefer unchanged attached claims and report conflicting members
individually. Apply each member's complete initial claims before opening
the shared fd, so conflicts leave healthy members usable.

Cover inbound IPv4/IPv6 collisions, cancellation during native changes
and dispatcher acknowledgments, admission priority, and atomic mobile
claim rejection with regression tests.

Use scoped TestNetwork fixtures for shared TUN bridge setup and cleanup.
Resolve public IPv6 namespace aliases through the lab's actual names so
the new tests remain isolated under parallel upstream test runs.
2026-10-13 10:23:08 +08:00

292 lines
9.5 KiB
Rust

use std::{sync::Arc, time::Duration};
use anyhow::Context as _;
use cidr::Ipv4Inet;
use easytier_core::{
gateway::dhcp::{DhcpIpv4ApplyOutcome, DhcpIpv4ApplyPermit, DhcpIpv4Host},
host::packet::HostPacketReceiver,
instance::CorePacketPlane,
};
use futures::FutureExt as _;
use tokio::{
sync::{Mutex, Notify, oneshot},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use super::{
MagicDnsRuntime,
tun_common::{TunNicState, create_nic_ctx},
};
use crate::{
common::{
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
},
instance::shared_virtual_nic::ArcSharedVirtualNicRegistry,
};
pub(super) struct NativeTunRuntime {
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
nic: TunNicState,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
static_ip_task: Mutex<Option<JoinHandle<()>>>,
}
impl NativeTunRuntime {
pub(super) fn new(
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
) -> Self {
Self {
global_ctx,
cancel,
nic: TunNicState::empty(),
shared_virtual_nic_registry,
static_ip_task: Mutex::new(None),
}
}
pub(super) fn install_packet_receiver(
&self,
receiver: HostPacketReceiver,
) -> anyhow::Result<()> {
self.nic.install_receiver(receiver)
}
fn report_static_ip_cancelled(output: &mut Option<oneshot::Sender<Result<(), Error>>>) {
if let Some(output) = output.take() {
let _ = output.send(Err(anyhow::anyhow!(
"instance is closing; static IP setup cancelled"
)
.into()));
}
}
async fn start_static_ip(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
let ipv4 = self.global_ctx.get_ipv4();
let ipv6 = self.global_ctx.get_ipv6();
if ipv4.is_none() && ipv6.is_none() {
return Ok(());
}
let nic_state = self.nic.clone();
let cancel = self.cancel.clone();
let global_ctx = self.global_ctx.clone();
let receiver = self.nic.receiver();
let shared_virtual_nic_registry = self.shared_virtual_nic_registry.clone();
let (output, first_round) = oneshot::channel();
let task = tokio::spawn(async move {
let mut output = Some(output);
loop {
if cancel.is_cancelled() {
Self::report_static_ip_cancelled(&mut output);
return;
}
let closed = Arc::new(Notify::new());
let mut nic = match create_nic_ctx(
global_ctx.clone(),
packet_plane.clone(),
receiver.clone(),
closed.clone(),
shared_virtual_nic_registry.clone(),
)
.await
{
Ok(nic) => nic,
Err(error) => {
if let Some(output) = output.take() {
let _ = output.send(Err(error));
return;
}
tracing::error!(?error, "failed to create native interface context");
tokio::select! {
_ = cancel.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(1)) => {}
}
continue;
}
};
let result = tokio::select! {
biased;
_ = cancel.cancelled() => {
Self::report_static_ip_cancelled(&mut output);
return;
}
result = nic.run(ipv4, ipv6) => result,
};
if let Err(error) = result {
if let Some(output) = output.take() {
let _ = output.send(Err(error));
return;
}
tracing::error!(?error, "failed to create native interface");
tokio::select! {
_ = cancel.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(1)) => {}
}
continue;
}
let magic_dns = if let Some(ip) = ipv4 {
let shared_route_backend = nic.shared_route_backend_for_dns();
MagicDnsRuntime::start(
global_ctx.clone(),
packet_plane.clone(),
nic.ifname().await,
ip,
shared_route_backend,
)
} else {
MagicDnsRuntime::default()
};
nic_state.install(nic, magic_dns).await;
if let Some(output) = output.take() {
let _ = output.send(Ok(()));
}
tokio::select! {
_ = cancel.cancelled() => return,
_ = closed.notified() => {}
}
}
});
self.static_ip_task.lock().await.replace(task);
first_round
.await
.context("static IP setup task stopped")??;
Ok(())
}
pub(super) async fn prepare(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
self.nic.drain().await;
if !self.global_ctx.get_flags().no_tun {
self.start_static_ip(packet_plane).await?;
}
Ok(())
}
pub(super) async fn shutdown(&self) {
if let Some(task) = self.static_ip_task.lock().await.take() {
let _ = task.await;
}
self.nic.stop().await;
}
pub(super) fn attach_fd(&self, _fd: i32) -> anyhow::Result<()> {
anyhow::bail!("external TUN attachment is only supported on mobile Hosts")
}
pub(super) fn dhcp_host(
&self,
operation: Arc<Mutex<()>>,
packet_plane: Arc<CorePacketPlane>,
) -> Arc<dyn DhcpIpv4Host> {
Arc::new(NativeDhcpIpv4Host {
global_ctx: self.global_ctx.clone(),
operation,
cancel: self.cancel.clone(),
nic: self.nic.clone(),
closed: Arc::new(Notify::new()),
packet_plane,
shared_virtual_nic_registry: self.shared_virtual_nic_registry.clone(),
})
}
}
struct NativeDhcpIpv4Host {
global_ctx: ArcGlobalCtx,
operation: Arc<Mutex<()>>,
cancel: CancellationToken,
nic: TunNicState,
closed: Arc<Notify>,
packet_plane: Arc<CorePacketPlane>,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
}
impl NativeDhcpIpv4Host {
fn ensure_open(&self) -> anyhow::Result<()> {
if self.cancel.is_cancelled() {
anyhow::bail!("instance is closing; DHCP update cancelled");
}
Ok(())
}
async fn apply(&self, next: Option<Ipv4Inet>) -> anyhow::Result<Option<Ipv4Inet>> {
self.ensure_open()?;
tokio::select! {
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; DHCP update cancelled"),
_ = self.nic.drain() => {}
}
self.ensure_open()?;
let Some(ip) = next else {
self.global_ctx.set_ipv4(None);
return Ok(None);
};
if self.global_ctx.no_tun() {
self.global_ctx.set_ipv4(Some(ip));
return Ok(Some(ip));
}
let mut nic = create_nic_ctx(
self.global_ctx.clone(),
self.packet_plane.clone(),
self.nic.receiver(),
self.closed.clone(),
self.shared_virtual_nic_registry.clone(),
)
.await?;
tokio::select! {
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; DHCP update cancelled"),
result = nic.run(Some(ip), self.global_ctx.get_ipv6()) => result?,
}
let shared_route_backend = nic.shared_route_backend_for_dns();
let magic_dns = MagicDnsRuntime::start(
self.global_ctx.clone(),
self.packet_plane.clone(),
nic.ifname().await,
ip,
shared_route_backend,
);
self.nic.install(nic, magic_dns).await;
self.global_ctx.set_ipv4(Some(ip));
Ok(Some(ip))
}
}
#[async_trait::async_trait]
impl DhcpIpv4Host for NativeDhcpIpv4Host {
fn take_interface_closed(&self) -> bool {
self.closed.notified().now_or_never().is_some()
}
async fn apply_dhcp_ipv4(
&self,
_previous: Option<Ipv4Inet>,
next: Option<Ipv4Inet>,
) -> DhcpIpv4ApplyOutcome {
let permit = self.operation.clone().lock_owned().await;
let outcome = match self.apply(next).await {
Ok(actual) => DhcpIpv4ApplyOutcome::applied(actual),
Err(error) => DhcpIpv4ApplyOutcome::failed(self.global_ctx.get_ipv4(), error),
};
outcome.with_permit(DhcpIpv4ApplyPermit::new(permit))
}
fn publish_dhcp_ipv4(
&self,
previous: Option<Ipv4Inet>,
requested: Option<Ipv4Inet>,
actual: Option<Ipv4Inet>,
) {
let event = if requested.is_none() {
GlobalCtxEvent::DhcpIpv4Conflicted(previous)
} else {
GlobalCtxEvent::DhcpIpv4Changed(previous, actual)
};
self.global_ctx.issue_event(event);
}
}