shared-tun: preserve member ownership on mobile

Introduce shared NIC source ownership and dispatcher handling so a
single dev_name can be shared by multiple tun-enabled instances while
keeping per-member IP and route claims distinct.

Pass Android VpnService fd registration with per-instance source and
route claims. Keep the VPN address list limited to real member
addresses and allow AF_INET6 without installing hidden fd00::1.

Invalidate dispatcher flow and NAT state when source ownership changes
or a member unregisters. Avoid rewriting non-first IPv4 fragment
payloads, and adjust fragmented TCP/UDP checksums without recomputing
over partial fragment bodies.

Preserve source-owner routing for equal-prefix route conflicts, keep
ICMP echo NAT entries distinct by echo id, and retry stale flow-owner
send failures from the original packet. Only record NAT state after a
translated packet is accepted by its member.

Apply Linux IPv4 route preferred-source hints for shared routes and keep
route repair paths source-aware. Keep Darwin ifcfg access scoped to
cleanup-only paths where netns is not available.
This commit is contained in:
sijie.sun
2026-06-16 02:16:25 +08:00
parent ca17e856a6
commit 800c840bb5
13 changed files with 2423 additions and 175 deletions
+93 -5
View File
@@ -66,6 +66,9 @@ static RPC_SERVER: once_cell::sync::Lazy<Mutex<Option<RpcServer>>> =
static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> = static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> =
once_cell::sync::Lazy::new(|| RwLock::new(None)); once_cell::sync::Lazy::new(|| RwLock::new(None));
#[cfg(target_os = "android")]
const ANDROID_SHARED_TUN_DEV_NAME: &str = "easytier-shared";
macro_rules! get_client_manager { macro_rules! get_client_manager {
() => {{ () => {{
let guard = CLIENT_MANAGER let guard = CLIENT_MANAGER
@@ -76,6 +79,17 @@ macro_rules! get_client_manager {
}}; }};
} }
fn normalize_network_config_for_runtime(cfg: &mut NetworkConfig) {
#[cfg(target_os = "android")]
{
if !cfg.no_tun() && cfg.dev_name.as_deref().map(str::is_empty).unwrap_or(true) {
cfg.dev_name = Some(ANDROID_SHARED_TUN_DEV_NAME.to_owned());
}
}
#[cfg(not(target_os = "android"))]
let _ = cfg;
}
#[tauri::command] #[tauri::command]
fn easytier_version() -> Result<String, String> { fn easytier_version() -> Result<String, String> {
Ok(easytier::VERSION.to_string()) Ok(easytier::VERSION.to_string())
@@ -100,6 +114,8 @@ fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), Strin
#[tauri::command] #[tauri::command]
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> { fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
let mut cfg = cfg;
normalize_network_config_for_runtime(&mut cfg);
let toml = cfg.gen_config().map_err(|e| e.to_string())?; let toml = cfg.gen_config().map_err(|e| e.to_string())?;
Ok(toml.dump()) Ok(toml.dump())
} }
@@ -117,6 +133,8 @@ async fn run_network_instance(
cfg: NetworkConfig, cfg: NetworkConfig,
save: bool, save: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let mut cfg = cfg;
normalize_network_config_for_runtime(&mut cfg);
let client_manager = get_client_manager!()?; let client_manager = get_client_manager!()?;
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?; let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
client_manager client_manager
@@ -155,16 +173,84 @@ async fn set_logging_level(level: String) -> Result<(), String> {
Ok(()) Ok(())
} }
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct TunFdInstanceSources {
instance_id: String,
ipv4_addrs: Vec<String>,
#[serde(default)]
ipv6_addrs: Vec<String>,
#[serde(default)]
ipv4_routes: Vec<String>,
#[serde(default)]
ipv6_routes: Vec<String>,
}
#[cfg(mobile)]
fn parse_tun_fd_instance_sources(
instance_sources: Vec<TunFdInstanceSources>,
) -> Result<
std::collections::HashMap<uuid::Uuid, easytier::instance::virtual_nic::MobileTunSources>,
String,
> {
instance_sources
.into_iter()
.map(|source| {
let instance_id = source
.instance_id
.parse::<uuid::Uuid>()
.map_err(|err| format!("invalid instance id {}: {err}", source.instance_id))?;
let sources = easytier::instance::virtual_nic::MobileTunSources::parse(
source.ipv4_addrs,
source.ipv6_addrs,
source.ipv4_routes,
source.ipv6_routes,
)
.map_err(|err| err.to_string())?;
Ok((instance_id, sources))
})
.collect()
}
#[tauri::command] #[tauri::command]
async fn set_tun_fd(fd: i32) -> Result<(), String> { async fn set_tun_fd(
fd: i32,
instance_ids: Option<Vec<String>>,
instance_sources: Option<Vec<TunFdInstanceSources>>,
) -> Result<(), String> {
let Some(instance_manager) = INSTANCE_MANAGER.read().await.clone() else { let Some(instance_manager) = INSTANCE_MANAGER.read().await.clone() else {
return Err("set_tun_fd is not supported in remote mode".to_string()); return Err("set_tun_fd is not supported in remote mode".to_string());
}; };
let target_ids = match instance_ids {
Some(instance_ids) if !instance_ids.is_empty() => instance_ids
.into_iter()
.map(|id| {
id.parse::<uuid::Uuid>()
.map_err(|err| format!("invalid instance id {id}: {err}"))
})
.collect::<Result<Vec<_>, _>>()?,
_ => get_client_manager!()?.get_enabled_instances_for_tun_fd(),
};
#[cfg(mobile)]
let mut source_map = parse_tun_fd_instance_sources(instance_sources.unwrap_or_default())?;
#[cfg(not(mobile))]
let _ = instance_sources;
let mut success_count = 0; let mut success_count = 0;
let mut errors = Vec::new(); let mut errors = Vec::new();
for uuid in get_client_manager!()?.get_enabled_instances_for_tun_fd() { for uuid in target_ids {
match instance_manager.set_tun_fd(&uuid, fd) { #[cfg(mobile)]
let set_result = match source_map.remove(&uuid) {
Some(sources) => instance_manager.set_tun_fd(&uuid, fd, sources),
None => Err(anyhow::anyhow!("missing tun sources for instance {uuid}")),
};
#[cfg(not(mobile))]
let set_result = instance_manager.set_tun_fd(&uuid, fd);
match set_result {
Ok(()) => { Ok(()) => {
success_count += 1; success_count += 1;
} }
@@ -1210,10 +1296,12 @@ mod manager {
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
self.storage.network_configs.clear(); self.storage.network_configs.clear();
for stored in configs { for stored in configs {
let instance_id = stored.config.instance_id(); let mut config = stored.config;
normalize_network_config_for_runtime(&mut config);
let instance_id = config.instance_id();
self.storage.network_configs.insert( self.storage.network_configs.insert(
instance_id.parse()?, instance_id.parse()?,
GUIConfig::new(instance_id.to_string(), stored.config, stored.source), GUIConfig::new(instance_id.to_string(), config, stored.source),
); );
} }
+18 -3
View File
@@ -70,8 +70,23 @@ export async function setLoggingLevel(level: string) {
return await invoke('set_logging_level', { level }) return await invoke('set_logging_level', { level })
} }
export async function setTunFd(fd: number) { export interface TunFdInstanceSources {
return await invoke('set_tun_fd', { fd }) instanceId: string
ipv4Addrs: string[]
ipv6Addrs?: string[]
ipv4Routes?: string[]
ipv6Routes?: string[]
}
export async function setTunFd(fd: number, instanceIds?: string[], instanceSources?: TunFdInstanceSources[]) {
const args: { fd: number, instanceIds?: string[], instanceSources?: TunFdInstanceSources[] } = { fd }
if (instanceIds?.length) {
args.instanceIds = instanceIds
}
if (instanceSources?.length) {
args.instanceSources = instanceSources
}
return await invoke('set_tun_fd', args)
} }
export async function getEasytierVersion() { export async function getEasytierVersion() {
@@ -110,7 +125,7 @@ export async function sendConfigs(enabledNetworks: string[]) {
config: NetworkTypes.toBackendNetworkConfig(config), config: NetworkTypes.toBackendNetworkConfig(config),
source, source,
})), })),
enabledNetworks enabledNetworks,
}) })
} }
+113 -11
View File
@@ -2,6 +2,7 @@ import type { NetworkTypes } from 'easytier-frontend-lib'
import { addPluginListener } from '@tauri-apps/api/core' import { addPluginListener } from '@tauri-apps/api/core'
import { Utils } from 'easytier-frontend-lib' import { Utils } from 'easytier-frontend-lib'
import { get_vpn_status, prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api' import { get_vpn_status, prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
import type { TunFdInstanceSources } from './backend'
type Route = NetworkTypes.Route type Route = NetworkTypes.Route
@@ -12,6 +13,8 @@ interface vpnStatus {
ipv4Cidr: number | null | undefined ipv4Cidr: number | null | undefined
routes: string[] routes: string[]
dns: string | null | undefined dns: string | null | undefined
instanceIds: string[]
instanceSources: TunFdInstanceSources[]
} }
let dhcpPollingTimer: NodeJS.Timeout | null = null let dhcpPollingTimer: NodeJS.Timeout | null = null
@@ -26,6 +29,8 @@ const curVpnStatus: vpnStatus = {
ipv4Cidr: undefined, ipv4Cidr: undefined,
routes: [], routes: [],
dns: undefined, dns: undefined,
instanceIds: [],
instanceSources: [],
} }
async function requestVpnPermission() { async function requestVpnPermission() {
@@ -50,6 +55,8 @@ function resetVpnConfigStatus() {
curVpnStatus.ipv4Cidr = undefined curVpnStatus.ipv4Cidr = undefined
curVpnStatus.routes = [] curVpnStatus.routes = []
curVpnStatus.dns = undefined curVpnStatus.dns = undefined
curVpnStatus.instanceIds = []
curVpnStatus.instanceSources = []
} }
function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) { function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) {
@@ -59,13 +66,14 @@ function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_statu
return return
} }
curVpnStatus.ipv4Addrs = status?.ipv4Addrs?.length const nativeIpv4Addrs = status?.ipv4Addrs?.length
? [...status.ipv4Addrs] ? [...status.ipv4Addrs]
: status?.ipv4Addr : status?.ipv4Addr
? [status.ipv4Addr] ? [status.ipv4Addr]
: [] : []
curVpnStatus.ipv4Addrs = nativeIpv4Addrs
const ipv4WithCidr = status?.ipv4Addr const ipv4WithCidr = curVpnStatus.ipv4Addrs[0]
if (ipv4WithCidr?.length) { if (ipv4WithCidr?.length) {
const [ipv4Addr, cidr] = ipv4WithCidr.split('/') const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
curVpnStatus.ipv4Addr = ipv4Addr curVpnStatus.ipv4Addr = ipv4Addr
@@ -107,14 +115,22 @@ async function doStopVpn(force = false) {
resetVpnConfigStatus() resetVpnConfigStatus()
} }
async function doStartVpn(ipv4Addrs: string[], routes: string[], dns?: string) { async function doStartVpn(
ipv4Addrs: string[],
routes: string[],
dns: string | undefined,
instanceIds: string[],
instanceSources: TunFdInstanceSources[],
) {
if (curVpnStatus.running) { if (curVpnStatus.running) {
return return
} }
const primaryIpv4 = ipv4Addrs[0] const primaryIpv4 = ipv4Addrs[0]
const [ipv4Addr, cidr] = primaryIpv4.split('/') const [ipv4Addr, cidr] = ipv4Addrs[0].split('/')
console.log('start vpn service', ipv4Addrs, routes, dns) curVpnStatus.instanceIds = [...instanceIds]
curVpnStatus.instanceSources = [...instanceSources]
console.log('start vpn service', ipv4Addrs, routes, dns, instanceIds)
const request = { const request = {
ipv4Addr: primaryIpv4, ipv4Addr: primaryIpv4,
ipv4Addrs, ipv4Addrs,
@@ -145,6 +161,8 @@ async function doStartVpn(ipv4Addrs: string[], routes: string[], dns?: string) {
curVpnStatus.ipv4Cidr = Number(cidr) curVpnStatus.ipv4Cidr = Number(cidr)
curVpnStatus.routes = routes curVpnStatus.routes = routes
curVpnStatus.dns = dns curVpnStatus.dns = dns
curVpnStatus.instanceIds = [...instanceIds]
curVpnStatus.instanceSources = [...instanceSources]
} }
function scheduleVpnConfigRetry(instanceId: string) { function scheduleVpnConfigRetry(instanceId: string) {
@@ -164,7 +182,7 @@ async function onVpnServiceStart(payload: any) {
console.log('vpn service start', JSON.stringify(payload)) console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true curVpnStatus.running = true
if (payload.fd) { if (payload.fd) {
await setTunFd(payload.fd).catch((e) => { await setTunFd(payload.fd, curVpnStatus.instanceIds, curVpnStatus.instanceSources).catch((e) => {
console.error('set tun fd failed', e) console.error('set tun fd failed', e)
}) })
} }
@@ -218,12 +236,73 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
return Array.from(new Set(ret)).sort() return Array.from(new Set(ret)).sort()
} }
function ipv4CidrToRoute(cidr: string): string | undefined {
const [address, prefixText] = cidr.split('/')
const prefix = Number(prefixText)
const octets = address?.split('.').map(octet => Number(octet))
if (
octets?.length !== 4
|| !Number.isInteger(prefix)
|| prefix < 0
|| prefix > 32
|| octets.some((octet) => {
return !Number.isInteger(octet) || octet < 0 || octet > 255
})
) {
return undefined
}
const ip = (
octets[0] * 0x1000000
+ octets[1] * 0x10000
+ octets[2] * 0x100
+ octets[3]
) >>> 0
const mask = prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0
const network = (ip & mask) >>> 0
const route = [
(network >>> 24) & 0xFF,
(network >>> 16) & 0xFF,
(network >>> 8) & 0xFF,
network & 0xFF,
].join('.')
return `${route}/${prefix}`
}
function getCollectedNetworkInfo(response: Awaited<ReturnType<typeof collectNetworkInfo>>, instanceId: string) { function getCollectedNetworkInfo(response: Awaited<ReturnType<typeof collectNetworkInfo>>, instanceId: string) {
const info = response.info as any const info = response.info as any
const map = info?.map ?? info const map = info?.map ?? info
return map?.[instanceId] return map?.[instanceId]
} }
function sortInstanceSources(sources: TunFdInstanceSources[]): TunFdInstanceSources[] {
return sources
.map(source => ({
instanceId: source.instanceId,
ipv4Addrs: [...source.ipv4Addrs].sort(),
ipv6Addrs: [...(source.ipv6Addrs ?? [])].sort(),
ipv4Routes: [...(source.ipv4Routes ?? [])].sort(),
ipv6Routes: [...(source.ipv6Routes ?? [])].sort(),
}))
.sort((a, b) => a.instanceId.localeCompare(b.instanceId))
}
function splitRoutesByFamily(routes: string[]) {
const ipv4Routes: string[] = []
const ipv6Routes: string[] = []
routes.forEach((route) => {
if (route.includes(':')) {
ipv6Routes.push(route)
}
else {
ipv4Routes.push(route)
}
})
return { ipv4Routes, ipv6Routes }
}
export async function onNetworkInstanceChange(instanceId: string) { export async function onNetworkInstanceChange(instanceId: string) {
pendingVpnConfigInstanceId = instanceId pendingVpnConfigInstanceId = instanceId
if (!vpnConfigSyncTask) { if (!vpnConfigSyncTask) {
@@ -273,6 +352,7 @@ async function applyNetworkInstanceChange(instanceId: string) {
} }
const ipv4Addrs: string[] = [] const ipv4Addrs: string[] = []
const instanceSources: TunFdInstanceSources[] = []
const routes = new Set<string>() const routes = new Set<string>()
let dns: string | undefined let dns: string | undefined
const retryInstanceIds: string[] = [] const retryInstanceIds: string[] = []
@@ -308,8 +388,26 @@ async function applyNetworkInstanceChange(instanceId: string) {
network_length = 24 network_length = 24
} }
ipv4Addrs.push(`${virtual_ip}/${network_length}`) const sourceIpv4 = `${virtual_ip}/${network_length}`
getRoutesForVpn(curNetworkInfo?.routes, config).forEach(route => routes.add(route)) ipv4Addrs.push(sourceIpv4)
const instanceRoutes = new Set<string>()
const localRoute = ipv4CidrToRoute(sourceIpv4)
if (localRoute) {
routes.add(localRoute)
instanceRoutes.add(localRoute)
}
getRoutesForVpn(curNetworkInfo?.routes, config).forEach((route) => {
routes.add(route)
instanceRoutes.add(route)
})
const { ipv4Routes, ipv6Routes } = splitRoutesByFamily([...instanceRoutes])
instanceSources.push({
instanceId,
ipv4Addrs: [sourceIpv4],
ipv6Addrs: [],
ipv4Routes,
ipv6Routes,
})
if (config.enable_magic_dns) { if (config.enable_magic_dns) {
dns = '100.100.100.101' dns = '100.100.100.101'
} }
@@ -330,10 +428,14 @@ async function applyNetworkInstanceChange(instanceId: string) {
const sortedIpv4Addrs = [...ipv4Addrs].sort() const sortedIpv4Addrs = [...ipv4Addrs].sort()
const sortedRoutes = Array.from(routes).sort() const sortedRoutes = Array.from(routes).sort()
const sortedInstanceIds = group.map(({ instanceId }) => instanceId).sort()
const sortedInstanceSources = sortInstanceSources(instanceSources)
const ipChanged = JSON.stringify(sortedIpv4Addrs) !== JSON.stringify(curVpnStatus.ipv4Addrs) const ipChanged = JSON.stringify(sortedIpv4Addrs) !== JSON.stringify(curVpnStatus.ipv4Addrs)
const routesChanged = JSON.stringify(sortedRoutes) !== JSON.stringify(curVpnStatus.routes) const routesChanged = JSON.stringify(sortedRoutes) !== JSON.stringify(curVpnStatus.routes)
const dnsChanged = dns != curVpnStatus.dns const dnsChanged = dns !== curVpnStatus.dns
const configChanged = ipChanged || routesChanged || dnsChanged const instanceIdsChanged = JSON.stringify(sortedInstanceIds) !== JSON.stringify(curVpnStatus.instanceIds)
const instanceSourcesChanged = JSON.stringify(sortedInstanceSources) !== JSON.stringify(sortInstanceSources(curVpnStatus.instanceSources))
const configChanged = ipChanged || routesChanged || dnsChanged || instanceIdsChanged || instanceSourcesChanged
const shouldStartVpn = !curVpnStatus.running const shouldStartVpn = !curVpnStatus.running
if (shouldStartVpn || configChanged) { if (shouldStartVpn || configChanged) {
@@ -357,7 +459,7 @@ async function applyNetworkInstanceChange(instanceId: string) {
} }
try { try {
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns) await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns, sortedInstanceIds, sortedInstanceSources)
} }
catch (e) { catch (e) {
if (e instanceof Error && e.message === 'need_prepare') { if (e instanceof Error && e.message === 'need_prepare') {
+17 -1
View File
@@ -46,12 +46,28 @@ impl IfConfiguerTrait for MacIfConfiger {
cidr_prefix: u8, cidr_prefix: u8,
cost: Option<i32>, cost: Option<i32>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.add_ipv4_route_with_source_hint(name, address, cidr_prefix, cost, None)
.await
}
async fn add_ipv4_route_with_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
let source_hint = source_hint
.map(|source| format!(" -ifa {}", source))
.unwrap_or_default();
run_shell_cmd( run_shell_cmd(
format!( format!(
"route -n add {} -netmask {} -interface {} -hopcount {}", "route -n add {} -netmask {} -interface {}{} -hopcount {}",
address, address,
cidr_to_subnet_mask(cidr_prefix), cidr_to_subnet_mask(cidr_prefix),
name, name,
source_hint,
cost.unwrap_or(7) cost.unwrap_or(7)
) )
.as_str(), .as_str(),
+20
View File
@@ -31,6 +31,16 @@ pub trait IfConfiguerTrait: Send + Sync {
) -> Result<(), Error> { ) -> Result<(), Error> {
Ok(()) Ok(())
} }
async fn add_ipv4_route_with_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
_source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
self.add_ipv4_route(name, address, cidr_prefix, cost).await
}
async fn remove_ipv4_route( async fn remove_ipv4_route(
&self, &self,
_name: &str, _name: &str,
@@ -39,6 +49,16 @@ pub trait IfConfiguerTrait: Send + Sync {
) -> Result<(), Error> { ) -> Result<(), Error> {
Ok(()) Ok(())
} }
async fn remove_ipv4_route_with_cost_and_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
_cost: Option<i32>,
_source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
self.remove_ipv4_route(name, address, cidr_prefix).await
}
async fn add_ipv4_ip( async fn add_ipv4_ip(
&self, &self,
_name: &str, _name: &str,
+194 -26
View File
@@ -376,6 +376,64 @@ impl NetlinkIfConfiger {
pub(crate) fn list_ipv6_route_messages() -> Result<Vec<RouteMessage>, Error> { pub(crate) fn list_ipv6_route_messages() -> Result<Vec<RouteMessage>, Error> {
Self::list_route_messages(AddressFamily::Inet6) Self::list_route_messages(AddressFamily::Inet6)
} }
fn ipv4_route_message(
ifindex: u32,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> RouteMessage {
let mut message = RouteMessage::default();
message.header.table = RouteHeader::RT_TABLE_MAIN;
message.header.protocol = RouteProtocol::Static;
message.header.scope = RouteScope::Universe;
message.header.kind = RouteType::Unicast;
message.header.address_family = AddressFamily::Inet;
message.header.destination_prefix_length = cidr_prefix;
message
.attributes
.push(RouteAttribute::Priority(cost.unwrap_or(65535) as u32));
message.attributes.push(RouteAttribute::Oif(ifindex));
message
.attributes
.push(RouteAttribute::Destination(RouteAddress::Inet(address)));
if let Some(source_hint) = source_hint {
message
.attributes
.push(RouteAttribute::PrefSource(RouteAddress::Inet(source_hint)));
}
message
}
fn ipv4_route_target_matches(
route: &Route,
address: Ipv4Addr,
cidr_prefix: u8,
ifidx: u32,
) -> bool {
route.destination == IpAddr::V4(address)
&& route.prefix == cidr_prefix
&& route.ifindex == Some(ifidx)
}
fn ipv4_route_exact_matches(
route: &Route,
address: Ipv4Addr,
cidr_prefix: u8,
ifidx: u32,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> bool {
Self::ipv4_route_target_matches(route, address, cidr_prefix, ifidx)
&& route.table == RouteHeader::RT_TABLE_MAIN
&& route.metric == Some(cost.unwrap_or(65535) as u32)
&& route.source_hint == source_hint.map(IpAddr::V4)
}
} }
#[async_trait] #[async_trait]
@@ -387,29 +445,25 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
cidr_prefix: u8, cidr_prefix: u8,
cost: Option<i32>, cost: Option<i32>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut message = RouteMessage::default(); self.add_ipv4_route_with_source_hint(name, address, cidr_prefix, cost, None)
.await
message.header.table = RouteHeader::RT_TABLE_MAIN; }
message.header.protocol = RouteProtocol::Static;
message.header.scope = RouteScope::Universe;
message.header.kind = RouteType::Unicast;
message.header.address_family = AddressFamily::Inet;
// metric
message
.attributes
.push(RouteAttribute::Priority(cost.unwrap_or(65535) as u32));
// output interface
message
.attributes
.push(RouteAttribute::Oif(NetlinkIfConfiger::get_interface_index(
name,
)?));
// source address
message.header.destination_prefix_length = cidr_prefix;
message
.attributes
.push(RouteAttribute::Destination(RouteAddress::Inet(address)));
async fn add_ipv4_route_with_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
let message = NetlinkIfConfiger::ipv4_route_message(
NetlinkIfConfiger::get_interface_index(name)?,
address,
cidr_prefix,
cost,
source_hint,
);
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false) send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false)
} }
@@ -424,10 +478,41 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
for msg in routes { for msg in routes {
let other_route: Route = msg.clone().into(); let other_route: Route = msg.clone().into();
if other_route.destination == std::net::IpAddr::V4(address) if NetlinkIfConfiger::ipv4_route_target_matches(
&& other_route.prefix == cidr_prefix &other_route,
&& other_route.ifindex == Some(ifidx) address,
{ cidr_prefix,
ifidx,
) {
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::DelRoute(msg), true)?;
return Ok(());
}
}
Ok(())
}
async fn remove_ipv4_route_with_cost_and_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
let routes = Self::list_routes()?;
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
for msg in routes {
let other_route: Route = msg.clone().into();
if NetlinkIfConfiger::ipv4_route_exact_matches(
&other_route,
address,
cidr_prefix,
ifidx,
cost,
source_hint,
) {
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::DelRoute(msg), true)?; send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::DelRoute(msg), true)?;
return Ok(()); return Ok(());
} }
@@ -666,6 +751,89 @@ mod tests {
} }
} }
#[test]
fn ipv4_route_message_includes_pref_source_when_source_hint_is_set() {
let source_hint = Ipv4Addr::new(10, 231, 1, 1);
let message = NetlinkIfConfiger::ipv4_route_message(
7,
Ipv4Addr::new(10, 99, 0, 0),
24,
Some(123),
Some(source_hint),
);
assert_eq!(message.header.destination_prefix_length, 24);
assert!(message.attributes.iter().any(|attr| {
matches!(
attr,
RouteAttribute::PrefSource(RouteAddress::Inet(source)) if *source == source_hint
)
}));
assert!(message.attributes.iter().any(|attr| {
matches!(attr, RouteAttribute::Priority(priority) if *priority == 123)
}));
assert!(
message
.attributes
.iter()
.any(|attr| matches!(attr, RouteAttribute::Oif(7)))
);
}
#[test]
fn ipv4_route_exact_match_distinguishes_metric_and_pref_source() {
let address = Ipv4Addr::new(10, 99, 0, 0);
let source_hint = Ipv4Addr::new(10, 99, 0, 1);
let other_source_hint = Ipv4Addr::new(10, 99, 0, 2);
let route: Route =
NetlinkIfConfiger::ipv4_route_message(7, address, 24, Some(123), Some(source_hint))
.into();
assert!(NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(123),
Some(source_hint),
));
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(124),
Some(source_hint),
));
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(123),
Some(other_source_hint),
));
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(123),
None,
));
let mut non_main_table_route = route.clone();
non_main_table_route.table = 100;
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&non_main_table_route,
address,
24,
7,
Some(123),
Some(source_hint),
));
}
struct PrepareEnv {} struct PrepareEnv {}
impl PrepareEnv { impl PrepareEnv {
fn new() -> Self { fn new() -> Self {
+6 -2
View File
@@ -193,6 +193,8 @@ impl IpProxy {
#[cfg(feature = "tun")] #[cfg(feature = "tun")]
type NicCtx = super::virtual_nic::NicCtx; type NicCtx = super::virtual_nic::NicCtx;
#[cfg(all(feature = "tun", mobile))]
use super::virtual_nic::MobileTunSources;
#[cfg(feature = "magic-dns")] #[cfg(feature = "magic-dns")]
struct MagicDnsContainer { struct MagicDnsContainer {
@@ -910,7 +912,8 @@ impl Instance {
close_notifier: Arc<Notify>, close_notifier: Arc<Notify>,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry, shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
) -> Result<NicCtx, Error> { ) -> Result<NicCtx, Error> {
if global_ctx.get_flags().dev_name.is_empty() { let flags = global_ctx.get_flags();
if flags.dev_name.is_empty() {
return Ok(NicCtx::new( return Ok(NicCtx::new(
global_ctx, global_ctx,
peer_manager, peer_manager,
@@ -1730,6 +1733,7 @@ impl Instance {
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>, peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry, shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
fd: i32, fd: i32,
sources: MobileTunSources,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
tracing::info!("setup_nic_ctx_for_mobile, fd: {}", fd); tracing::info!("setup_nic_ctx_for_mobile, fd: {}", fd);
Self::clear_nic_ctx(nic_ctx.clone(), peer_packet_receiver.clone()).await; Self::clear_nic_ctx(nic_ctx.clone(), peer_packet_receiver.clone()).await;
@@ -1747,7 +1751,7 @@ impl Instance {
.await .await
.with_context(|| "create nic ctx failed")?; .with_context(|| "create nic ctx failed")?;
new_nic_ctx new_nic_ctx
.run_for_mobile(fd) .run_for_mobile(fd, sources)
.await .await
.with_context(|| "add ip failed")?; .with_context(|| "add ip failed")?;
+349 -36
View File
@@ -96,6 +96,9 @@ pub struct SharedIfConfigDelta {
pub ipv4_addresses: OwnedItemDelta<Ipv4Inet>, pub ipv4_addresses: OwnedItemDelta<Ipv4Inet>,
pub ipv6_addresses: OwnedItemDelta<Ipv6Inet>, pub ipv6_addresses: OwnedItemDelta<Ipv6Inet>,
pub ipv4_routes: OwnedItemDelta<SharedIpv4Route>, pub ipv4_routes: OwnedItemDelta<SharedIpv4Route>,
pub ipv4_route_removed_old_source_hints: BTreeMap<SharedIpv4Route, Option<Ipv4Addr>>,
pub ipv4_route_source_changed: BTreeSet<SharedIpv4Route>,
pub ipv4_route_source_changed_old_hints: BTreeMap<SharedIpv4Route, Option<Ipv4Addr>>,
pub ipv6_routes: OwnedItemDelta<SharedIpv6Route>, pub ipv6_routes: OwnedItemDelta<SharedIpv6Route>,
pub mtu: Option<SharedMtuChange>, pub mtu: Option<SharedMtuChange>,
} }
@@ -131,6 +134,12 @@ impl SharedIfConfig {
.cloned() .cloned()
.unwrap_or_default(); .unwrap_or_default();
let old_mtu = self.effective_mtu(); let old_mtu = self.effective_mtu();
let source_change_candidates = old_claims
.ipv4_routes
.union(&claims.ipv4_routes)
.cloned()
.collect::<BTreeSet<_>>();
let old_ipv4_route_sources = self.ipv4_route_sources(&source_change_candidates);
let ipv4_addresses = update_owned_items( let ipv4_addresses = update_owned_items(
&mut self.ipv4_address_owners, &mut self.ipv4_address_owners,
@@ -159,11 +168,22 @@ impl SharedIfConfig {
update_member_mtu(&mut self.member_mtu, member_id, claims.mtu); update_member_mtu(&mut self.member_mtu, member_id, claims.mtu);
self.member_claims.insert(member_id, claims); self.member_claims.insert(member_id, claims);
let ipv4_route_removed_old_source_hints =
old_ipv4_route_hints(&old_ipv4_route_sources, &ipv4_routes.removed);
let ipv4_route_source_changed_old_hints =
self.changed_ipv4_route_sources(&old_ipv4_route_sources, &ipv4_routes);
let ipv4_route_source_changed = ipv4_route_source_changed_old_hints
.keys()
.cloned()
.collect();
SharedIfConfigDelta { SharedIfConfigDelta {
ipv4_addresses, ipv4_addresses,
ipv6_addresses, ipv6_addresses,
ipv4_routes, ipv4_routes,
ipv4_route_removed_old_source_hints,
ipv4_route_source_changed,
ipv4_route_source_changed_old_hints,
ipv6_routes, ipv6_routes,
mtu: mtu_delta(old_mtu, self.effective_mtu()), mtu: mtu_delta(old_mtu, self.effective_mtu()),
} }
@@ -173,8 +193,10 @@ impl SharedIfConfig {
&mut self, &mut self,
member_id: SharedVirtualNicMemberId, member_id: SharedVirtualNicMemberId,
) -> Option<SharedIfConfigDelta> { ) -> Option<SharedIfConfigDelta> {
let old_claims = self.member_claims.remove(&member_id)?; let old_claims = self.member_claims.get(&member_id).cloned()?;
let old_mtu = self.effective_mtu(); let old_mtu = self.effective_mtu();
let old_ipv4_route_sources = self.ipv4_route_sources(&old_claims.ipv4_routes);
self.member_claims.remove(&member_id);
let ipv4_addresses = remove_owned_items( let ipv4_addresses = remove_owned_items(
&mut self.ipv4_address_owners, &mut self.ipv4_address_owners,
@@ -198,11 +220,22 @@ impl SharedIfConfig {
); );
self.member_mtu.remove(&member_id); self.member_mtu.remove(&member_id);
let ipv4_route_removed_old_source_hints =
old_ipv4_route_hints(&old_ipv4_route_sources, &ipv4_routes.removed);
let ipv4_route_source_changed_old_hints =
self.changed_ipv4_route_sources(&old_ipv4_route_sources, &ipv4_routes);
let ipv4_route_source_changed = ipv4_route_source_changed_old_hints
.keys()
.cloned()
.collect();
Some(SharedIfConfigDelta { Some(SharedIfConfigDelta {
ipv4_addresses, ipv4_addresses,
ipv6_addresses, ipv6_addresses,
ipv4_routes, ipv4_routes,
ipv4_route_removed_old_source_hints,
ipv4_route_source_changed,
ipv4_route_source_changed_old_hints,
ipv6_routes, ipv6_routes,
mtu: mtu_delta(old_mtu, self.effective_mtu()), mtu: mtu_delta(old_mtu, self.effective_mtu()),
}) })
@@ -250,6 +283,57 @@ impl SharedIfConfig {
.cloned() .cloned()
.unwrap_or_default() .unwrap_or_default()
} }
fn ipv4_route_source_hint(&self, route: &SharedIpv4Route) -> Option<Ipv4Addr> {
let owners = self.ipv4_route_owners.get(route)?;
let route_inet = Ipv4Inet::new(route.address, route.prefix).ok();
let mut fallback = None;
for owner in owners {
let Some(claims) = self.member_claims.get(owner) else {
continue;
};
for address in &claims.ipv4_addresses {
fallback.get_or_insert(address.address());
if route_inet
.as_ref()
.is_some_and(|route_inet| route_inet.contains(&address.address()))
{
return Some(address.address());
}
}
}
fallback
}
fn ipv4_route_sources(
&self,
routes: &BTreeSet<SharedIpv4Route>,
) -> BTreeMap<SharedIpv4Route, Option<Ipv4Addr>> {
routes
.iter()
.map(|route| (route.clone(), self.ipv4_route_source_hint(route)))
.collect()
}
fn changed_ipv4_route_sources(
&self,
old_sources: &BTreeMap<SharedIpv4Route, Option<Ipv4Addr>>,
route_delta: &OwnedItemDelta<SharedIpv4Route>,
) -> BTreeMap<SharedIpv4Route, Option<Ipv4Addr>> {
old_sources
.iter()
.filter(|(route, old_source)| {
!route_delta.added.contains(*route)
&& !route_delta.removed.contains(*route)
&& self.ipv4_route_owners.contains_key(*route)
&& self.ipv4_route_source_hint(route) != **old_source
})
.map(|(route, old_source)| (route.clone(), *old_source))
.collect()
}
} }
pub struct SharedVirtualNic { pub struct SharedVirtualNic {
@@ -509,7 +593,24 @@ impl SharedVirtualNic {
let nic = self.nic.lock().await; let nic = self.nic.lock().await;
for route in &delta.ipv4_routes.removed { for route in &delta.ipv4_routes.removed {
ignore_removed_ifcfg_not_found(nic.remove_route(route.address, route.prefix).await)?; let source_hint = delta
.ipv4_route_removed_old_source_hints
.get(route)
.copied()
.flatten();
ignore_removed_ifcfg_not_found(
remove_shared_ipv4_route(&nic, route, source_hint).await,
)?;
}
for route in &delta.ipv4_route_source_changed {
let source_hint = delta
.ipv4_route_source_changed_old_hints
.get(route)
.copied()
.flatten();
ignore_removed_ifcfg_not_found(
remove_shared_ipv4_route(&nic, route, source_hint).await,
)?;
} }
for route in &delta.ipv6_routes.removed { for route in &delta.ipv6_routes.removed {
ignore_removed_ifcfg_not_found( ignore_removed_ifcfg_not_found(
@@ -531,8 +632,10 @@ impl SharedVirtualNic {
.await?; .await?;
} }
for route in &delta.ipv4_routes.added { for route in &delta.ipv4_routes.added {
nic.add_route_with_cost(route.address, route.prefix, route.cost) add_shared_ipv4_route(&nic, route, _next_ifcfg).await?;
.await?; }
for route in &delta.ipv4_route_source_changed {
add_shared_ipv4_route(&nic, route, _next_ifcfg).await?;
} }
for route in &delta.ipv6_routes.added { for route in &delta.ipv6_routes.added {
nic.add_ipv6_route_with_cost(route.address, route.prefix, route.cost) nic.add_ipv6_route_with_cost(route.address, route.prefix, route.cost)
@@ -548,8 +651,7 @@ impl SharedVirtualNic {
if !delta.ipv4_addresses.removed.is_empty() { if !delta.ipv4_addresses.removed.is_empty() {
for route in _next_ifcfg.ipv4_route_owners.keys() { for route in _next_ifcfg.ipv4_route_owners.keys() {
ignore_added_ifcfg_already_exists( ignore_added_ifcfg_already_exists(
nic.add_route_with_cost(route.address, route.prefix, route.cost) add_shared_ipv4_route(&nic, route, _next_ifcfg).await,
.await,
)?; )?;
} }
} }
@@ -631,9 +733,7 @@ impl SharedVirtualNic {
dispatcher: &SharedVirtualNicDispatcher, dispatcher: &SharedVirtualNicDispatcher,
) -> Result<(), Error> { ) -> Result<(), Error> {
for (member_id, claims) in &self.ifcfg.member_claims { for (member_id, claims) in &self.ifcfg.member_claims {
dispatcher dispatcher.update_sources(*member_id, claims).await?;
.update_sources(*member_id, &claims.ipv4_addresses, &claims.ipv6_addresses)
.await?;
} }
Ok(()) Ok(())
} }
@@ -644,17 +744,9 @@ impl SharedVirtualNic {
old_claims: &SharedIfConfigClaims, old_claims: &SharedIfConfigClaims,
next_claims: &SharedIfConfigClaims, next_claims: &SharedIfConfigClaims,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut active_ipv4_addresses = old_claims.ipv4_addresses.clone(); let active_claims = dispatcher_claims_for_ifcfg_transition(old_claims, next_claims);
active_ipv4_addresses.extend(next_claims.ipv4_addresses.iter().copied()); self.sync_dispatcher_sources_for_claims(member_id, &active_claims)
let mut active_ipv6_addresses = old_claims.ipv6_addresses.clone(); .await
active_ipv6_addresses.extend(next_claims.ipv6_addresses.iter().copied());
self.sync_dispatcher_sources_for_addresses(
member_id,
&active_ipv4_addresses,
&active_ipv6_addresses,
)
.await
} }
async fn sync_dispatcher_sources_for_member( async fn sync_dispatcher_sources_for_member(
@@ -662,24 +754,17 @@ impl SharedVirtualNic {
member_id: SharedVirtualNicMemberId, member_id: SharedVirtualNicMemberId,
claims: &SharedIfConfigClaims, claims: &SharedIfConfigClaims,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.sync_dispatcher_sources_for_addresses( self.sync_dispatcher_sources_for_claims(member_id, claims)
member_id, .await
&claims.ipv4_addresses,
&claims.ipv6_addresses,
)
.await
} }
async fn sync_dispatcher_sources_for_addresses( async fn sync_dispatcher_sources_for_claims(
&self, &self,
member_id: SharedVirtualNicMemberId, member_id: SharedVirtualNicMemberId,
ipv4_addresses: &BTreeSet<Ipv4Inet>, claims: &SharedIfConfigClaims,
ipv6_addresses: &BTreeSet<Ipv6Inet>,
) -> Result<(), Error> { ) -> Result<(), Error> {
if let Some(dispatcher) = &self.dispatcher { if let Some(dispatcher) = &self.dispatcher {
dispatcher dispatcher.update_sources(member_id, claims).await?;
.update_sources(member_id, ipv4_addresses, ipv6_addresses)
.await?;
} }
Ok(()) Ok(())
} }
@@ -695,6 +780,75 @@ impl SharedVirtualNic {
} }
} }
fn dispatcher_claims_for_ifcfg_transition(
old_claims: &SharedIfConfigClaims,
next_claims: &SharedIfConfigClaims,
) -> SharedIfConfigClaims {
let mut claims = SharedIfConfigClaims::default();
claims
.ipv4_addresses
.extend(old_claims.ipv4_addresses.iter().copied());
claims
.ipv4_addresses
.extend(next_claims.ipv4_addresses.iter().copied());
claims
.ipv6_addresses
.extend(old_claims.ipv6_addresses.iter().copied());
claims
.ipv6_addresses
.extend(next_claims.ipv6_addresses.iter().copied());
claims
.ipv4_routes
.extend(old_claims.ipv4_routes.iter().cloned());
claims
.ipv4_routes
.extend(next_claims.ipv4_routes.iter().cloned());
claims
.ipv6_routes
.extend(old_claims.ipv6_routes.iter().cloned());
claims
.ipv6_routes
.extend(next_claims.ipv6_routes.iter().cloned());
claims
}
async fn add_shared_ipv4_route(
nic: &VirtualNic,
route: &SharedIpv4Route,
ifcfg: &SharedIfConfig,
) -> Result<(), Error> {
nic.add_route_with_cost_and_source_hint(
route.address,
route.prefix,
route.cost,
ifcfg.ipv4_route_source_hint(route),
)
.await
}
async fn remove_shared_ipv4_route(
nic: &VirtualNic,
route: &SharedIpv4Route,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
nic.remove_route_with_cost_and_source_hint(route.address, route.prefix, route.cost, source_hint)
.await
}
fn old_ipv4_route_hints(
old_sources: &BTreeMap<SharedIpv4Route, Option<Ipv4Addr>>,
routes: &BTreeSet<SharedIpv4Route>,
) -> BTreeMap<SharedIpv4Route, Option<Ipv4Addr>> {
routes
.iter()
.filter_map(|route| {
old_sources
.get(route)
.map(|source_hint| (route.clone(), *source_hint))
})
.collect()
}
fn ignore_removed_ifcfg_not_found(result: Result<(), Error>) -> Result<(), Error> { fn ignore_removed_ifcfg_not_found(result: Result<(), Error>) -> Result<(), Error> {
match result { match result {
Err(Error::NotFound) => Ok(()), Err(Error::NotFound) => Ok(()),
@@ -897,8 +1051,7 @@ impl SharedVirtualNicMember {
} }
#[cfg(mobile)] #[cfg(mobile)]
pub async fn add_mobile_source_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> { pub async fn add_mobile_source_ip(&self, ip: Ipv4Inet) -> Result<(), Error> {
let ip = ipv4_inet(ip, cidr)?;
self.update_claims_for_mobile(|claims| { self.update_claims_for_mobile(|claims| {
claims.ipv4_addresses.insert(ip); claims.ipv4_addresses.insert(ip);
}) })
@@ -906,14 +1059,29 @@ impl SharedVirtualNicMember {
} }
#[cfg(mobile)] #[cfg(mobile)]
pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Addr, cidr: i32) -> Result<(), Error> { pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Inet) -> Result<(), Error> {
let ip = ipv6_inet(ip, cidr)?;
self.update_claims_for_mobile(|claims| { self.update_claims_for_mobile(|claims| {
claims.ipv6_addresses.insert(ip); claims.ipv6_addresses.insert(ip);
}) })
.await .await
} }
#[cfg(mobile)]
pub async fn add_mobile_source_ipv4_route(&self, route: SharedIpv4Route) -> Result<(), Error> {
self.update_claims_for_mobile(|claims| {
claims.ipv4_routes.insert(route);
})
.await
}
#[cfg(mobile)]
pub async fn add_mobile_source_ipv6_route(&self, route: SharedIpv6Route) -> Result<(), Error> {
self.update_claims_for_mobile(|claims| {
claims.ipv6_routes.insert(route);
})
.await
}
pub async fn remove_ipv6(&self, ip: Option<Ipv6Inet>) -> Result<(), Error> { pub async fn remove_ipv6(&self, ip: Option<Ipv6Inet>) -> Result<(), Error> {
self.update_claims(|claims| match ip { self.update_claims(|claims| match ip {
Some(ip) => { Some(ip) => {
@@ -1292,6 +1460,17 @@ mod tests {
} }
} }
fn claims_with_ipv4_address_and_route(
address: Ipv4Inet,
route: SharedIpv4Route,
) -> SharedIfConfigClaims {
SharedIfConfigClaims {
ipv4_addresses: BTreeSet::from([address]),
ipv4_routes: BTreeSet::from([route]),
..Default::default()
}
}
fn virtual_nic_config() -> VirtualNicConfig { fn virtual_nic_config() -> VirtualNicConfig {
VirtualNicConfig::new(String::new(), 1500, NetNS::new(None)) VirtualNicConfig::new(String::new(), 1500, NetNS::new(None))
} }
@@ -1385,6 +1564,140 @@ mod tests {
); );
} }
#[test]
fn ipv4_route_source_hint_prefers_address_inside_route() {
let route = SharedIpv4Route::new(Ipv4Addr::new(10, 90, 1, 0), 24, None);
let member = member_id(1);
let mut ifcfg = SharedIfConfig::default();
ifcfg.apply_member_claims(
member,
SharedIfConfigClaims {
ipv4_addresses: BTreeSet::from([
Ipv4Inet::from_str("10.1.1.1/24").unwrap(),
Ipv4Inet::from_str("10.90.1.1/24").unwrap(),
]),
ipv4_routes: BTreeSet::from([route.clone()]),
..Default::default()
},
);
assert_eq!(
ifcfg.ipv4_route_source_hint(&route),
Some(Ipv4Addr::new(10, 90, 1, 1))
);
}
#[test]
fn adding_better_ipv4_route_owner_marks_source_change() {
let route = SharedIpv4Route::new(Ipv4Addr::new(10, 90, 2, 0), 24, None);
let first = member_id(1);
let second = member_id(2);
let mut ifcfg = SharedIfConfig::default();
ifcfg.apply_member_claims(
first,
claims_with_ipv4_address_and_route(
Ipv4Inet::from_str("10.1.2.1/24").unwrap(),
route.clone(),
),
);
let delta = ifcfg.apply_member_claims(
second,
claims_with_ipv4_address_and_route(
Ipv4Inet::from_str("10.90.2.1/24").unwrap(),
route.clone(),
),
);
assert!(delta.ipv4_routes.added.is_empty());
assert_eq!(
delta.ipv4_route_source_changed,
BTreeSet::from([route.clone()])
);
assert_eq!(
delta.ipv4_route_source_changed_old_hints,
BTreeMap::from([(route.clone(), Some(Ipv4Addr::new(10, 1, 2, 1)))])
);
assert_eq!(
ifcfg.ipv4_route_source_hint(&route),
Some(Ipv4Addr::new(10, 90, 2, 1))
);
}
#[test]
fn removing_ipv4_route_owner_marks_source_change_when_route_remains() {
let route = SharedIpv4Route::new(Ipv4Addr::new(10, 90, 3, 0), 24, None);
let first = member_id(1);
let second = member_id(2);
let mut ifcfg = SharedIfConfig::default();
ifcfg.apply_member_claims(
first,
claims_with_ipv4_address_and_route(
Ipv4Inet::from_str("10.1.3.1/24").unwrap(),
route.clone(),
),
);
ifcfg.apply_member_claims(
second,
claims_with_ipv4_address_and_route(
Ipv4Inet::from_str("10.90.3.1/24").unwrap(),
route.clone(),
),
);
let delta = ifcfg.remove_member(second).unwrap();
assert!(delta.ipv4_routes.removed.is_empty());
assert_eq!(
delta.ipv4_route_source_changed,
BTreeSet::from([route.clone()])
);
assert_eq!(
delta.ipv4_route_source_changed_old_hints,
BTreeMap::from([(route.clone(), Some(Ipv4Addr::new(10, 90, 3, 1)))])
);
assert_eq!(
ifcfg.ipv4_route_source_hint(&route),
Some(Ipv4Addr::new(10, 1, 3, 1))
);
}
#[test]
fn removing_ipv4_route_records_old_source_hint_per_cost() {
let kept_route = SharedIpv4Route::new(Ipv4Addr::new(10, 90, 4, 0), 24, Some(10));
let removed_route = SharedIpv4Route::new(Ipv4Addr::new(10, 90, 4, 0), 24, Some(20));
let member = member_id(1);
let address = Ipv4Inet::from_str("10.90.4.1/24").unwrap();
let mut ifcfg = SharedIfConfig::default();
ifcfg.apply_member_claims(
member,
SharedIfConfigClaims {
ipv4_addresses: BTreeSet::from([address]),
ipv4_routes: BTreeSet::from([kept_route.clone(), removed_route.clone()]),
..Default::default()
},
);
let delta = ifcfg.apply_member_claims(
member,
SharedIfConfigClaims {
ipv4_addresses: BTreeSet::from([address]),
ipv4_routes: BTreeSet::from([kept_route.clone()]),
..Default::default()
},
);
assert_eq!(
delta.ipv4_routes.removed,
BTreeSet::from([removed_route.clone()])
);
assert_eq!(
delta.ipv4_route_removed_old_source_hints,
BTreeMap::from([(removed_route, Some(Ipv4Addr::new(10, 90, 4, 1)))])
);
}
#[test] #[test]
fn shared_virtual_nic_wraps_virtual_nic_and_tracks_ifcfg() { fn shared_virtual_nic_wraps_virtual_nic_and_tracks_ifcfg() {
let mut shared_nic = SharedVirtualNic::new(virtual_nic_config()); let mut shared_nic = SharedVirtualNic::new(virtual_nic_config());
File diff suppressed because it is too large Load Diff
+139 -10
View File
@@ -46,10 +46,82 @@ use crate::common::ifcfg::RegistryManager;
#[cfg(test)] #[cfg(test)]
use super::shared_virtual_nic::SharedVirtualNic; use super::shared_virtual_nic::SharedVirtualNic;
#[cfg(mobile)]
use super::shared_virtual_nic::{SharedIpv4Route, SharedIpv6Route};
use super::shared_virtual_nic::{ use super::shared_virtual_nic::{
SharedVirtualNicMember, SharedVirtualNicMemberId, SharedVirtualNicRegistry, SharedVirtualNicMember, SharedVirtualNicMemberId, SharedVirtualNicRegistry,
}; };
#[cfg(mobile)]
#[derive(Clone, Debug, Default)]
pub struct MobileTunSources {
pub ipv4: Vec<Ipv4Inet>,
pub ipv6: Vec<Ipv6Inet>,
pub ipv4_routes: Vec<SharedIpv4Route>,
pub ipv6_routes: Vec<SharedIpv6Route>,
}
#[cfg(mobile)]
impl MobileTunSources {
pub fn parse(
ipv4: Vec<String>,
ipv6: Vec<String>,
ipv4_routes: Vec<String>,
ipv6_routes: Vec<String>,
) -> Result<Self, Error> {
let ipv4 = ipv4
.into_iter()
.map(|addr| {
addr.parse::<Ipv4Inet>()
.map_err(|err| anyhow::anyhow!("invalid IPv4 source {addr}: {err}").into())
})
.collect::<Result<Vec<_>, Error>>()?;
let ipv6 = ipv6
.into_iter()
.map(|addr| {
addr.parse::<Ipv6Inet>()
.map_err(|err| anyhow::anyhow!("invalid IPv6 source {addr}: {err}").into())
})
.collect::<Result<Vec<_>, Error>>()?;
let ipv4_routes = ipv4_routes
.into_iter()
.map(|route| {
let route = route.parse::<Ipv4Inet>().map_err(|err| {
let err: Error =
anyhow::anyhow!("invalid IPv4 route source {route}: {err}").into();
err
})?;
Ok(SharedIpv4Route::new(
route.address(),
route.network_length(),
None,
))
})
.collect::<Result<Vec<_>, Error>>()?;
let ipv6_routes = ipv6_routes
.into_iter()
.map(|route| {
let route = route.parse::<Ipv6Inet>().map_err(|err| {
let err: Error =
anyhow::anyhow!("invalid IPv6 route source {route}: {err}").into();
err
})?;
Ok(SharedIpv6Route::new(
route.address(),
route.network_length(),
None,
))
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(Self {
ipv4,
ipv6,
ipv4_routes,
ipv6_routes,
})
}
}
pin_project! { pin_project! {
pub struct TunStream { pub struct TunStream {
#[pin] #[pin]
@@ -765,10 +837,21 @@ impl VirtualNic {
address: Ipv4Addr, address: Ipv4Addr,
cidr: u8, cidr: u8,
cost: Option<i32>, cost: Option<i32>,
) -> Result<(), Error> {
self.add_route_with_cost_and_source_hint(address, cidr, cost, None)
.await
}
pub async fn add_route_with_cost_and_source_hint(
&self,
address: Ipv4Addr,
cidr: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let _g = self.config.net_ns.guard(); let _g = self.config.net_ns.guard();
self.ifcfg self.ifcfg
.add_ipv4_route(self.ifname(), address, cidr, cost) .add_ipv4_route_with_source_hint(self.ifname(), address, cidr, cost, source_hint)
.await?; .await?;
Ok(()) Ok(())
} }
@@ -781,6 +864,26 @@ impl VirtualNic {
Ok(()) Ok(())
} }
pub async fn remove_route_with_cost_and_source_hint(
&self,
address: Ipv4Addr,
cidr: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
let _g = self.config.net_ns.guard();
self.ifcfg
.remove_ipv4_route_with_cost_and_source_hint(
self.ifname(),
address,
cidr,
cost,
source_hint,
)
.await?;
Ok(())
}
pub async fn add_ipv6_route(&self, address: Ipv6Addr, cidr: u8) -> Result<(), Error> { pub async fn add_ipv6_route(&self, address: Ipv6Addr, cidr: u8) -> Result<(), Error> {
self.add_ipv6_route_with_cost(address, cidr, None).await self.add_ipv6_route_with_cost(address, cidr, None).await
} }
@@ -1031,18 +1134,34 @@ impl NicBackend {
} }
#[cfg(mobile)] #[cfg(mobile)]
pub async fn add_mobile_source_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> { pub async fn add_mobile_source_ip(&self, ip: Ipv4Inet) -> Result<(), Error> {
match self { match self {
Self::Dedicated(_) => Ok(()), Self::Dedicated(_) => Ok(()),
Self::Shared(member) => member.add_mobile_source_ip(ip, cidr).await, Self::Shared(member) => member.add_mobile_source_ip(ip).await,
} }
} }
#[cfg(mobile)] #[cfg(mobile)]
pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Addr, cidr: i32) -> Result<(), Error> { pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Inet) -> Result<(), Error> {
match self { match self {
Self::Dedicated(_) => Ok(()), Self::Dedicated(_) => Ok(()),
Self::Shared(member) => member.add_mobile_source_ipv6(ip, cidr).await, Self::Shared(member) => member.add_mobile_source_ipv6(ip).await,
}
}
#[cfg(mobile)]
pub async fn add_mobile_source_ipv4_route(&self, route: SharedIpv4Route) -> Result<(), Error> {
match self {
Self::Dedicated(_) => Ok(()),
Self::Shared(member) => member.add_mobile_source_ipv4_route(route).await,
}
}
#[cfg(mobile)]
pub async fn add_mobile_source_ipv6_route(&self, route: SharedIpv6Route) -> Result<(), Error> {
match self {
Self::Dedicated(_) => Ok(()),
Self::Shared(member) => member.add_mobile_source_ipv6_route(route).await,
} }
} }
} }
@@ -1739,7 +1858,11 @@ impl NicCtx {
} }
#[cfg(mobile)] #[cfg(mobile)]
pub async fn run_for_mobile(&mut self, tun_fd: std::os::fd::RawFd) -> Result<(), Error> { pub async fn run_for_mobile(
&mut self,
tun_fd: std::os::fd::RawFd,
sources: MobileTunSources,
) -> Result<(), Error> {
let (tunnel, ifname) = match self.backend.create_dev_for_mobile(tun_fd).await { let (tunnel, ifname) = match self.backend.create_dev_for_mobile(tun_fd).await {
Ok(ret) => { Ok(ret) => {
let ifname = self let ifname = self
@@ -1756,14 +1879,20 @@ impl NicCtx {
} }
}; };
if let Some(ipv4_addr) = self.global_ctx.get_ipv4() { for ipv4_addr in sources.ipv4 {
self.backend.add_mobile_source_ip(ipv4_addr).await?;
}
for ipv6_addr in sources.ipv6 {
self.backend.add_mobile_source_ipv6(ipv6_addr).await?;
}
for ipv4_route in sources.ipv4_routes {
self.backend self.backend
.add_mobile_source_ip(ipv4_addr.address(), ipv4_addr.network_length() as i32) .add_mobile_source_ipv4_route(ipv4_route)
.await?; .await?;
} }
if let Some(ipv6_addr) = self.global_ctx.get_ipv6() { for ipv6_route in sources.ipv6_routes {
self.backend self.backend
.add_mobile_source_ipv6(ipv6_addr.address(), ipv6_addr.network_length() as i32) .add_mobile_source_ipv6_route(ipv6_route)
.await?; .await?;
} }
+22
View File
@@ -302,6 +302,28 @@ impl NetworkInstanceManager {
.and_then(|instance| instance.value().get_api_service()) .and_then(|instance| instance.value().get_api_service())
} }
#[cfg(mobile)]
pub fn set_tun_fd(
&self,
instance_id: &uuid::Uuid,
fd: i32,
sources: crate::instance::virtual_nic::MobileTunSources,
) -> Result<(), anyhow::Error> {
let sender = self
.instance_map
.get(instance_id)
.ok_or_else(|| anyhow::anyhow!("instance not found"))?
.get_tun_fd_sender()
.ok_or_else(|| anyhow::anyhow!("tun fd sender not found"))?;
sender
.try_send(Some(crate::launcher::MobileTunFd { fd, sources }))
.map_err(|e| anyhow::anyhow!("failed to send tun fd: {}", e))?;
Ok(())
}
#[cfg(not(mobile))]
pub fn set_tun_fd(&self, instance_id: &uuid::Uuid, fd: i32) -> Result<(), anyhow::Error> { pub fn set_tun_fd(&self, instance_id: &uuid::Uuid, fd: i32) -> Result<(), anyhow::Error> {
let sender = self let sender = self
.instance_map .instance_map
+15 -2
View File
@@ -6,6 +6,8 @@ use crate::common::config::{
use crate::gateway::socks5::Socks5Server; use crate::gateway::socks5::Socks5Server;
#[cfg(feature = "ffi-dataplane")] #[cfg(feature = "ffi-dataplane")]
pub use crate::gateway::socks5::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket}; pub use crate::gateway::socks5::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
#[cfg(mobile)]
use crate::instance::virtual_nic::MobileTunSources;
use crate::proto::api::{self, manage}; use crate::proto::api::{self, manage};
use crate::proto::rpc_types::controller::BaseController; use crate::proto::rpc_types::controller::BaseController;
use crate::rpc_service::InstanceRpcService; use crate::rpc_service::InstanceRpcService;
@@ -36,6 +38,16 @@ use tokio::{
pub type MyNodeInfo = crate::proto::api::manage::MyNodeInfo; pub type MyNodeInfo = crate::proto::api::manage::MyNodeInfo;
type ArcMutApiService = Arc<RwLock<Option<Arc<dyn InstanceRpcService>>>>; type ArcMutApiService = Arc<RwLock<Option<Arc<dyn InstanceRpcService>>>>;
#[cfg(mobile)]
#[derive(Clone, Debug)]
pub struct MobileTunFd {
pub fd: i32,
pub sources: MobileTunSources,
}
#[cfg(mobile)]
type TunFd = Option<MobileTunFd>;
#[cfg(not(mobile))]
type TunFd = Option<i32>; type TunFd = Option<i32>;
#[derive(serde::Serialize, Clone)] #[derive(serde::Serialize, Clone)]
@@ -135,11 +147,12 @@ impl EasyTierLauncher {
peer_mgr.clone(), peer_mgr.clone(),
peer_packet_receiver.clone(), peer_packet_receiver.clone(),
shared_virtual_nic_registry.clone(), shared_virtual_nic_registry.clone(),
tun_fd, tun_fd.fd,
tun_fd.sources,
) )
.await .await
{ {
tracing::error!(?err, tun_fd, "setup mobile nic ctx failed"); tracing::error!(?err, fd = tun_fd.fd, "setup mobile nic ctx failed");
} }
} }
}); });
@@ -5,6 +5,7 @@ import android.net.VpnService
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.Bundle import android.os.Bundle
import android.system.OsConstants.AF_INET6
import java.net.InetAddress import java.net.InetAddress
import java.util.Arrays import java.util.Arrays
@@ -115,7 +116,7 @@ class TauriVpnService : VpnService() {
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string") if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
builder.addAddress(ipParts[0], ipParts[1].toInt()) builder.addAddress(ipParts[0], ipParts[1].toInt())
} }
builder.addAddress("fd00::1", 128) builder.allowFamily(AF_INET6)
builder.setMtu(mtu) builder.setMtu(mtu)
dns?.let { builder.addDnsServer(it) } dns?.let { builder.addDnsServer(it) }