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.
This commit is contained in:
KKRainbow committed 2026-10-13 00:44:35 +08:00
1 parent 9bc1a0941f
commit ca4f867e43
26 files changed
+8502 -339

No files matched your search

+143 -39
View File
@@ -5,7 +5,15 @@ use std::{net::Ipv4Addr, sync::Arc, time::Duration};
use easytier_core::instance::CorePacketPlane;
use crate::common::global_ctx::ArcGlobalCtx;
use crate::{
common::{
error::Error as EtError,
global_ctx::ArcGlobalCtx,
ifcfg::{IfConfiger, IfConfiguerTrait},
netns::NetNS,
},
instance::virtual_nic::NicBackend,
};
use super::{client_instance::MagicDnsClientInstance, server_instance::MagicDnsServerInstance};
@@ -17,6 +25,52 @@ pub struct DnsRunner {
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
shared_route_backend: Option<NicBackend>,
active_fake_ip_route: Option<MagicDnsFakeIpRouteClaim>,
}
struct MagicDnsFakeIpRouteClaim {
tun_dev: Option<String>,
net_ns: NetNS,
fake_ip: Ipv4Addr,
route_backend: NicBackend,
}
impl MagicDnsFakeIpRouteClaim {
async fn add(&self) -> anyhow::Result<()> {
let cost = if cfg!(target_os = "windows") {
Some(4)
} else {
None
};
match self
.route_backend
.add_route_with_cost(self.fake_ip, 32, cost)
.await
{
Err(EtError::IOError(err))
if err.kind() == std::io::ErrorKind::AlreadyExists && self.tun_dev.is_some() =>
{
let ifcfg = IfConfiger::default();
let _guard = self.net_ns.guard();
ifcfg
.remove_ipv4_route(self.tun_dev.as_deref().unwrap(), self.fake_ip, 32)
.await?;
self.route_backend
.add_route_with_cost(self.fake_ip, 32, cost)
.await?;
Ok(())
}
result => result.map_err(Into::into),
}
}
async fn remove(&self) {
if let Err(err) = self.route_backend.remove_route(self.fake_ip, 32).await {
tracing::warn!(?err, fake_ip = ?self.fake_ip, "remove magic dns route failed");
}
}
}
impl DnsRunner {
@@ -35,29 +89,76 @@ impl DnsRunner {
tun_dev,
tun_inet,
fake_ip,
shared_route_backend: None,
active_fake_ip_route: None,
}
}
pub(crate) fn with_shared_route_backend(mut self, route_backend: Option<NicBackend>) -> Self {
self.shared_route_backend = route_backend;
self
}
async fn clean_env(&mut self) {
if let Some(claim) = self.active_fake_ip_route.take() {
claim.remove().await;
}
if let Some(server) = self.server.take() {
server.clean_env().await;
}
self.client.take();
}
async fn run_once(&mut self) -> anyhow::Result<()> {
fn should_manage_fake_ip_route_externally(&self) -> bool {
self.shared_route_backend.is_some() && !self.tun_inet.contains(&self.fake_ip)
}
fn fake_ip_route_claim(&self) -> Option<MagicDnsFakeIpRouteClaim> {
if !self.should_manage_fake_ip_route_externally() {
return None;
}
Some(MagicDnsFakeIpRouteClaim {
tun_dev: self.tun_dev.clone(),
net_ns: self.global_ctx.net_ns.clone(),
fake_ip: self.fake_ip,
route_backend: self.shared_route_backend.clone()?,
})
}
async fn run_once(&mut self, cancel_token: &CancellationToken) -> anyhow::Result<()> {
// try server first
match MagicDnsServerInstance::new(
self.packet_plane.clone(),
self.global_ctx.clone(),
self.tun_dev.clone(),
self.tun_inet,
self.fake_ip,
)
.await
{
let server_result = if self.should_manage_fake_ip_route_externally() {
MagicDnsServerInstance::new_with_external_fake_ip_route(
self.packet_plane.clone(),
self.global_ctx.clone(),
self.tun_dev.clone(),
self.tun_inet,
self.fake_ip,
)
.await
} else {
MagicDnsServerInstance::new(
self.packet_plane.clone(),
self.global_ctx.clone(),
self.tun_dev.clone(),
self.tun_inet,
self.fake_ip,
)
.await
};
match server_result {
Ok(server) => {
self.server = Some(server);
// Keep the initialized resolver owned by the runner before adding
// its route, so route failure still reaches async server cleanup.
self.active_fake_ip_route = self.fake_ip_route_claim();
if let Some(claim) = &self.active_fake_ip_route {
claim.add().await.map_err(|err| {
anyhow::anyhow!("failed to add magic dns fake-ip route: {err}")
})?;
}
tracing::info!("DnsRunner::run_once: server started");
}
Err(e) => {
@@ -65,34 +166,37 @@ impl DnsRunner {
}
}
// every runner must run a client
let client = MagicDnsClientInstance::new(self.packet_plane.clone()).await?;
self.client = Some(client);
self.client.as_mut().unwrap().run_and_wait().await;
Err(anyhow::anyhow!("Client instance exit"))
}
pub async fn run(&mut self, canel_token: CancellationToken) {
loop {
tracing::info!("DnsRunner::run: start");
tokio::select! {
_ = canel_token.cancelled() => {
self.clean_env().await;
tracing::info!("DnsRunner::run: cancelled");
return;
}
ret = self.run_once() => {
self.clean_env().await;
if let Err(e) = ret {
tracing::error!("DnsRunner::run: {:?}", e);
} else {
tracing::info!("DnsRunner::run: unexpected exit, server may be down");
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
// Every runner must run a client. Only this phase is cancellable: server
// initialization and route updates must finish before async cleanup.
tokio::select! {
_ = cancel_token.cancelled() => Ok(()),
ret = async {
let client = MagicDnsClientInstance::new(self.packet_plane.clone()).await?;
self.client = Some(client);
self.client.as_mut().unwrap().run_and_wait().await;
Err(anyhow::anyhow!("Client instance exit"))
} => ret,
}
}
pub async fn run(&mut self, cancel_token: CancellationToken) {
while !cancel_token.is_cancelled() {
tracing::info!("DnsRunner::run: start");
let ret = self.run_once(&cancel_token).await;
self.clean_env().await;
if cancel_token.is_cancelled() {
break;
}
if let Err(e) = ret {
tracing::error!("DnsRunner::run: {:?}", e);
} else {
tracing::info!("DnsRunner::run: unexpected exit, server may be down");
}
tokio::select! {
_ = cancel_token.cancelled() => break,
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
}
}
tracing::info!("DnsRunner::run: cancelled");
}
}
@@ -14,8 +14,10 @@ use super::{
};
use crate::{
common::{
error::Error as EtError,
global_ctx::ArcGlobalCtx,
ifcfg::{IfConfiger, IfConfiguerTrait},
netns::NetNS,
},
instance::dns_server::{
config::{Record, RecordBuilder, RecordType},
@@ -51,7 +53,9 @@ use std::{collections::BTreeMap, io, net::Ipv4Addr, str::FromStr, sync::Arc, tim
pub(super) struct MagicDnsServerInstanceData {
dns_server: Server,
tun_dev: Option<String>,
net_ns: NetNS,
fake_ip: Ipv4Addr,
manage_fake_ip_route: bool,
route_store: MagicDnsRecordStore,
record_apply: tokio::sync::Mutex<()>,
@@ -356,12 +360,66 @@ fn get_system_config(
}
impl MagicDnsServerInstance {
async fn add_fake_ip_route(
tun_dev_name: &str,
fake_ip: Ipv4Addr,
net_ns: &NetNS,
cost: Option<i32>,
) -> Result<(), anyhow::Error> {
let ifcfg = IfConfiger::default();
let _guard = net_ns.guard();
match ifcfg.add_ipv4_route(tun_dev_name, fake_ip, 32, cost).await {
Err(EtError::IOError(err)) if err.kind() == io::ErrorKind::AlreadyExists => {
ifcfg.remove_ipv4_route(tun_dev_name, fake_ip, 32).await?;
ifcfg
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
.await?;
Ok(())
}
ret => ret.map_err(Into::into),
}
}
async fn remove_fake_ip_route(tun_dev_name: &str, fake_ip: Ipv4Addr, net_ns: &NetNS) {
let ifcfg = IfConfiger::default();
let _guard = net_ns.guard();
if let Err(err) = ifcfg.remove_ipv4_route(tun_dev_name, fake_ip, 32).await {
tracing::warn!(
?err,
?tun_dev_name,
?fake_ip,
"remove magic dns route failed"
);
}
}
pub(crate) async fn new(
packet_plane: Arc<CorePacketPlane>,
global_ctx: ArcGlobalCtx,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
) -> Result<Self, anyhow::Error> {
Self::new_inner(packet_plane, global_ctx, tun_dev, tun_inet, fake_ip, true).await
}
pub(crate) async fn new_with_external_fake_ip_route(
packet_plane: Arc<CorePacketPlane>,
global_ctx: ArcGlobalCtx,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
) -> Result<Self, anyhow::Error> {
Self::new_inner(packet_plane, global_ctx, tun_dev, tun_inet, fake_ip, false).await
}
async fn new_inner(
packet_plane: Arc<CorePacketPlane>,
global_ctx: ArcGlobalCtx,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
manage_fake_ip_route: bool,
) -> Result<Self, anyhow::Error> {
let tcp_listener = runtime_rpc_listener(MAGIC_DNS_INSTANCE_SOCKET_ADDR.parse()?);
let mut rpc_server = StandAloneServer::new(tcp_listener);
@@ -374,7 +432,8 @@ impl MagicDnsServerInstance {
let mut dns_server = Server::new(dns_config);
dns_server.run().await?;
if !tun_inet.contains(&fake_ip)
if manage_fake_ip_route
&& !tun_inet.contains(&fake_ip)
&& let Some(tun_dev_name) = &tun_dev
{
let cost = if cfg!(target_os = "windows") {
@@ -382,16 +441,15 @@ impl MagicDnsServerInstance {
} else {
None
};
let ifcfg = IfConfiger {};
ifcfg
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
.await?;
Self::add_fake_ip_route(tun_dev_name, fake_ip, &global_ctx.net_ns, cost).await?;
}
let data = Arc::new(MagicDnsServerInstanceData {
dns_server,
tun_dev: tun_dev.clone(),
net_ns: global_ctx.net_ns.clone(),
fake_ip,
manage_fake_ip_route,
route_store: MagicDnsRecordStore::default(),
record_apply: tokio::sync::Mutex::new(()),
system_config: get_system_config(tun_dev.as_deref())?,
@@ -436,14 +494,13 @@ impl MagicDnsServerInstance {
if let Err(e) = ret {
tracing::error!("Failed to close system config: {:?}", e);
}
if !self.tun_inet.contains(&self.data.fake_ip)
&& let Some(tun_dev_name) = &self.data.tun_dev
{
let ifcfg = IfConfiger {};
let _ = ifcfg
.remove_ipv4_route(tun_dev_name, self.data.fake_ip, 32)
.await;
}
}
if self.data.manage_fake_ip_route
&& !self.tun_inet.contains(&self.data.fake_ip)
&& let Some(tun_dev_name) = &self.data.tun_dev
{
Self::remove_fake_ip_route(tun_dev_name, self.data.fake_ip, &self.data.net_ns).await;
}
self.packet_filter.close().await;