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') {
|
||||
|
||||
@@ -46,12 +46,28 @@ impl IfConfiguerTrait for MacIfConfiger {
|
||||
cidr_prefix: u8,
|
||||
cost: Option<i32>,
|
||||
) -> 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(
|
||||
format!(
|
||||
"route -n add {} -netmask {} -interface {} -hopcount {}",
|
||||
"route -n add {} -netmask {} -interface {}{} -hopcount {}",
|
||||
address,
|
||||
cidr_to_subnet_mask(cidr_prefix),
|
||||
name,
|
||||
source_hint,
|
||||
cost.unwrap_or(7)
|
||||
)
|
||||
.as_str(),
|
||||
|
||||
@@ -31,6 +31,16 @@ pub trait IfConfiguerTrait: Send + Sync {
|
||||
) -> Result<(), Error> {
|
||||
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(
|
||||
&self,
|
||||
_name: &str,
|
||||
@@ -39,6 +49,16 @@ pub trait IfConfiguerTrait: Send + Sync {
|
||||
) -> Result<(), Error> {
|
||||
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(
|
||||
&self,
|
||||
_name: &str,
|
||||
|
||||
@@ -376,6 +376,64 @@ impl NetlinkIfConfiger {
|
||||
pub(crate) fn list_ipv6_route_messages() -> Result<Vec<RouteMessage>, Error> {
|
||||
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]
|
||||
@@ -387,29 +445,25 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
cidr_prefix: u8,
|
||||
cost: Option<i32>,
|
||||
) -> Result<(), Error> {
|
||||
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;
|
||||
// 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)));
|
||||
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 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)
|
||||
}
|
||||
|
||||
@@ -424,10 +478,41 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
|
||||
for msg in routes {
|
||||
let other_route: Route = msg.clone().into();
|
||||
if other_route.destination == std::net::IpAddr::V4(address)
|
||||
&& other_route.prefix == cidr_prefix
|
||||
&& other_route.ifindex == Some(ifidx)
|
||||
{
|
||||
if NetlinkIfConfiger::ipv4_route_target_matches(
|
||||
&other_route,
|
||||
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)?;
|
||||
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 {}
|
||||
impl PrepareEnv {
|
||||
fn new() -> Self {
|
||||
|
||||
@@ -193,6 +193,8 @@ impl IpProxy {
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
type NicCtx = super::virtual_nic::NicCtx;
|
||||
#[cfg(all(feature = "tun", mobile))]
|
||||
use super::virtual_nic::MobileTunSources;
|
||||
|
||||
#[cfg(feature = "magic-dns")]
|
||||
struct MagicDnsContainer {
|
||||
@@ -910,7 +912,8 @@ impl Instance {
|
||||
close_notifier: Arc<Notify>,
|
||||
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
) -> 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(
|
||||
global_ctx,
|
||||
peer_manager,
|
||||
@@ -1730,6 +1733,7 @@ impl Instance {
|
||||
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
fd: i32,
|
||||
sources: MobileTunSources,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("setup_nic_ctx_for_mobile, fd: {}", fd);
|
||||
Self::clear_nic_ctx(nic_ctx.clone(), peer_packet_receiver.clone()).await;
|
||||
@@ -1747,7 +1751,7 @@ impl Instance {
|
||||
.await
|
||||
.with_context(|| "create nic ctx failed")?;
|
||||
new_nic_ctx
|
||||
.run_for_mobile(fd)
|
||||
.run_for_mobile(fd, sources)
|
||||
.await
|
||||
.with_context(|| "add ip failed")?;
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ pub struct SharedIfConfigDelta {
|
||||
pub ipv4_addresses: OwnedItemDelta<Ipv4Inet>,
|
||||
pub ipv6_addresses: OwnedItemDelta<Ipv6Inet>,
|
||||
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 mtu: Option<SharedMtuChange>,
|
||||
}
|
||||
@@ -131,6 +134,12 @@ impl SharedIfConfig {
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
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(
|
||||
&mut self.ipv4_address_owners,
|
||||
@@ -159,11 +168,22 @@ impl SharedIfConfig {
|
||||
|
||||
update_member_mtu(&mut self.member_mtu, member_id, claims.mtu);
|
||||
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 {
|
||||
ipv4_addresses,
|
||||
ipv6_addresses,
|
||||
ipv4_routes,
|
||||
ipv4_route_removed_old_source_hints,
|
||||
ipv4_route_source_changed,
|
||||
ipv4_route_source_changed_old_hints,
|
||||
ipv6_routes,
|
||||
mtu: mtu_delta(old_mtu, self.effective_mtu()),
|
||||
}
|
||||
@@ -173,8 +193,10 @@ impl SharedIfConfig {
|
||||
&mut self,
|
||||
member_id: SharedVirtualNicMemberId,
|
||||
) -> 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_ipv4_route_sources = self.ipv4_route_sources(&old_claims.ipv4_routes);
|
||||
self.member_claims.remove(&member_id);
|
||||
|
||||
let ipv4_addresses = remove_owned_items(
|
||||
&mut self.ipv4_address_owners,
|
||||
@@ -198,11 +220,22 @@ impl SharedIfConfig {
|
||||
);
|
||||
|
||||
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 {
|
||||
ipv4_addresses,
|
||||
ipv6_addresses,
|
||||
ipv4_routes,
|
||||
ipv4_route_removed_old_source_hints,
|
||||
ipv4_route_source_changed,
|
||||
ipv4_route_source_changed_old_hints,
|
||||
ipv6_routes,
|
||||
mtu: mtu_delta(old_mtu, self.effective_mtu()),
|
||||
})
|
||||
@@ -250,6 +283,57 @@ impl SharedIfConfig {
|
||||
.cloned()
|
||||
.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 {
|
||||
@@ -509,7 +593,24 @@ impl SharedVirtualNic {
|
||||
let nic = self.nic.lock().await;
|
||||
|
||||
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 {
|
||||
ignore_removed_ifcfg_not_found(
|
||||
@@ -531,8 +632,10 @@ impl SharedVirtualNic {
|
||||
.await?;
|
||||
}
|
||||
for route in &delta.ipv4_routes.added {
|
||||
nic.add_route_with_cost(route.address, route.prefix, route.cost)
|
||||
.await?;
|
||||
add_shared_ipv4_route(&nic, route, _next_ifcfg).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 {
|
||||
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() {
|
||||
for route in _next_ifcfg.ipv4_route_owners.keys() {
|
||||
ignore_added_ifcfg_already_exists(
|
||||
nic.add_route_with_cost(route.address, route.prefix, route.cost)
|
||||
.await,
|
||||
add_shared_ipv4_route(&nic, route, _next_ifcfg).await,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -631,9 +733,7 @@ impl SharedVirtualNic {
|
||||
dispatcher: &SharedVirtualNicDispatcher,
|
||||
) -> Result<(), Error> {
|
||||
for (member_id, claims) in &self.ifcfg.member_claims {
|
||||
dispatcher
|
||||
.update_sources(*member_id, &claims.ipv4_addresses, &claims.ipv6_addresses)
|
||||
.await?;
|
||||
dispatcher.update_sources(*member_id, claims).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -644,17 +744,9 @@ impl SharedVirtualNic {
|
||||
old_claims: &SharedIfConfigClaims,
|
||||
next_claims: &SharedIfConfigClaims,
|
||||
) -> Result<(), Error> {
|
||||
let mut active_ipv4_addresses = old_claims.ipv4_addresses.clone();
|
||||
active_ipv4_addresses.extend(next_claims.ipv4_addresses.iter().copied());
|
||||
let mut active_ipv6_addresses = old_claims.ipv6_addresses.clone();
|
||||
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
|
||||
let active_claims = dispatcher_claims_for_ifcfg_transition(old_claims, next_claims);
|
||||
self.sync_dispatcher_sources_for_claims(member_id, &active_claims)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sync_dispatcher_sources_for_member(
|
||||
@@ -662,24 +754,17 @@ impl SharedVirtualNic {
|
||||
member_id: SharedVirtualNicMemberId,
|
||||
claims: &SharedIfConfigClaims,
|
||||
) -> Result<(), Error> {
|
||||
self.sync_dispatcher_sources_for_addresses(
|
||||
member_id,
|
||||
&claims.ipv4_addresses,
|
||||
&claims.ipv6_addresses,
|
||||
)
|
||||
.await
|
||||
self.sync_dispatcher_sources_for_claims(member_id, claims)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn sync_dispatcher_sources_for_addresses(
|
||||
async fn sync_dispatcher_sources_for_claims(
|
||||
&self,
|
||||
member_id: SharedVirtualNicMemberId,
|
||||
ipv4_addresses: &BTreeSet<Ipv4Inet>,
|
||||
ipv6_addresses: &BTreeSet<Ipv6Inet>,
|
||||
claims: &SharedIfConfigClaims,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(dispatcher) = &self.dispatcher {
|
||||
dispatcher
|
||||
.update_sources(member_id, ipv4_addresses, ipv6_addresses)
|
||||
.await?;
|
||||
dispatcher.update_sources(member_id, claims).await?;
|
||||
}
|
||||
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> {
|
||||
match result {
|
||||
Err(Error::NotFound) => Ok(()),
|
||||
@@ -897,8 +1051,7 @@ impl SharedVirtualNicMember {
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub async fn add_mobile_source_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> {
|
||||
let ip = ipv4_inet(ip, cidr)?;
|
||||
pub async fn add_mobile_source_ip(&self, ip: Ipv4Inet) -> Result<(), Error> {
|
||||
self.update_claims_for_mobile(|claims| {
|
||||
claims.ipv4_addresses.insert(ip);
|
||||
})
|
||||
@@ -906,14 +1059,29 @@ impl SharedVirtualNicMember {
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Addr, cidr: i32) -> Result<(), Error> {
|
||||
let ip = ipv6_inet(ip, cidr)?;
|
||||
pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Inet) -> Result<(), Error> {
|
||||
self.update_claims_for_mobile(|claims| {
|
||||
claims.ipv6_addresses.insert(ip);
|
||||
})
|
||||
.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> {
|
||||
self.update_claims(|claims| match 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 {
|
||||
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]
|
||||
fn shared_virtual_nic_wraps_virtual_nic_and_tracks_ifcfg() {
|
||||
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)]
|
||||
use super::shared_virtual_nic::SharedVirtualNic;
|
||||
#[cfg(mobile)]
|
||||
use super::shared_virtual_nic::{SharedIpv4Route, SharedIpv6Route};
|
||||
use super::shared_virtual_nic::{
|
||||
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! {
|
||||
pub struct TunStream {
|
||||
#[pin]
|
||||
@@ -765,10 +837,21 @@ impl VirtualNic {
|
||||
address: Ipv4Addr,
|
||||
cidr: u8,
|
||||
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> {
|
||||
let _g = self.config.net_ns.guard();
|
||||
self.ifcfg
|
||||
.add_ipv4_route(self.ifname(), address, cidr, cost)
|
||||
.add_ipv4_route_with_source_hint(self.ifname(), address, cidr, cost, source_hint)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -781,6 +864,26 @@ impl VirtualNic {
|
||||
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> {
|
||||
self.add_ipv6_route_with_cost(address, cidr, None).await
|
||||
}
|
||||
@@ -1031,18 +1134,34 @@ impl NicBackend {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
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)]
|
||||
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 {
|
||||
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)]
|
||||
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 {
|
||||
Ok(ret) => {
|
||||
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
|
||||
.add_mobile_source_ip(ipv4_addr.address(), ipv4_addr.network_length() as i32)
|
||||
.add_mobile_source_ipv4_route(ipv4_route)
|
||||
.await?;
|
||||
}
|
||||
if let Some(ipv6_addr) = self.global_ctx.get_ipv6() {
|
||||
for ipv6_route in sources.ipv6_routes {
|
||||
self.backend
|
||||
.add_mobile_source_ipv6(ipv6_addr.address(), ipv6_addr.network_length() as i32)
|
||||
.add_mobile_source_ipv6_route(ipv6_route)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,28 @@ impl NetworkInstanceManager {
|
||||
.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> {
|
||||
let sender = self
|
||||
.instance_map
|
||||
|
||||
@@ -6,6 +6,8 @@ use crate::common::config::{
|
||||
use crate::gateway::socks5::Socks5Server;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
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::rpc_types::controller::BaseController;
|
||||
use crate::rpc_service::InstanceRpcService;
|
||||
@@ -36,6 +38,16 @@ use tokio::{
|
||||
pub type MyNodeInfo = crate::proto::api::manage::MyNodeInfo;
|
||||
|
||||
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>;
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
@@ -135,11 +147,12 @@ impl EasyTierLauncher {
|
||||
peer_mgr.clone(),
|
||||
peer_packet_receiver.clone(),
|
||||
shared_virtual_nic_registry.clone(),
|
||||
tun_fd,
|
||||
tun_fd.fd,
|
||||
tun_fd.sources,
|
||||
)
|
||||
.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.ParcelFileDescriptor
|
||||
import android.os.Bundle
|
||||
import android.system.OsConstants.AF_INET6
|
||||
import java.net.InetAddress
|
||||
import java.util.Arrays
|
||||
|
||||
@@ -115,7 +116,7 @@ class TauriVpnService : VpnService() {
|
||||
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
||||
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
||||
}
|
||||
builder.addAddress("fd00::1", 128)
|
||||
builder.allowFamily(AF_INET6)
|
||||
|
||||
builder.setMtu(mtu)
|
||||
dns?.let { builder.addDnsServer(it) }
|
||||
|
||||
Reference in New Issue
Block a user