test: cover shared tun magic dns in netns

Add an end-to-end shared tun Magic DNS test with two members
sharing one device in the same namespace. The test checks real
netns ping paths and queries the Magic DNS fake IP through dig.

Teach DnsRunner to pass the instance netns into
MagicDnsServerInstance so fake DNS route add/remove operations run
where the tun device exists. Keep the existing constructors as
wrappers so current callers stay source-compatible.
This commit is contained in:
sijie.sun
2026-06-13 20:40:58 +08:00
parent 17195a5cc4
commit cf3fcb75b5
4 changed files with 257 additions and 17 deletions
+16 -1
View File
@@ -13,6 +13,7 @@ pub struct DnsRunner {
server: Option<MagicDnsServerInstance>,
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
netns: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
}
@@ -29,11 +30,24 @@ impl DnsRunner {
server: None,
peer_mgr,
tun_dev,
netns: None,
tun_inet,
fake_ip,
}
}
pub fn new_with_netns(
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
netns: Option<String>,
) -> Self {
let mut runner = Self::new(peer_mgr, tun_dev, tun_inet, fake_ip);
runner.netns = netns;
runner
}
async fn clean_env(&mut self) {
if let Some(server) = self.server.take() {
server.clean_env().await;
@@ -43,11 +57,12 @@ impl DnsRunner {
async fn run_once(&mut self) -> anyhow::Result<()> {
// try server first
match MagicDnsServerInstance::new(
match MagicDnsServerInstance::new_with_netns(
self.peer_mgr.clone(),
self.tun_dev.clone(),
self.tun_inet,
self.fake_ip,
self.netns.clone(),
)
.await
{
@@ -15,7 +15,9 @@ use super::{
use crate::{
common::{
PeerId,
error::Error as EtError,
ifcfg::{IfConfiger, IfConfiguerTrait},
netns::NetNS,
},
instance::dns_server::{
config::{Record, RecordBuilder, RecordType},
@@ -61,6 +63,7 @@ static NIC_PIPELINE_NAME: &str = "magic_dns_server";
pub(super) struct MagicDnsServerInstanceData {
dns_server: Server,
tun_dev: Option<String>,
netns: Option<String>,
tun_ip: Ipv4Addr,
fake_ip: Ipv4Addr,
my_peer_id: PeerId,
@@ -510,11 +513,54 @@ fn get_system_config(
}
impl MagicDnsServerInstance {
async fn add_fake_ip_route(
tun_dev_name: &str,
fake_ip: Ipv4Addr,
netns: Option<String>,
cost: Option<i32>,
) -> Result<(), anyhow::Error> {
let ifcfg = IfConfiger {};
let _guard = NetNS::new(netns).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, netns: Option<String>) {
let ifcfg = IfConfiger {};
let _guard = NetNS::new(netns).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 async fn new(
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
) -> Result<Self, anyhow::Error> {
Self::new_with_netns(peer_mgr, tun_dev, tun_inet, fake_ip, None).await
}
pub async fn new_with_netns(
peer_mgr: Arc<PeerManager>,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
netns: Option<String>,
) -> Result<Self, anyhow::Error> {
let tcp_listener = TcpTunnelListener::new(MAGIC_DNS_INSTANCE_ADDR.parse()?);
let mut rpc_server = StandAloneServer::new(tcp_listener);
@@ -535,15 +581,13 @@ 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, netns.clone(), cost).await?;
}
let data = Arc::new(MagicDnsServerInstanceData {
dns_server,
tun_dev: tun_dev.clone(),
netns,
tun_ip: tun_inet.address(),
fake_ip,
my_peer_id: peer_mgr.my_peer_id(),
@@ -586,14 +630,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.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.netns.clone())
.await;
}
let _ = self
+7 -4
View File
@@ -817,11 +817,12 @@ impl Instance {
return None;
}
let runner = DnsRunner::new(
let runner = DnsRunner::new_with_netns(
peer_mgr,
tun_dev,
tun_ip,
MAGIC_DNS_FAKE_IP.parse().unwrap(),
ctx.net_ns.name(),
);
Some(runner)
}
@@ -1695,9 +1696,9 @@ impl Instance {
}
pub async fn clear_resources(&mut self) {
self.peer_manager.clear_resources().await;
#[cfg(feature = "tun")]
let _ = self.nic_ctx.lock().await.take();
Self::clear_nic_ctx(self.nic_ctx.clone(), self.peer_packet_receiver.clone()).await;
self.peer_manager.clear_resources().await;
}
}
@@ -1707,9 +1708,11 @@ impl Drop for Instance {
let pm = Arc::downgrade(&self.peer_manager);
#[cfg(feature = "tun")]
let nic_ctx = self.nic_ctx.clone();
#[cfg(feature = "tun")]
let peer_packet_receiver = self.peer_packet_receiver.clone();
tokio::spawn(async move {
#[cfg(feature = "tun")]
nic_ctx.lock().await.take();
Self::clear_nic_ctx(nic_ctx, peer_packet_receiver).await;
if let Some(pm) = pm.upgrade() {
pm.clear_resources().await;
};
+179
View File
@@ -13,6 +13,9 @@ use x25519_dalek::StaticSecret;
use super::*;
#[cfg(all(feature = "tun", feature = "magic-dns"))]
use crate::instance::dns_server::{DEFAULT_ET_DNS_ZONE, MAGIC_DNS_FAKE_IP};
// TODO: 需要加一个单测,确保 socks5 + exit node == self || proxy_cidr == 0.0.0.0/0 时,可以实现出口节点的能力。
use crate::{
@@ -608,6 +611,146 @@ pub async fn shared_tun_proxy_cidr_reaches_member_network() {
drop_insts(vec![center, shared_1, shared_2, remote]).await;
}
#[cfg(all(feature = "tun", feature = "magic-dns"))]
#[tokio::test]
#[serial_test::serial]
pub async fn shared_tun_magic_dns_same_namespace_real_tun() {
prepare_linux_namespaces();
let dev_name = shared_tun_test_dev_name();
let network_name = "shared_tun_magic_dns_network";
let network_secret = "shared_tun_magic_dns_secret";
let center_cfg = shared_tun_test_config(
"shared_dns_center",
network_name,
network_secret,
Some("net_a"),
None,
"10.144.255.1/24",
false,
);
let mut center = Instance::new(center_cfg);
let shared_cfg_1 = shared_tun_test_config(
"shared_dns_first",
network_name,
network_secret,
Some("net_b"),
Some(&dev_name),
"10.144.255.2/24",
false,
);
shared_cfg_1.set_hostname(Some("shared-dns-1".to_string()));
let mut shared_flags = shared_cfg_1.get_flags();
shared_flags.accept_dns = true;
shared_cfg_1.set_flags(shared_flags.clone());
let mut shared_1 = Instance::new(shared_cfg_1);
let shared_cfg_2 = shared_tun_test_config(
"shared_dns_second",
network_name,
network_secret,
Some("net_b"),
Some(&dev_name),
"10.144.255.3/24",
false,
);
shared_cfg_2.set_hostname(Some("shared-dns-2".to_string()));
shared_cfg_2.set_flags(shared_flags);
let mut shared_2 = Instance::new(shared_cfg_2);
let remote_cfg = shared_tun_test_config(
"shared_dns_remote",
network_name,
network_secret,
Some("net_c"),
None,
"10.144.255.4/24",
false,
);
remote_cfg.set_hostname(Some("shared-dns-remote".to_string()));
let mut remote = Instance::new(remote_cfg);
let mut shared_1_events = shared_1.get_global_ctx().subscribe();
let mut shared_2_events = shared_2.get_global_ctx().subscribe();
center.run().await.unwrap();
shared_1.run().await.unwrap();
shared_2.run().await.unwrap();
remote.run().await.unwrap();
let shared_1_ifname = wait_tun_ready(&mut shared_1_events).await;
let shared_2_ifname = wait_tun_ready(&mut shared_2_events).await;
assert_eq!(shared_1_ifname, dev_name);
assert_eq!(shared_2_ifname, dev_name);
shared_1
.get_conn_manager()
.add_connector(RingTunnelConnector::new(
format!("ring://{}", center.id()).parse().unwrap(),
));
shared_2
.get_conn_manager()
.add_connector(RingTunnelConnector::new(
format!("ring://{}", center.id()).parse().unwrap(),
));
remote
.get_conn_manager()
.add_connector(RingTunnelConnector::new(
format!("ring://{}", center.id()).parse().unwrap(),
));
wait_for_condition(
|| async {
center.get_peer_manager().list_routes().await.len() == 3
&& shared_1.get_peer_manager().list_routes().await.len() == 3
&& shared_2.get_peer_manager().list_routes().await.len() == 3
&& remote.get_peer_manager().list_routes().await.len() == 3
},
Duration::from_secs(8),
)
.await;
wait_for_condition(
|| async { ping_test("net_c", "10.144.255.2", None).await },
Duration::from_secs(8),
)
.await;
wait_for_condition(
|| async { ping_test("net_c", "10.144.255.3", None).await },
Duration::from_secs(8),
)
.await;
wait_for_condition(
|| async { ping_test("net_b", "10.144.255.4", None).await },
Duration::from_secs(8),
)
.await;
wait_for_condition(
|| async { magic_dns_record_matches("net_b", "shared-dns-1", "10.144.255.2").await },
Duration::from_secs(8),
)
.await;
wait_for_condition(
|| async { magic_dns_record_matches("net_b", "shared-dns-2", "10.144.255.3").await },
Duration::from_secs(8),
)
.await;
wait_for_condition(
|| async { magic_dns_record_matches("net_b", "shared-dns-remote", "10.144.255.4").await },
Duration::from_secs(8),
)
.await;
drop_insts(vec![center, shared_1, shared_2, remote]).await;
assert!(
!ipv4_route_exists_in_ns("net_b", &format!("{MAGIC_DNS_FAKE_IP} dev {dev_name}")),
"magic dns fake-ip route should be removed with the shared tun member"
);
}
mod direct_connector_mapped_listener_tests {
use std::sync::Arc;
@@ -752,6 +895,35 @@ async fn ping6_test(from_netns: &str, target_ip: &str, payload_size: Option<usiz
code.code().unwrap() == 0
}
#[cfg(all(feature = "tun", feature = "magic-dns"))]
async fn magic_dns_record_matches(from_netns: &str, hostname: &str, expected_ip: &str) -> bool {
let _g = NetNS::new(Some(ROOT_NETNS_NAME.to_owned())).guard();
let server = format!("@{}", MAGIC_DNS_FAKE_IP);
let domain = format!("{}.{DEFAULT_ET_DNS_ZONE}", hostname);
let output = tokio::process::Command::new("ip")
.arg("netns")
.arg("exec")
.arg(from_netns)
.arg("dig")
.arg(server)
.arg(domain)
.arg("A")
.arg("+short")
.arg("+tries=1")
.arg("+time=1")
.output()
.await
.unwrap();
if !output.status.success() {
return false;
}
String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| line.trim() == expected_ip)
}
fn run_cmd(program: &str, args: &[&str]) {
let output = std::process::Command::new(program)
.args(args)
@@ -1102,6 +1274,13 @@ fn route_exists_in_ns(ns: &str, needle: &str) -> bool {
.any(|line| line.contains(needle))
}
#[cfg(all(feature = "tun", feature = "magic-dns"))]
fn ipv4_route_exists_in_ns(ns: &str, needle: &str) -> bool {
run_ip_in_ns_output(ns, &["route", "show"])
.lines()
.any(|line| line.contains(needle))
}
fn addr_exists_in_ns(ns: &str, dev: &str, needle: &str) -> bool {
run_ip_in_ns_output(ns, &["-6", "addr", "show", "dev", dev]).contains(needle)
}