mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 09:35:41 +00:00
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:
@@ -66,6 +66,9 @@ static RPC_SERVER: once_cell::sync::Lazy<Mutex<Option<RpcServer>>> =
|
||||
static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> =
|
||||
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 {
|
||||
() => {{
|
||||
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]
|
||||
fn easytier_version() -> Result<String, String> {
|
||||
Ok(easytier::VERSION.to_string())
|
||||
@@ -100,6 +114,8 @@ fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), Strin
|
||||
|
||||
#[tauri::command]
|
||||
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())?;
|
||||
Ok(toml.dump())
|
||||
}
|
||||
@@ -117,6 +133,8 @@ async fn run_network_instance(
|
||||
cfg: NetworkConfig,
|
||||
save: bool,
|
||||
) -> Result<(), String> {
|
||||
let mut cfg = cfg;
|
||||
normalize_network_config_for_runtime(&mut cfg);
|
||||
let client_manager = get_client_manager!()?;
|
||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
@@ -155,16 +173,84 @@ async fn set_logging_level(level: String) -> Result<(), String> {
|
||||
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]
|
||||
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 {
|
||||
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 errors = Vec::new();
|
||||
for uuid in get_client_manager!()?.get_enabled_instances_for_tun_fd() {
|
||||
match instance_manager.set_tun_fd(&uuid, fd) {
|
||||
for uuid in target_ids {
|
||||
#[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(()) => {
|
||||
success_count += 1;
|
||||
}
|
||||
@@ -1210,10 +1296,12 @@ mod manager {
|
||||
) -> anyhow::Result<()> {
|
||||
self.storage.network_configs.clear();
|
||||
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(
|
||||
instance_id.parse()?,
|
||||
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
||||
GUIConfig::new(instance_id.to_string(), config, stored.source),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,23 @@ export async function setLoggingLevel(level: string) {
|
||||
return await invoke('set_logging_level', { level })
|
||||
}
|
||||
|
||||
export async function setTunFd(fd: number) {
|
||||
return await invoke('set_tun_fd', { fd })
|
||||
export interface TunFdInstanceSources {
|
||||
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() {
|
||||
@@ -110,7 +125,7 @@ export async function sendConfigs(enabledNetworks: string[]) {
|
||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||
source,
|
||||
})),
|
||||
enabledNetworks
|
||||
enabledNetworks,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { addPluginListener } from '@tauri-apps/api/core'
|
||||
import { Utils } from 'easytier-frontend-lib'
|
||||
import { get_vpn_status, prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
import type { TunFdInstanceSources } from './backend'
|
||||
|
||||
type Route = NetworkTypes.Route
|
||||
|
||||
@@ -12,6 +13,8 @@ interface vpnStatus {
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
dns: string | null | undefined
|
||||
instanceIds: string[]
|
||||
instanceSources: TunFdInstanceSources[]
|
||||
}
|
||||
|
||||
let dhcpPollingTimer: NodeJS.Timeout | null = null
|
||||
@@ -26,6 +29,8 @@ const curVpnStatus: vpnStatus = {
|
||||
ipv4Cidr: undefined,
|
||||
routes: [],
|
||||
dns: undefined,
|
||||
instanceIds: [],
|
||||
instanceSources: [],
|
||||
}
|
||||
|
||||
async function requestVpnPermission() {
|
||||
@@ -50,6 +55,8 @@ function resetVpnConfigStatus() {
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
curVpnStatus.instanceIds = []
|
||||
curVpnStatus.instanceSources = []
|
||||
}
|
||||
|
||||
function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) {
|
||||
@@ -59,13 +66,14 @@ function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_statu
|
||||
return
|
||||
}
|
||||
|
||||
curVpnStatus.ipv4Addrs = status?.ipv4Addrs?.length
|
||||
const nativeIpv4Addrs = status?.ipv4Addrs?.length
|
||||
? [...status.ipv4Addrs]
|
||||
: status?.ipv4Addr
|
||||
? [status.ipv4Addr]
|
||||
: []
|
||||
curVpnStatus.ipv4Addrs = nativeIpv4Addrs
|
||||
|
||||
const ipv4WithCidr = status?.ipv4Addr
|
||||
const ipv4WithCidr = curVpnStatus.ipv4Addrs[0]
|
||||
if (ipv4WithCidr?.length) {
|
||||
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
@@ -107,14 +115,22 @@ async function doStopVpn(force = false) {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
const primaryIpv4 = ipv4Addrs[0]
|
||||
const [ipv4Addr, cidr] = primaryIpv4.split('/')
|
||||
console.log('start vpn service', ipv4Addrs, routes, dns)
|
||||
const [ipv4Addr, cidr] = ipv4Addrs[0].split('/')
|
||||
curVpnStatus.instanceIds = [...instanceIds]
|
||||
curVpnStatus.instanceSources = [...instanceSources]
|
||||
console.log('start vpn service', ipv4Addrs, routes, dns, instanceIds)
|
||||
const request = {
|
||||
ipv4Addr: primaryIpv4,
|
||||
ipv4Addrs,
|
||||
@@ -145,6 +161,8 @@ async function doStartVpn(ipv4Addrs: string[], routes: string[], dns?: string) {
|
||||
curVpnStatus.ipv4Cidr = Number(cidr)
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.dns = dns
|
||||
curVpnStatus.instanceIds = [...instanceIds]
|
||||
curVpnStatus.instanceSources = [...instanceSources]
|
||||
}
|
||||
|
||||
function scheduleVpnConfigRetry(instanceId: string) {
|
||||
@@ -164,7 +182,7 @@ async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -218,12 +236,73 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
|
||||
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) {
|
||||
const info = response.info as any
|
||||
const map = info?.map ?? info
|
||||
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) {
|
||||
pendingVpnConfigInstanceId = instanceId
|
||||
if (!vpnConfigSyncTask) {
|
||||
@@ -273,6 +352,7 @@ async function applyNetworkInstanceChange(instanceId: string) {
|
||||
}
|
||||
|
||||
const ipv4Addrs: string[] = []
|
||||
const instanceSources: TunFdInstanceSources[] = []
|
||||
const routes = new Set<string>()
|
||||
let dns: string | undefined
|
||||
const retryInstanceIds: string[] = []
|
||||
@@ -308,8 +388,26 @@ async function applyNetworkInstanceChange(instanceId: string) {
|
||||
network_length = 24
|
||||
}
|
||||
|
||||
ipv4Addrs.push(`${virtual_ip}/${network_length}`)
|
||||
getRoutesForVpn(curNetworkInfo?.routes, config).forEach(route => routes.add(route))
|
||||
const sourceIpv4 = `${virtual_ip}/${network_length}`
|
||||
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) {
|
||||
dns = '100.100.100.101'
|
||||
}
|
||||
@@ -330,10 +428,14 @@ async function applyNetworkInstanceChange(instanceId: string) {
|
||||
|
||||
const sortedIpv4Addrs = [...ipv4Addrs].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 routesChanged = JSON.stringify(sortedRoutes) !== JSON.stringify(curVpnStatus.routes)
|
||||
const dnsChanged = dns != curVpnStatus.dns
|
||||
const configChanged = ipChanged || routesChanged || dnsChanged
|
||||
const dnsChanged = dns !== curVpnStatus.dns
|
||||
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
|
||||
|
||||
if (shouldStartVpn || configChanged) {
|
||||
@@ -357,7 +459,7 @@ async function applyNetworkInstanceChange(instanceId: string) {
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns)
|
||||
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns, sortedInstanceIds, sortedInstanceSources)
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error && e.message === 'need_prepare') {
|
||||
|
||||
Reference in New Issue
Block a user