mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-03 17:45:44 +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>>> =
|
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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")?;
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
Reference in New Issue
Block a user