mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-02 17:15:43 +00:00
fix: support android shared tun fd groups
Android previously treated setTunFd as a single-instance update, and the VpnService plugin could only expose one IPv4 address. That made shared TUN members disable each other or leave only one address configured. Group enabled Android TUN instances by shared dev_name, send the fd to every compatible member, and only disable incompatible TUN users. Build the Android VPN request from the whole running shared group and pass every IPv4 address to VpnService. The shared mobile dispatcher now owns current fd device state on a process-level runtime. New setTunFd calls replace that state even when the raw fd number is reused, and mobile TUN read/write/create failures rebuild with backoff while preserving member registrations. Protect shared member cleanup with per-registration ownership tokens, so old async cleanup cannot unregister a recreated member or remove its source claims. Mobile source addresses are registered in the dispatcher without applying OS ifcfg changes, so Android-originated packets return through the owning instance. When one shared member stops while another remains, notify the frontend to recalculate the VpnService config instead of leaving stale addresses and routes. Serialize Android VpnService config recalculation so stale async events cannot overwrite newer shared-group state. If one shared member is not ready, rebuild from the healthy members and retry the missing member later. If no healthy member remains, stop the Android VPN service instead of keeping stale routes active.
This commit is contained in:
@@ -160,14 +160,31 @@ async fn set_tun_fd(fd: i32) -> 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());
|
||||||
};
|
};
|
||||||
if let Some(uuid) = get_client_manager!()?
|
|
||||||
.get_enabled_instances_with_tun_ids()
|
let mut success_count = 0;
|
||||||
.next()
|
let mut errors = Vec::new();
|
||||||
{
|
for uuid in get_client_manager!()?.get_enabled_instances_for_tun_fd() {
|
||||||
instance_manager
|
match instance_manager.set_tun_fd(&uuid, fd) {
|
||||||
.set_tun_fd(&uuid, fd)
|
Ok(()) => {
|
||||||
.map_err(|e| e.to_string())?;
|
success_count += 1;
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
errors.push(format!("{}: {}", uuid, err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if success_count == 0 && !errors.is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"failed to set tun fd for all instances: {}",
|
||||||
|
errors.join("; ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for err in errors {
|
||||||
|
eprintln!("set_tun_fd skipped instance: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,32 +935,98 @@ mod manager {
|
|||||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_enabled_instances_for_tun_fd(&self) -> Vec<uuid::Uuid> {
|
||||||
|
let Some(first) = self
|
||||||
|
.storage
|
||||||
|
.network_configs
|
||||||
|
.iter()
|
||||||
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||||
|
.filter(|v| !v.config.no_tun())
|
||||||
|
.find_map(|c| {
|
||||||
|
c.config
|
||||||
|
.instance_id()
|
||||||
|
.parse::<uuid::Uuid>()
|
||||||
|
.ok()
|
||||||
|
.map(|id| (id, Self::shared_tun_dev_name(&c.config).map(str::to_owned)))
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let (first_id, Some(shared_dev_name)) = first else {
|
||||||
|
return vec![first.0];
|
||||||
|
};
|
||||||
|
|
||||||
|
let ids: Vec<uuid::Uuid> = self
|
||||||
|
.storage
|
||||||
|
.network_configs
|
||||||
|
.iter()
|
||||||
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||||
|
.filter(|v| Self::shared_tun_dev_name(&v.config) == Some(shared_dev_name.as_str()))
|
||||||
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||||
|
.collect();
|
||||||
|
if ids.is_empty() { vec![first_id] } else { ids }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shared_tun_dev_name(config: &NetworkConfig) -> Option<&str> {
|
||||||
|
if config.no_tun() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
config
|
||||||
|
.dev_name
|
||||||
|
.as_deref()
|
||||||
|
.filter(|dev_name| !dev_name.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub fn get_enabled_instances_with_web_like_tun_ids(
|
fn runtime_shared_tun_dev_name(
|
||||||
|
cfg: &easytier::common::config::TomlConfigLoader,
|
||||||
|
) -> Option<String> {
|
||||||
|
let flags = cfg.get_flags();
|
||||||
|
if flags.no_tun || flags.dev_name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(flags.dev_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
fn is_compatible_android_tun(
|
||||||
|
config: &NetworkConfig,
|
||||||
|
shared_dev_name: Option<&str>,
|
||||||
|
) -> bool {
|
||||||
|
matches!(
|
||||||
|
(Self::shared_tun_dev_name(config), shared_dev_name),
|
||||||
|
(Some(existing), Some(next)) if existing == next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
fn enabled_incompatible_tun_ids(
|
||||||
&self,
|
&self,
|
||||||
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
web_only: bool,
|
||||||
|
shared_dev_name: Option<&str>,
|
||||||
|
) -> Vec<uuid::Uuid> {
|
||||||
self.storage
|
self.storage
|
||||||
.network_configs
|
.network_configs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||||
.filter(|v| !v.config.no_tun())
|
.filter(|v| !v.config.no_tun())
|
||||||
.filter(|v| v.source.is_web_like())
|
.filter(|v| !web_only || v.source.is_web_like())
|
||||||
|
.filter(|v| !Self::is_compatible_android_tun(&v.config, shared_dev_name))
|
||||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub(super) async fn disable_instances_with_tun(
|
pub(super) async fn disable_incompatible_instances_with_tun(
|
||||||
&self,
|
&self,
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
web_only: bool,
|
web_only: bool,
|
||||||
|
shared_dev_name: Option<&str>,
|
||||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||||
{
|
{
|
||||||
let inst_ids: Vec<uuid::Uuid> = if web_only {
|
for inst_id in self.enabled_incompatible_tun_ids(web_only, shared_dev_name) {
|
||||||
self.get_enabled_instances_with_web_like_tun_ids().collect()
|
|
||||||
} else {
|
|
||||||
self.get_enabled_instances_with_tun_ids().collect()
|
|
||||||
};
|
|
||||||
for inst_id in inst_ids {
|
|
||||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -951,11 +1034,20 @@ mod manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn notify_vpn_stop_if_no_tun(&self, app: &AppHandle) -> Result<(), String> {
|
pub(super) fn notify_vpn_stop_if_no_tun(&self, app: &AppHandle) -> Result<(), String> {
|
||||||
let has_tun = self.get_enabled_instances_with_tun_ids().any(|_| true);
|
#[cfg(target_os = "android")]
|
||||||
if !has_tun {
|
if let Some(instance_id) = self.get_enabled_instances_with_tun_ids().next() {
|
||||||
|
app.emit("vpn_service_config_changed", instance_id.to_string())
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
app.emit("vpn_service_stop", "")
|
app.emit("vpn_service_stop", "")
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,19 +1063,31 @@ mod manager {
|
|||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
if !cfg.get_flags().no_tun {
|
if !cfg.get_flags().no_tun {
|
||||||
|
let shared_dev_name = Self::runtime_shared_tun_dev_name(cfg);
|
||||||
match source {
|
match source {
|
||||||
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
||||||
self.disable_instances_with_tun(app, false)
|
self.disable_incompatible_instances_with_tun(
|
||||||
|
app,
|
||||||
|
false,
|
||||||
|
shared_dev_name.as_deref(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
PersistedConfigSource::Web => {
|
PersistedConfigSource::Web => {
|
||||||
self.disable_instances_with_tun(app, true)
|
self.disable_incompatible_instances_with_tun(
|
||||||
|
app,
|
||||||
|
true,
|
||||||
|
shared_dev_name.as_deref(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
if !self
|
||||||
|
.enabled_incompatible_tun_ids(false, shared_dev_name.as_deref())
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
return Err(
|
return Err(
|
||||||
"Android only supports one active TUN network; user-managed VPN remains active"
|
"Android only supports one active TUN device; user-managed VPN remains active with an incompatible dev_name"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
@@ -52,6 +52,7 @@ declare global {
|
|||||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const normalizeConfigSource: typeof import('./composables/config_source')['normalizeConfigSource']
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
@@ -177,6 +178,7 @@ declare module 'vue' {
|
|||||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
|
readonly normalizeConfigSource: UnwrapRef<typeof import('./composables/config_source')['normalizeConfigSource']>
|
||||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const EVENTS = Object.freeze({
|
|||||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||||
POST_RUN_NETWORK_INSTANCE: 'post_run_network_instance',
|
POST_RUN_NETWORK_INSTANCE: 'post_run_network_instance',
|
||||||
VPN_SERVICE_STOP: 'vpn_service_stop',
|
VPN_SERVICE_STOP: 'vpn_service_stop',
|
||||||
|
VPN_SERVICE_CONFIG_CHANGED: 'vpn_service_config_changed',
|
||||||
DHCP_IP_CHANGED: 'dhcp_ip_changed',
|
DHCP_IP_CHANGED: 'dhcp_ip_changed',
|
||||||
PROXY_CIDRS_UPDATED: 'proxy_cidrs_updated',
|
PROXY_CIDRS_UPDATED: 'proxy_cidrs_updated',
|
||||||
EVENT_LAGGED: 'event_lagged',
|
EVENT_LAGGED: 'event_lagged',
|
||||||
@@ -76,6 +77,14 @@ async function onVpnServiceStop(event: Event<unknown>) {
|
|||||||
await syncMobileVpnService();
|
await syncMobileVpnService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onVpnServiceConfigChanged(event: Event<unknown>) {
|
||||||
|
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||||
|
console.log(`Received event '${EVENTS.VPN_SERVICE_CONFIG_CHANGED}' for instance: ${instanceId}`)
|
||||||
|
if (type() === 'android') {
|
||||||
|
await onNetworkInstanceChange(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onDhcpIpChanged(event: Event<unknown>) {
|
async function onDhcpIpChanged(event: Event<unknown>) {
|
||||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||||
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${instanceId}`);
|
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${instanceId}`);
|
||||||
@@ -104,6 +113,7 @@ export async function listenGlobalEvents() {
|
|||||||
await listen(EVENTS.PRE_RUN_NETWORK_INSTANCE, onPreRunNetworkInstance),
|
await listen(EVENTS.PRE_RUN_NETWORK_INSTANCE, onPreRunNetworkInstance),
|
||||||
await listen(EVENTS.POST_RUN_NETWORK_INSTANCE, onPostRunNetworkInstance),
|
await listen(EVENTS.POST_RUN_NETWORK_INSTANCE, onPostRunNetworkInstance),
|
||||||
await listen(EVENTS.VPN_SERVICE_STOP, onVpnServiceStop),
|
await listen(EVENTS.VPN_SERVICE_STOP, onVpnServiceStop),
|
||||||
|
await listen(EVENTS.VPN_SERVICE_CONFIG_CHANGED, onVpnServiceConfigChanged),
|
||||||
await listen(EVENTS.DHCP_IP_CHANGED, onDhcpIpChanged),
|
await listen(EVENTS.DHCP_IP_CHANGED, onDhcpIpChanged),
|
||||||
await listen(EVENTS.PROXY_CIDRS_UPDATED, onProxyCidrsUpdated),
|
await listen(EVENTS.PROXY_CIDRS_UPDATED, onProxyCidrsUpdated),
|
||||||
await listen(EVENTS.EVENT_LAGGED, onEventLagged),
|
await listen(EVENTS.EVENT_LAGGED, onEventLagged),
|
||||||
|
|||||||
@@ -8,17 +8,21 @@ type Route = NetworkTypes.Route
|
|||||||
interface vpnStatus {
|
interface vpnStatus {
|
||||||
running: boolean
|
running: boolean
|
||||||
ipv4Addr: string | null | undefined
|
ipv4Addr: string | null | undefined
|
||||||
|
ipv4Addrs: string[]
|
||||||
ipv4Cidr: number | null | undefined
|
ipv4Cidr: number | null | undefined
|
||||||
routes: string[]
|
routes: string[]
|
||||||
dns: string | null | undefined
|
dns: string | null | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
let dhcpPollingTimer: NodeJS.Timeout | null = null
|
let dhcpPollingTimer: NodeJS.Timeout | null = null
|
||||||
|
let vpnConfigSyncTask: Promise<void> | null = null
|
||||||
|
let pendingVpnConfigInstanceId: string | null = null
|
||||||
const DHCP_POLLING_INTERVAL = 2000 // 2秒后重试
|
const DHCP_POLLING_INTERVAL = 2000 // 2秒后重试
|
||||||
|
|
||||||
const curVpnStatus: vpnStatus = {
|
const curVpnStatus: vpnStatus = {
|
||||||
running: false,
|
running: false,
|
||||||
ipv4Addr: undefined,
|
ipv4Addr: undefined,
|
||||||
|
ipv4Addrs: [],
|
||||||
ipv4Cidr: undefined,
|
ipv4Cidr: undefined,
|
||||||
routes: [],
|
routes: [],
|
||||||
dns: undefined,
|
dns: undefined,
|
||||||
@@ -42,6 +46,7 @@ async function requestVpnPermission() {
|
|||||||
|
|
||||||
function resetVpnConfigStatus() {
|
function resetVpnConfigStatus() {
|
||||||
curVpnStatus.ipv4Addr = undefined
|
curVpnStatus.ipv4Addr = undefined
|
||||||
|
curVpnStatus.ipv4Addrs = []
|
||||||
curVpnStatus.ipv4Cidr = undefined
|
curVpnStatus.ipv4Cidr = undefined
|
||||||
curVpnStatus.routes = []
|
curVpnStatus.routes = []
|
||||||
curVpnStatus.dns = undefined
|
curVpnStatus.dns = undefined
|
||||||
@@ -54,6 +59,12 @@ function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_statu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
curVpnStatus.ipv4Addrs = status?.ipv4Addrs?.length
|
||||||
|
? [...status.ipv4Addrs]
|
||||||
|
: status?.ipv4Addr
|
||||||
|
? [status.ipv4Addr]
|
||||||
|
: []
|
||||||
|
|
||||||
const ipv4WithCidr = status?.ipv4Addr
|
const ipv4WithCidr = status?.ipv4Addr
|
||||||
if (ipv4WithCidr?.length) {
|
if (ipv4WithCidr?.length) {
|
||||||
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
||||||
@@ -96,14 +107,17 @@ async function doStopVpn(force = false) {
|
|||||||
resetVpnConfigStatus()
|
resetVpnConfigStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?: string) {
|
async function doStartVpn(ipv4Addrs: string[], routes: string[], dns?: string) {
|
||||||
if (curVpnStatus.running) {
|
if (curVpnStatus.running) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('start vpn service', ipv4Addr, cidr, routes, dns)
|
const primaryIpv4 = ipv4Addrs[0]
|
||||||
|
const [ipv4Addr, cidr] = primaryIpv4.split('/')
|
||||||
|
console.log('start vpn service', ipv4Addrs, routes, dns)
|
||||||
const request = {
|
const request = {
|
||||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
ipv4Addr: primaryIpv4,
|
||||||
|
ipv4Addrs,
|
||||||
routes,
|
routes,
|
||||||
dns,
|
dns,
|
||||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||||
@@ -127,11 +141,25 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?
|
|||||||
await waitVpnStatus(true, 3)
|
await waitVpnStatus(true, 3)
|
||||||
|
|
||||||
curVpnStatus.ipv4Addr = ipv4Addr
|
curVpnStatus.ipv4Addr = ipv4Addr
|
||||||
curVpnStatus.ipv4Cidr = cidr
|
curVpnStatus.ipv4Addrs = [...ipv4Addrs]
|
||||||
|
curVpnStatus.ipv4Cidr = Number(cidr)
|
||||||
curVpnStatus.routes = routes
|
curVpnStatus.routes = routes
|
||||||
curVpnStatus.dns = dns
|
curVpnStatus.dns = dns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleVpnConfigRetry(instanceId: string) {
|
||||||
|
if (dhcpPollingTimer) {
|
||||||
|
clearTimeout(dhcpPollingTimer)
|
||||||
|
}
|
||||||
|
dhcpPollingTimer = setTimeout(() => {
|
||||||
|
onNetworkInstanceChange(instanceId)
|
||||||
|
}, DHCP_POLLING_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasQueuedVpnConfigChange() {
|
||||||
|
return pendingVpnConfigInstanceId !== null
|
||||||
|
}
|
||||||
|
|
||||||
async function onVpnServiceStart(payload: any) {
|
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
|
||||||
@@ -170,7 +198,7 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
|
|||||||
|
|
||||||
const ret = []
|
const ret = []
|
||||||
for (const r of routes) {
|
for (const r of routes) {
|
||||||
for (let cidr of r.proxy_cidrs) {
|
for (let cidr of r.proxy_cidrs ?? []) {
|
||||||
if (!cidr.includes('/')) {
|
if (!cidr.includes('/')) {
|
||||||
cidr += '/32'
|
cidr += '/32'
|
||||||
}
|
}
|
||||||
@@ -178,7 +206,7 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node_config.routes.forEach(r => {
|
node_config.routes?.forEach(r => {
|
||||||
ret.push(r)
|
ret.push(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,7 +218,32 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
|
|||||||
return Array.from(new Set(ret)).sort()
|
return Array.from(new Set(ret)).sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCollectedNetworkInfo(response: Awaited<ReturnType<typeof collectNetworkInfo>>, instanceId: string) {
|
||||||
|
const info = response.info as any
|
||||||
|
const map = info?.map ?? info
|
||||||
|
return map?.[instanceId]
|
||||||
|
}
|
||||||
|
|
||||||
export async function onNetworkInstanceChange(instanceId: string) {
|
export async function onNetworkInstanceChange(instanceId: string) {
|
||||||
|
pendingVpnConfigInstanceId = instanceId
|
||||||
|
if (!vpnConfigSyncTask) {
|
||||||
|
vpnConfigSyncTask = drainVpnConfigChanges().finally(() => {
|
||||||
|
vpnConfigSyncTask = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await vpnConfigSyncTask
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drainVpnConfigChanges() {
|
||||||
|
while (pendingVpnConfigInstanceId !== null) {
|
||||||
|
const instanceId = pendingVpnConfigInstanceId
|
||||||
|
pendingVpnConfigInstanceId = null
|
||||||
|
await applyNetworkInstanceChange(instanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyNetworkInstanceChange(instanceId: string) {
|
||||||
console.error('vpn service network instance change id', instanceId)
|
console.error('vpn service network instance change id', instanceId)
|
||||||
|
|
||||||
if (dhcpPollingTimer) {
|
if (dhcpPollingTimer) {
|
||||||
@@ -201,40 +254,53 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
|||||||
if (!instanceId) {
|
if (!instanceId) {
|
||||||
console.warn('vpn service skipped because instance id is empty')
|
console.warn('vpn service skipped because instance id is empty')
|
||||||
if (curVpnStatus.running) {
|
if (curVpnStatus.running) {
|
||||||
|
if (hasQueuedVpnConfigChange()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
await doStopVpn()
|
await doStopVpn()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = await getConfig(instanceId)
|
|
||||||
|
const group = await findRunningTunInstanceGroup(instanceId)
|
||||||
|
if (!group.length) {
|
||||||
|
console.warn('vpn service skipped because no running tun instance is available', instanceId)
|
||||||
|
if (hasQueuedVpnConfigChange()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await doStopVpn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipv4Addrs: string[] = []
|
||||||
|
const routes = new Set<string>()
|
||||||
|
let dns: string | undefined
|
||||||
|
const retryInstanceIds: string[] = []
|
||||||
|
for (const { instanceId, config } of group) {
|
||||||
console.log('vpn service loaded config', instanceId, JSON.stringify({
|
console.log('vpn service loaded config', instanceId, JSON.stringify({
|
||||||
no_tun: config.no_tun,
|
no_tun: config.no_tun,
|
||||||
dhcp: config.dhcp,
|
dhcp: config.dhcp,
|
||||||
enable_magic_dns: config.enable_magic_dns,
|
enable_magic_dns: config.enable_magic_dns,
|
||||||
|
dev_name: config.dev_name,
|
||||||
}))
|
}))
|
||||||
if (config.no_tun) {
|
|
||||||
console.log('vpn service skipped because no_tun is enabled', instanceId)
|
const curNetworkInfo = getCollectedNetworkInfo(await collectNetworkInfo(instanceId), instanceId)
|
||||||
return
|
|
||||||
}
|
|
||||||
const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId]
|
|
||||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||||
console.warn('vpn service skipped because network info is unavailable', instanceId, curNetworkInfo?.error_msg)
|
console.warn('vpn service skipped because network info is unavailable, will retry', instanceId, curNetworkInfo?.error_msg)
|
||||||
await doStopVpn()
|
retryInstanceIds.push(instanceId)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
||||||
|
|
||||||
if (config.dhcp && (!virtual_ip || !virtual_ip.length)) {
|
if (config.dhcp && (!virtual_ip || !virtual_ip.length)) {
|
||||||
console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms')
|
console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms')
|
||||||
dhcpPollingTimer = setTimeout(() => {
|
retryInstanceIds.push(instanceId)
|
||||||
onNetworkInstanceChange(instanceId)
|
continue
|
||||||
}, DHCP_POLLING_INTERVAL)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!virtual_ip || !virtual_ip.length) {
|
if (!virtual_ip || !virtual_ip.length) {
|
||||||
await doStopVpn()
|
retryInstanceIds.push(instanceId)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
||||||
@@ -242,19 +308,41 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
|||||||
network_length = 24
|
network_length = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = getRoutesForVpn(curNetworkInfo?.routes, config)
|
ipv4Addrs.push(`${virtual_ip}/${network_length}`)
|
||||||
|
getRoutesForVpn(curNetworkInfo?.routes, config).forEach(route => routes.add(route))
|
||||||
|
if (config.enable_magic_dns) {
|
||||||
|
dns = '100.100.100.101'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined
|
if (retryInstanceIds.length) {
|
||||||
|
scheduleVpnConfigRetry(retryInstanceIds[0])
|
||||||
|
}
|
||||||
|
|
||||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
if (!ipv4Addrs.length) {
|
||||||
const cidrChanged = network_length !== curVpnStatus.ipv4Cidr
|
console.warn('vpn service skipped because no healthy tun instance info is available', instanceId)
|
||||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
if (hasQueuedVpnConfigChange()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await doStopVpn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedIpv4Addrs = [...ipv4Addrs].sort()
|
||||||
|
const sortedRoutes = Array.from(routes).sort()
|
||||||
|
const ipChanged = JSON.stringify(sortedIpv4Addrs) !== JSON.stringify(curVpnStatus.ipv4Addrs)
|
||||||
|
const routesChanged = JSON.stringify(sortedRoutes) !== JSON.stringify(curVpnStatus.routes)
|
||||||
const dnsChanged = dns != curVpnStatus.dns
|
const dnsChanged = dns != curVpnStatus.dns
|
||||||
const configChanged = ipChanged || cidrChanged || routesChanged || dnsChanged
|
const configChanged = ipChanged || routesChanged || dnsChanged
|
||||||
const shouldStartVpn = !curVpnStatus.running
|
const shouldStartVpn = !curVpnStatus.running
|
||||||
|
|
||||||
if (shouldStartVpn || configChanged) {
|
if (shouldStartVpn || configChanged) {
|
||||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
if (hasQueuedVpnConfigChange()) {
|
||||||
|
console.info('vpn service skipped stale config apply because a newer change is queued')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), sortedIpv4Addrs)
|
||||||
if (curVpnStatus.running) {
|
if (curVpnStatus.running) {
|
||||||
try {
|
try {
|
||||||
await doStopVpn()
|
await doStopVpn()
|
||||||
@@ -262,10 +350,14 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
|||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
if (hasQueuedVpnConfigChange()) {
|
||||||
|
console.info('vpn service skipped stale config start because a newer change is queued')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await doStartVpn(virtual_ip, network_length, routes, dns)
|
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (e instanceof Error && e.message === 'need_prepare') {
|
if (e instanceof Error && e.message === 'need_prepare') {
|
||||||
@@ -304,6 +396,33 @@ async function findRunningTunInstanceId() {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findRunningTunInstanceGroup(preferredInstanceId?: string) {
|
||||||
|
const instanceIds = await listNetworkInstanceIds()
|
||||||
|
const runningIds = instanceIds.running_inst_ids.map(Utils.UuidToStr)
|
||||||
|
const runningTunInstances = []
|
||||||
|
|
||||||
|
for (const instanceId of runningIds) {
|
||||||
|
const config = await getConfig(instanceId)
|
||||||
|
if (config.no_tun) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
runningTunInstances.push({ instanceId, config })
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = runningTunInstances.find(inst => inst.instanceId === preferredInstanceId)
|
||||||
|
?? runningTunInstances[0]
|
||||||
|
if (!selected) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const devName = selected.config.dev_name
|
||||||
|
if (!devName?.length) {
|
||||||
|
return [selected]
|
||||||
|
}
|
||||||
|
|
||||||
|
return runningTunInstances.filter(inst => inst.config.dev_name === devName)
|
||||||
|
}
|
||||||
|
|
||||||
export async function initMobileVpnService() {
|
export async function initMobileVpnService() {
|
||||||
await registerVpnServiceListener()
|
await registerVpnServiceListener()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ mod dispatcher;
|
|||||||
use dispatcher::{SharedVirtualNicDispatcher, SharedVirtualNicMemberTunnelTable};
|
use dispatcher::{SharedVirtualNicDispatcher, SharedVirtualNicMemberTunnelTable};
|
||||||
|
|
||||||
pub type SharedVirtualNicMemberId = uuid::Uuid;
|
pub type SharedVirtualNicMemberId = uuid::Uuid;
|
||||||
|
pub(super) type SharedVirtualNicMemberRegistrationId = uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct SharedIpv4Route {
|
pub struct SharedIpv4Route {
|
||||||
@@ -256,6 +257,7 @@ pub struct SharedVirtualNic {
|
|||||||
ifcfg: SharedIfConfig,
|
ifcfg: SharedIfConfig,
|
||||||
valid: Arc<AtomicBool>,
|
valid: Arc<AtomicBool>,
|
||||||
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
||||||
|
member_registrations: BTreeMap<SharedVirtualNicMemberId, SharedVirtualNicMemberRegistrationId>,
|
||||||
dispatcher: Option<SharedVirtualNicDispatcher>,
|
dispatcher: Option<SharedVirtualNicDispatcher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +268,7 @@ impl SharedVirtualNic {
|
|||||||
ifcfg: SharedIfConfig::default(),
|
ifcfg: SharedIfConfig::default(),
|
||||||
valid: Arc::new(AtomicBool::new(true)),
|
valid: Arc::new(AtomicBool::new(true)),
|
||||||
member_tunnel_table: SharedVirtualNicMemberTunnelTable::default(),
|
member_tunnel_table: SharedVirtualNicMemberTunnelTable::default(),
|
||||||
|
member_registrations: BTreeMap::new(),
|
||||||
dispatcher: None,
|
dispatcher: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,6 +305,71 @@ impl SharedVirtualNic {
|
|||||||
self.nic.lock().await.link_up().await
|
self.nic.lock().await.link_up().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn attach_member_registration(
|
||||||
|
&mut self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.ensure_valid()?;
|
||||||
|
|
||||||
|
match self.member_registrations.insert(member_id, registration_id) {
|
||||||
|
Some(old_registration_id) if old_registration_id != registration_id => {
|
||||||
|
self.remove_member_claims(member_id).await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_current_member_registration(
|
||||||
|
&self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
) -> bool {
|
||||||
|
self.member_registrations
|
||||||
|
.get(&member_id)
|
||||||
|
.is_some_and(|current| *current == registration_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_member_claims_for_registration(
|
||||||
|
&mut self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
claims: SharedIfConfigClaims,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !self.is_current_member_registration(member_id, registration_id) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.apply_member_claims(member_id, claims).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
async fn apply_member_claims_for_mobile_registration(
|
||||||
|
&mut self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
claims: SharedIfConfigClaims,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !self.is_current_member_registration(member_id, registration_id) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.apply_member_claims_for_mobile(member_id, claims).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_member_registration_claims(
|
||||||
|
&mut self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if !self.is_current_member_registration(member_id, registration_id) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.member_registrations.remove(&member_id);
|
||||||
|
self.remove_member_claims(member_id).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn apply_member_claims(
|
async fn apply_member_claims(
|
||||||
&mut self,
|
&mut self,
|
||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
@@ -330,6 +398,25 @@ impl SharedVirtualNic {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
async fn apply_member_claims_for_mobile(
|
||||||
|
&mut self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
claims: SharedIfConfigClaims,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
self.ensure_valid()?;
|
||||||
|
|
||||||
|
let mut next_ifcfg = self.ifcfg.clone();
|
||||||
|
let next_claims = claims.clone();
|
||||||
|
next_ifcfg.apply_member_claims(member_id, claims);
|
||||||
|
|
||||||
|
self.sync_dispatcher_sources_for_member(member_id, &next_claims)
|
||||||
|
.await?;
|
||||||
|
self.ifcfg = next_ifcfg;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn remove_member_claims(
|
async fn remove_member_claims(
|
||||||
&mut self,
|
&mut self,
|
||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
@@ -340,7 +427,10 @@ impl SharedVirtualNic {
|
|||||||
let Some(delta) = next_ifcfg.remove_member(member_id) else {
|
let Some(delta) = next_ifcfg.remove_member(member_id) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
#[cfg(not(mobile))]
|
||||||
self.apply_ifcfg_delta(&delta).await?;
|
self.apply_ifcfg_delta(&delta).await?;
|
||||||
|
#[cfg(mobile)]
|
||||||
|
drop(delta);
|
||||||
self.remove_dispatcher_sources_for_member(member_id).await?;
|
self.remove_dispatcher_sources_for_member(member_id).await?;
|
||||||
self.ifcfg = next_ifcfg;
|
self.ifcfg = next_ifcfg;
|
||||||
|
|
||||||
@@ -430,13 +520,16 @@ impl SharedVirtualNic {
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.ensure_valid()?;
|
self.ensure_valid()?;
|
||||||
|
|
||||||
if self.dispatcher.is_some() {
|
if let Some(dispatcher) = &self.dispatcher {
|
||||||
|
dispatcher.update_mobile_tun_fd(tun_fd);
|
||||||
|
self.nic.lock().await.set_mobile_tun_fd_name(tun_fd);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let tunnel = self.nic.lock().await.create_dev_for_mobile(tun_fd).await?;
|
self.nic.lock().await.set_mobile_tun_fd_name(tun_fd);
|
||||||
let dispatcher = SharedVirtualNicDispatcher::start(
|
let dispatcher = SharedVirtualNicDispatcher::start_for_mobile(
|
||||||
tunnel,
|
self.nic.clone(),
|
||||||
|
tun_fd,
|
||||||
self.member_tunnel_table.clone(),
|
self.member_tunnel_table.clone(),
|
||||||
self.valid.clone(),
|
self.valid.clone(),
|
||||||
);
|
);
|
||||||
@@ -523,6 +616,7 @@ fn ignore_removed_ifcfg_not_found(result: Result<(), Error>) -> Result<(), Error
|
|||||||
|
|
||||||
struct SharedVirtualNicMemberRegistration {
|
struct SharedVirtualNicMemberRegistration {
|
||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
shared_nic: Arc<Mutex<SharedVirtualNic>>,
|
shared_nic: Arc<Mutex<SharedVirtualNic>>,
|
||||||
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
||||||
}
|
}
|
||||||
@@ -533,16 +627,22 @@ impl SharedVirtualNicMemberRegistration {
|
|||||||
tunnel: Box<dyn Tunnel>,
|
tunnel: Box<dyn Tunnel>,
|
||||||
close_notifier: Arc<Notify>,
|
close_notifier: Arc<Notify>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
self.member_tunnel_table
|
self.member_tunnel_table.register(
|
||||||
.register(self.member_id, tunnel, close_notifier)
|
self.member_id,
|
||||||
|
self.registration_id,
|
||||||
|
tunnel,
|
||||||
|
close_notifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for SharedVirtualNicMemberRegistration {
|
impl Drop for SharedVirtualNicMemberRegistration {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.member_tunnel_table.unregister(self.member_id);
|
self.member_tunnel_table
|
||||||
|
.unregister(self.member_id, self.registration_id);
|
||||||
let shared_nic = self.shared_nic.clone();
|
let shared_nic = self.shared_nic.clone();
|
||||||
let member_id = self.member_id;
|
let member_id = self.member_id;
|
||||||
|
let registration_id = self.registration_id;
|
||||||
|
|
||||||
let Ok(handle) = tokio::runtime::Handle::try_current() else {
|
let Ok(handle) = tokio::runtime::Handle::try_current() else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -554,7 +654,10 @@ impl Drop for SharedVirtualNicMemberRegistration {
|
|||||||
|
|
||||||
handle.spawn(async move {
|
handle.spawn(async move {
|
||||||
let mut shared_nic = shared_nic.lock().await;
|
let mut shared_nic = shared_nic.lock().await;
|
||||||
if let Err(err) = shared_nic.remove_member_claims(member_id).await {
|
if let Err(err) = shared_nic
|
||||||
|
.remove_member_registration_claims(member_id, registration_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
?member_id,
|
?member_id,
|
||||||
?err,
|
?err,
|
||||||
@@ -580,12 +683,14 @@ impl SharedVirtualNicMember {
|
|||||||
close_notifier: Arc<Notify>,
|
close_notifier: Arc<Notify>,
|
||||||
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let registration_id = uuid::Uuid::new_v4();
|
||||||
Self {
|
Self {
|
||||||
member_id,
|
member_id,
|
||||||
shared_nic: shared_nic.clone(),
|
shared_nic: shared_nic.clone(),
|
||||||
close_notifier,
|
close_notifier,
|
||||||
registration: Arc::new(SharedVirtualNicMemberRegistration {
|
registration: Arc::new(SharedVirtualNicMemberRegistration {
|
||||||
member_id,
|
member_id,
|
||||||
|
registration_id,
|
||||||
shared_nic: shared_nic.clone(),
|
shared_nic: shared_nic.clone(),
|
||||||
member_tunnel_table,
|
member_tunnel_table,
|
||||||
}),
|
}),
|
||||||
@@ -608,6 +713,9 @@ impl SharedVirtualNicMember {
|
|||||||
let (member_tunnel, shared_tunnel) = create_ring_tunnel_pair();
|
let (member_tunnel, shared_tunnel) = create_ring_tunnel_pair();
|
||||||
{
|
{
|
||||||
let mut shared_nic = self.shared_nic.lock().await;
|
let mut shared_nic = self.shared_nic.lock().await;
|
||||||
|
shared_nic
|
||||||
|
.attach_member_registration(self.member_id, self.registration.registration_id)
|
||||||
|
.await?;
|
||||||
shared_nic.ensure_dispatcher().await?;
|
shared_nic.ensure_dispatcher().await?;
|
||||||
}
|
}
|
||||||
self.registration
|
self.registration
|
||||||
@@ -623,6 +731,9 @@ impl SharedVirtualNicMember {
|
|||||||
let (member_tunnel, shared_tunnel) = create_ring_tunnel_pair();
|
let (member_tunnel, shared_tunnel) = create_ring_tunnel_pair();
|
||||||
{
|
{
|
||||||
let mut shared_nic = self.shared_nic.lock().await;
|
let mut shared_nic = self.shared_nic.lock().await;
|
||||||
|
shared_nic
|
||||||
|
.attach_member_registration(self.member_id, self.registration.registration_id)
|
||||||
|
.await?;
|
||||||
shared_nic.ensure_dispatcher_for_mobile(tun_fd).await?;
|
shared_nic.ensure_dispatcher_for_mobile(tun_fd).await?;
|
||||||
}
|
}
|
||||||
self.registration
|
self.registration
|
||||||
@@ -667,6 +778,24 @@ impl SharedVirtualNicMember {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
pub async fn add_mobile_source_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> {
|
||||||
|
let ip = ipv4_inet(ip, cidr)?;
|
||||||
|
self.update_claims_for_mobile(|claims| {
|
||||||
|
claims.ipv4_addresses.insert(ip);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Addr, cidr: i32) -> Result<(), Error> {
|
||||||
|
let ip = ipv6_inet(ip, cidr)?;
|
||||||
|
self.update_claims_for_mobile(|claims| {
|
||||||
|
claims.ipv6_addresses.insert(ip);
|
||||||
|
})
|
||||||
|
.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) => {
|
||||||
@@ -740,7 +869,30 @@ impl SharedVirtualNicMember {
|
|||||||
let mut shared_nic = self.shared_nic.lock().await;
|
let mut shared_nic = self.shared_nic.lock().await;
|
||||||
let mut claims = shared_nic.ifcfg.claims_of(self.member_id);
|
let mut claims = shared_nic.ifcfg.claims_of(self.member_id);
|
||||||
update(&mut claims);
|
update(&mut claims);
|
||||||
shared_nic.apply_member_claims(self.member_id, claims).await
|
shared_nic
|
||||||
|
.apply_member_claims_for_registration(
|
||||||
|
self.member_id,
|
||||||
|
self.registration.registration_id,
|
||||||
|
claims,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
async fn update_claims_for_mobile<F>(&self, update: F) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut SharedIfConfigClaims) + Send,
|
||||||
|
{
|
||||||
|
let mut shared_nic = self.shared_nic.lock().await;
|
||||||
|
let mut claims = shared_nic.ifcfg.claims_of(self.member_id);
|
||||||
|
update(&mut claims);
|
||||||
|
shared_nic
|
||||||
|
.apply_member_claims_for_mobile_registration(
|
||||||
|
self.member_id,
|
||||||
|
self.registration.registration_id,
|
||||||
|
claims,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1081,6 +1233,40 @@ mod tests {
|
|||||||
drop(shared_nic.nic());
|
drop(shared_nic.nic());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stale_member_registration_cleanup_keeps_current_claims() {
|
||||||
|
let mut shared_nic = SharedVirtualNic::new(virtual_nic_config());
|
||||||
|
let member = member_id(1);
|
||||||
|
let old_registration = uuid::Uuid::from_u128(10);
|
||||||
|
let current_registration = uuid::Uuid::from_u128(11);
|
||||||
|
let ip = Ipv4Inet::from_str("10.50.0.2/24").unwrap();
|
||||||
|
|
||||||
|
shared_nic
|
||||||
|
.member_registrations
|
||||||
|
.insert(member, current_registration);
|
||||||
|
shared_nic.ifcfg_mut().apply_member_claims(
|
||||||
|
member,
|
||||||
|
SharedIfConfigClaims {
|
||||||
|
ipv4_addresses: BTreeSet::from([ip]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
shared_nic
|
||||||
|
.remove_member_registration_claims(member, old_registration)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
shared_nic.ifcfg().owners_of_ipv4_address(&ip),
|
||||||
|
BTreeSet::from([member])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shared_nic.member_registrations.get(&member),
|
||||||
|
Some(¤t_registration)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_reuses_shared_virtual_nic_for_same_dev_name() {
|
fn registry_reuses_shared_virtual_nic_for_same_dev_name() {
|
||||||
let mut registry = SharedVirtualNicRegistry::new();
|
let mut registry = SharedVirtualNicRegistry::new();
|
||||||
|
|||||||
@@ -5,19 +5,28 @@ use std::{
|
|||||||
Arc, Mutex as StdMutex,
|
Arc, Mutex as StdMutex,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use cidr::{Ipv4Inet, Ipv6Inet};
|
use cidr::{Ipv4Inet, Ipv6Inet};
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
|
#[cfg(mobile)]
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
#[cfg(mobile)]
|
||||||
|
use tokio::runtime::{Builder, Runtime};
|
||||||
|
#[cfg(mobile)]
|
||||||
|
use tokio::sync::{Mutex, watch};
|
||||||
use tokio::sync::{Notify, mpsc, oneshot};
|
use tokio::sync::{Notify, mpsc, oneshot};
|
||||||
use tokio_util::task::AbortOnDropHandle;
|
use tokio_util::task::AbortOnDropHandle;
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
use crate::instance::virtual_nic::VirtualNic;
|
||||||
use crate::{
|
use crate::{
|
||||||
common::error::Error,
|
common::error::Error,
|
||||||
tunnel::{Tunnel, ZCPacketSink, ZCPacketStream, packet_def::ZCPacket},
|
tunnel::{Tunnel, ZCPacketSink, ZCPacketStream, packet_def::ZCPacket},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::SharedVirtualNicMemberId;
|
use super::{SharedVirtualNicMemberId, SharedVirtualNicMemberRegistrationId};
|
||||||
|
|
||||||
const MEMBER_TUNNEL_BUFFER_SIZE: usize = 1024;
|
const MEMBER_TUNNEL_BUFFER_SIZE: usize = 1024;
|
||||||
const FLOW_OWNER_LIMIT: usize = 4096;
|
const FLOW_OWNER_LIMIT: usize = 4096;
|
||||||
@@ -27,6 +36,24 @@ const TCP_HEADER_MIN_LEN: usize = 20;
|
|||||||
const UDP_HEADER_LEN: usize = 8;
|
const UDP_HEADER_LEN: usize = 8;
|
||||||
const TCP_PROTOCOL: u8 = 6;
|
const TCP_PROTOCOL: u8 = 6;
|
||||||
const UDP_PROTOCOL: u8 = 17;
|
const UDP_PROTOCOL: u8 = 17;
|
||||||
|
const DISPATCHER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
#[cfg(mobile)]
|
||||||
|
const MOBILE_REBUILD_INITIAL_DELAY: Duration = Duration::from_millis(100);
|
||||||
|
#[cfg(mobile)]
|
||||||
|
const MOBILE_REBUILD_MAX_DELAY: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
fn mobile_dispatcher_runtime() -> &'static Runtime {
|
||||||
|
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
|
||||||
|
RUNTIME.get_or_init(|| {
|
||||||
|
Builder::new_multi_thread()
|
||||||
|
.worker_threads(1)
|
||||||
|
.thread_name("easytier-shared-tun")
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("failed to build shared virtual nic mobile dispatcher runtime")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
struct SharedVirtualNicMemberPacket {
|
struct SharedVirtualNicMemberPacket {
|
||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
@@ -40,12 +67,17 @@ enum SharedVirtualNicControl {
|
|||||||
},
|
},
|
||||||
Unregister {
|
Unregister {
|
||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
},
|
},
|
||||||
UpdateSources {
|
UpdateSources {
|
||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
sources: BTreeSet<SharedVirtualNicFlowAddr>,
|
sources: BTreeSet<SharedVirtualNicFlowAddr>,
|
||||||
ack: oneshot::Sender<()>,
|
ack: oneshot::Sender<()>,
|
||||||
},
|
},
|
||||||
|
Shutdown {
|
||||||
|
invalidate: bool,
|
||||||
|
ack: oneshot::Sender<()>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
@@ -60,6 +92,7 @@ struct SharedVirtualNicMemberTunnelTableState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SharedVirtualNicMemberTunnelEntry {
|
struct SharedVirtualNicMemberTunnelEntry {
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
sender: mpsc::Sender<ZCPacket>,
|
sender: mpsc::Sender<ZCPacket>,
|
||||||
close_notifier: Arc<Notify>,
|
close_notifier: Arc<Notify>,
|
||||||
_tasks: Vec<AbortOnDropHandle<()>>,
|
_tasks: Vec<AbortOnDropHandle<()>>,
|
||||||
@@ -76,7 +109,7 @@ impl SharedVirtualNicMemberTunnelTable {
|
|||||||
state.control_sender = Some(control_sender);
|
state.control_sender = Some(control_sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detach_dispatcher(&self) {
|
pub(super) fn detach_dispatcher(&self) {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
state.to_tun_sender.take();
|
state.to_tun_sender.take();
|
||||||
state.control_sender.take();
|
state.control_sender.take();
|
||||||
@@ -85,6 +118,7 @@ impl SharedVirtualNicMemberTunnelTable {
|
|||||||
pub(super) fn register(
|
pub(super) fn register(
|
||||||
&self,
|
&self,
|
||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
tunnel: Box<dyn Tunnel>,
|
tunnel: Box<dyn Tunnel>,
|
||||||
close_notifier: Arc<Notify>,
|
close_notifier: Arc<Notify>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
@@ -121,7 +155,10 @@ impl SharedVirtualNicMemberTunnelTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = reader_control_sender.send(SharedVirtualNicControl::Unregister { member_id });
|
let _ = reader_control_sender.send(SharedVirtualNicControl::Unregister {
|
||||||
|
member_id,
|
||||||
|
registration_id,
|
||||||
|
});
|
||||||
reader_close_notifier.notify_one();
|
reader_close_notifier.notify_one();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -131,8 +168,10 @@ impl SharedVirtualNicMemberTunnelTable {
|
|||||||
while let Some(packet) = to_member_receiver.recv().await {
|
while let Some(packet) = to_member_receiver.recv().await {
|
||||||
if let Err(err) = member_sink.send(packet).await {
|
if let Err(err) = member_sink.send(packet).await {
|
||||||
tracing::error!(?member_id, ?err, "shared member tunnel write failed");
|
tracing::error!(?member_id, ?err, "shared member tunnel write failed");
|
||||||
let _ = writer_control_sender
|
let _ = writer_control_sender.send(SharedVirtualNicControl::Unregister {
|
||||||
.send(SharedVirtualNicControl::Unregister { member_id });
|
member_id,
|
||||||
|
registration_id,
|
||||||
|
});
|
||||||
writer_close_notifier.notify_one();
|
writer_close_notifier.notify_one();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -140,6 +179,7 @@ impl SharedVirtualNicMemberTunnelTable {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
let entry = SharedVirtualNicMemberTunnelEntry {
|
let entry = SharedVirtualNicMemberTunnelEntry {
|
||||||
|
registration_id,
|
||||||
sender: to_member_sender,
|
sender: to_member_sender,
|
||||||
close_notifier,
|
close_notifier,
|
||||||
_tasks: vec![reader_task, writer_task],
|
_tasks: vec![reader_task, writer_task],
|
||||||
@@ -152,11 +192,18 @@ impl SharedVirtualNicMemberTunnelTable {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn unregister(&self, member_id: SharedVirtualNicMemberId) {
|
pub(super) fn unregister(
|
||||||
|
&self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
) {
|
||||||
let Some(control_sender) = self.control_sender() else {
|
let Some(control_sender) = self.control_sender() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let _ = control_sender.send(SharedVirtualNicControl::Unregister { member_id });
|
let _ = control_sender.send(SharedVirtualNicControl::Unregister {
|
||||||
|
member_id,
|
||||||
|
registration_id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatcher_channels(
|
fn dispatcher_channels(
|
||||||
@@ -309,6 +356,8 @@ impl SharedVirtualNicFlowTable {
|
|||||||
pub(super) struct SharedVirtualNicDispatcher {
|
pub(super) struct SharedVirtualNicDispatcher {
|
||||||
_task: AbortOnDropHandle<()>,
|
_task: AbortOnDropHandle<()>,
|
||||||
control_sender: mpsc::UnboundedSender<SharedVirtualNicControl>,
|
control_sender: mpsc::UnboundedSender<SharedVirtualNicControl>,
|
||||||
|
#[cfg(mobile)]
|
||||||
|
mobile_tun_fd_sender: Option<watch::Sender<std::os::fd::RawFd>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharedVirtualNicDispatcher {
|
impl SharedVirtualNicDispatcher {
|
||||||
@@ -335,6 +384,51 @@ impl SharedVirtualNicDispatcher {
|
|||||||
Self {
|
Self {
|
||||||
_task: AbortOnDropHandle::new(tokio::spawn(task.run())),
|
_task: AbortOnDropHandle::new(tokio::spawn(task.run())),
|
||||||
control_sender,
|
control_sender,
|
||||||
|
#[cfg(mobile)]
|
||||||
|
mobile_tun_fd_sender: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
pub(super) fn start_for_mobile(
|
||||||
|
nic: Arc<Mutex<VirtualNic>>,
|
||||||
|
tun_fd: std::os::fd::RawFd,
|
||||||
|
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
||||||
|
valid: Arc<AtomicBool>,
|
||||||
|
) -> Self {
|
||||||
|
let (to_tun_sender, to_tun_receiver) = mpsc::channel(MEMBER_TUNNEL_BUFFER_SIZE);
|
||||||
|
let (control_sender, control_receiver) = mpsc::unbounded_channel();
|
||||||
|
let (mobile_tun_fd_sender, mobile_tun_fd_receiver) = watch::channel(tun_fd);
|
||||||
|
let task_member_tunnel_table = member_tunnel_table.clone();
|
||||||
|
member_tunnel_table.attach_dispatcher(to_tun_sender, control_sender.clone());
|
||||||
|
|
||||||
|
let task = mobile_dispatcher_runtime().spawn(async move {
|
||||||
|
let task = SharedVirtualNicMobileDispatcherTask {
|
||||||
|
nic,
|
||||||
|
tun_stream: None,
|
||||||
|
tun_sink: None,
|
||||||
|
tun_fd: mobile_tun_fd_receiver,
|
||||||
|
to_tun_receiver,
|
||||||
|
control_receiver,
|
||||||
|
member_tunnel_table: task_member_tunnel_table,
|
||||||
|
valid,
|
||||||
|
state: SharedVirtualNicDispatcherState::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
task.run().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
_task: AbortOnDropHandle::new(task),
|
||||||
|
control_sender,
|
||||||
|
mobile_tun_fd_sender: Some(mobile_tun_fd_sender),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
pub(super) fn update_mobile_tun_fd(&self, tun_fd: std::os::fd::RawFd) {
|
||||||
|
if let Some(sender) = &self.mobile_tun_fd_sender {
|
||||||
|
sender.send_replace(tun_fd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +465,27 @@ impl SharedVirtualNicDispatcher {
|
|||||||
rx.await
|
rx.await
|
||||||
.map_err(|_| anyhow::anyhow!("shared virtual nic dispatcher is not running").into())
|
.map_err(|_| anyhow::anyhow!("shared virtual nic dispatcher is not running").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn shutdown_without_invalidation(self) {
|
||||||
|
let (ack, rx) = oneshot::channel();
|
||||||
|
if self
|
||||||
|
.control_sender
|
||||||
|
.send(SharedVirtualNicControl::Shutdown {
|
||||||
|
invalidate: false,
|
||||||
|
ack,
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokio::time::timeout(DISPATCHER_SHUTDOWN_TIMEOUT, rx)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
tracing::warn!("timed out shutting down shared virtual nic dispatcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SharedVirtualNicDispatcherTask {
|
struct SharedVirtualNicDispatcherTask {
|
||||||
@@ -391,7 +506,14 @@ impl SharedVirtualNicDispatcherTask {
|
|||||||
let Some(control) = control else {
|
let Some(control) = control else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
self.state.handle_control(control);
|
match control {
|
||||||
|
SharedVirtualNicControl::Shutdown { invalidate, ack } => {
|
||||||
|
self.cleanup(invalidate);
|
||||||
|
let _ = ack.send(());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
other => self.state.handle_control(other),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
member_packet = self.to_tun_receiver.recv() => {
|
member_packet = self.to_tun_receiver.recv() => {
|
||||||
let Some(member_packet) = member_packet else {
|
let Some(member_packet) = member_packet else {
|
||||||
@@ -417,7 +539,13 @@ impl SharedVirtualNicDispatcherTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.cleanup(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, invalidate: bool) {
|
||||||
|
if invalidate {
|
||||||
self.valid.store(false, Ordering::Release);
|
self.valid.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
self.member_tunnel_table.detach_dispatcher();
|
self.member_tunnel_table.detach_dispatcher();
|
||||||
self.state.close_all();
|
self.state.close_all();
|
||||||
}
|
}
|
||||||
@@ -436,6 +564,207 @@ impl SharedVirtualNicDispatcherTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
struct SharedVirtualNicMobileDispatcherTask {
|
||||||
|
nic: Arc<Mutex<VirtualNic>>,
|
||||||
|
tun_stream: Option<Pin<Box<dyn ZCPacketStream>>>,
|
||||||
|
tun_sink: Option<Pin<Box<dyn ZCPacketSink>>>,
|
||||||
|
tun_fd: watch::Receiver<std::os::fd::RawFd>,
|
||||||
|
to_tun_receiver: mpsc::Receiver<SharedVirtualNicMemberPacket>,
|
||||||
|
control_receiver: mpsc::UnboundedReceiver<SharedVirtualNicControl>,
|
||||||
|
member_tunnel_table: SharedVirtualNicMemberTunnelTable,
|
||||||
|
valid: Arc<AtomicBool>,
|
||||||
|
state: SharedVirtualNicDispatcherState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
impl SharedVirtualNicMobileDispatcherTask {
|
||||||
|
async fn run(mut self) {
|
||||||
|
let mut rebuild_delay = MOBILE_REBUILD_INITIAL_DELAY;
|
||||||
|
let mut wait_before_rebuild = false;
|
||||||
|
let mut rebuild_deadline = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if self.tun_stream.is_none() {
|
||||||
|
if !wait_before_rebuild {
|
||||||
|
if !self.rebuild_tun().await {
|
||||||
|
wait_before_rebuild = true;
|
||||||
|
rebuild_deadline = None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deadline = *rebuild_deadline
|
||||||
|
.get_or_insert_with(|| tokio::time::Instant::now() + rebuild_delay);
|
||||||
|
tokio::select! {
|
||||||
|
control = self.control_receiver.recv() => {
|
||||||
|
if !self.handle_control(control) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member_packet = self.to_tun_receiver.recv() => {
|
||||||
|
let Some(member_packet) = member_packet else {
|
||||||
|
self.cleanup(true);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
tracing::trace!(
|
||||||
|
member_id = ?member_packet.member_id,
|
||||||
|
"shared virtual nic dropped member packet while rebuilding mobile tun"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
changed = self.tun_fd.changed() => {
|
||||||
|
if changed.is_err() {
|
||||||
|
self.cleanup(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rebuild_delay = MOBILE_REBUILD_INITIAL_DELAY;
|
||||||
|
wait_before_rebuild = false;
|
||||||
|
rebuild_deadline = None;
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep_until(deadline) => {
|
||||||
|
rebuild_delay = next_mobile_rebuild_delay(rebuild_delay);
|
||||||
|
wait_before_rebuild = false;
|
||||||
|
rebuild_deadline = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
control = self.control_receiver.recv() => {
|
||||||
|
if !self.handle_control(control) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member_packet = self.to_tun_receiver.recv() => {
|
||||||
|
let Some(member_packet) = member_packet else {
|
||||||
|
self.cleanup(true);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.forward_member_packet_to_tun(member_packet).await {
|
||||||
|
wait_before_rebuild = true;
|
||||||
|
rebuild_deadline = None;
|
||||||
|
} else {
|
||||||
|
rebuild_delay = MOBILE_REBUILD_INITIAL_DELAY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packet = self.tun_stream.as_mut().expect("mobile tun stream should exist").next() => {
|
||||||
|
let Some(packet) = packet else {
|
||||||
|
tracing::error!("shared virtual nic mobile tun stream closed");
|
||||||
|
self.drop_tun();
|
||||||
|
wait_before_rebuild = true;
|
||||||
|
rebuild_deadline = None;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let packet = match packet {
|
||||||
|
Ok(packet) => packet,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(?err, "shared virtual nic read from mobile tun failed");
|
||||||
|
self.drop_tun();
|
||||||
|
wait_before_rebuild = true;
|
||||||
|
rebuild_deadline = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rebuild_delay = MOBILE_REBUILD_INITIAL_DELAY;
|
||||||
|
self.state.forward_tun_packet_to_member(packet).await;
|
||||||
|
}
|
||||||
|
changed = self.tun_fd.changed() => {
|
||||||
|
if changed.is_err() {
|
||||||
|
self.cleanup(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.drop_tun();
|
||||||
|
rebuild_delay = MOBILE_REBUILD_INITIAL_DELAY;
|
||||||
|
wait_before_rebuild = false;
|
||||||
|
rebuild_deadline = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rebuild_tun(&mut self) -> bool {
|
||||||
|
let tun_fd = *self.tun_fd.borrow_and_update();
|
||||||
|
match self.nic.lock().await.create_dev_for_mobile(tun_fd).await {
|
||||||
|
Ok(tunnel) => {
|
||||||
|
let (tun_stream, tun_sink) = tunnel.split();
|
||||||
|
self.tun_stream = Some(tun_stream);
|
||||||
|
self.tun_sink = Some(tun_sink);
|
||||||
|
tracing::info!(fd = tun_fd, "rebuilt shared virtual nic mobile tun");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(
|
||||||
|
fd = tun_fd,
|
||||||
|
?err,
|
||||||
|
"failed to rebuild shared virtual nic mobile tun"
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn forward_member_packet_to_tun(
|
||||||
|
&mut self,
|
||||||
|
member_packet: SharedVirtualNicMemberPacket,
|
||||||
|
) -> bool {
|
||||||
|
self.state
|
||||||
|
.remember_reverse_owner(member_packet.member_id, &member_packet.packet);
|
||||||
|
let Some(tun_sink) = self.tun_sink.as_mut() else {
|
||||||
|
tracing::trace!(
|
||||||
|
member_id = ?member_packet.member_id,
|
||||||
|
"shared virtual nic dropped member packet without mobile tun"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = tun_sink.send(member_packet.packet).await {
|
||||||
|
tracing::error!(?err, "shared virtual nic write to mobile tun failed");
|
||||||
|
self.drop_tun();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_control(&mut self, control: Option<SharedVirtualNicControl>) -> bool {
|
||||||
|
let Some(control) = control else {
|
||||||
|
self.cleanup(true);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
match control {
|
||||||
|
SharedVirtualNicControl::Shutdown { invalidate, ack } => {
|
||||||
|
self.cleanup(invalidate);
|
||||||
|
let _ = ack.send(());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
self.state.handle_control(other);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_tun(&mut self) {
|
||||||
|
self.tun_stream.take();
|
||||||
|
self.tun_sink.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, invalidate: bool) {
|
||||||
|
self.drop_tun();
|
||||||
|
if invalidate {
|
||||||
|
self.valid.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
|
self.member_tunnel_table.detach_dispatcher();
|
||||||
|
self.state.close_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
fn next_mobile_rebuild_delay(delay: Duration) -> Duration {
|
||||||
|
delay.saturating_mul(2).min(MOBILE_REBUILD_MAX_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct SharedVirtualNicDispatcherState {
|
struct SharedVirtualNicDispatcherState {
|
||||||
members: BTreeMap<SharedVirtualNicMemberId, SharedVirtualNicMemberTunnelEntry>,
|
members: BTreeMap<SharedVirtualNicMemberId, SharedVirtualNicMemberTunnelEntry>,
|
||||||
@@ -449,8 +778,11 @@ impl SharedVirtualNicDispatcherState {
|
|||||||
SharedVirtualNicControl::Register { member_id, entry } => {
|
SharedVirtualNicControl::Register { member_id, entry } => {
|
||||||
self.register(member_id, entry);
|
self.register(member_id, entry);
|
||||||
}
|
}
|
||||||
SharedVirtualNicControl::Unregister { member_id } => {
|
SharedVirtualNicControl::Unregister {
|
||||||
self.unregister(member_id);
|
member_id,
|
||||||
|
registration_id,
|
||||||
|
} => {
|
||||||
|
self.unregister(member_id, registration_id);
|
||||||
}
|
}
|
||||||
SharedVirtualNicControl::UpdateSources {
|
SharedVirtualNicControl::UpdateSources {
|
||||||
member_id,
|
member_id,
|
||||||
@@ -460,6 +792,9 @@ impl SharedVirtualNicDispatcherState {
|
|||||||
self.source_table.update_member_sources(member_id, sources);
|
self.source_table.update_member_sources(member_id, sources);
|
||||||
let _ = ack.send(());
|
let _ = ack.send(());
|
||||||
}
|
}
|
||||||
|
SharedVirtualNicControl::Shutdown { .. } => {
|
||||||
|
unreachable!("dispatcher shutdown is handled by the dispatcher task")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +807,20 @@ impl SharedVirtualNicDispatcherState {
|
|||||||
drop(old_entry);
|
drop(old_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unregister(&mut self, member_id: SharedVirtualNicMemberId) {
|
fn unregister(
|
||||||
|
&mut self,
|
||||||
|
member_id: SharedVirtualNicMemberId,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
) {
|
||||||
|
if self
|
||||||
|
.members
|
||||||
|
.get(&member_id)
|
||||||
|
.map(|entry| entry.registration_id)
|
||||||
|
!= Some(registration_id)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let entry = self.members.remove(&member_id);
|
let entry = self.members.remove(&member_id);
|
||||||
drop(entry);
|
drop(entry);
|
||||||
self.flow_table.remove_owner(member_id);
|
self.flow_table.remove_owner(member_id);
|
||||||
@@ -530,10 +878,10 @@ impl SharedVirtualNicDispatcherState {
|
|||||||
member_id: SharedVirtualNicMemberId,
|
member_id: SharedVirtualNicMemberId,
|
||||||
packet: ZCPacket,
|
packet: ZCPacket,
|
||||||
) -> Result<(), ZCPacket> {
|
) -> Result<(), ZCPacket> {
|
||||||
let Some(sender) = self
|
let Some((registration_id, sender)) = self
|
||||||
.members
|
.members
|
||||||
.get(&member_id)
|
.get(&member_id)
|
||||||
.map(|entry| entry.sender.clone())
|
.map(|entry| (entry.registration_id, entry.sender.clone()))
|
||||||
else {
|
else {
|
||||||
return Err(packet);
|
return Err(packet);
|
||||||
};
|
};
|
||||||
@@ -541,7 +889,7 @@ impl SharedVirtualNicDispatcherState {
|
|||||||
match sender.send(packet).await {
|
match sender.send(packet).await {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.unregister(member_id);
|
self.unregister(member_id, registration_id);
|
||||||
Err(err.0)
|
Err(err.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -690,7 +1038,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn member_entry(sender: mpsc::Sender<ZCPacket>) -> SharedVirtualNicMemberTunnelEntry {
|
fn member_entry(sender: mpsc::Sender<ZCPacket>) -> SharedVirtualNicMemberTunnelEntry {
|
||||||
|
member_entry_with_registration(sender, uuid::Uuid::from_u128(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn member_entry_with_registration(
|
||||||
|
sender: mpsc::Sender<ZCPacket>,
|
||||||
|
registration_id: SharedVirtualNicMemberRegistrationId,
|
||||||
|
) -> SharedVirtualNicMemberTunnelEntry {
|
||||||
SharedVirtualNicMemberTunnelEntry {
|
SharedVirtualNicMemberTunnelEntry {
|
||||||
|
registration_id,
|
||||||
sender,
|
sender,
|
||||||
close_notifier: Arc::new(Notify::new()),
|
close_notifier: Arc::new(Notify::new()),
|
||||||
_tasks: Vec::new(),
|
_tasks: Vec::new(),
|
||||||
@@ -772,7 +1128,7 @@ mod tests {
|
|||||||
owner,
|
owner,
|
||||||
BTreeSet::from([SharedVirtualNicFlowAddr::V6(source.octets())]),
|
BTreeSet::from([SharedVirtualNicFlowAddr::V6(source.octets())]),
|
||||||
);
|
);
|
||||||
state.unregister(owner);
|
state.unregister(owner, uuid::Uuid::from_u128(1));
|
||||||
state
|
state
|
||||||
.forward_tun_packet_to_member(ipv6_packet(source, dst))
|
.forward_tun_packet_to_member(ipv6_packet(source, dst))
|
||||||
.await;
|
.await;
|
||||||
@@ -780,6 +1136,28 @@ mod tests {
|
|||||||
assert!(fallback_receiver.try_recv().is_err());
|
assert!(fallback_receiver.try_recv().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn dispatcher_ignores_stale_member_unregister() {
|
||||||
|
let member_id = uuid::Uuid::from_u128(1);
|
||||||
|
let stale_registration = uuid::Uuid::from_u128(10);
|
||||||
|
let current_registration = uuid::Uuid::from_u128(11);
|
||||||
|
let src = "2001:db8::1".parse::<Ipv6Addr>().unwrap();
|
||||||
|
let dst = "2001:db8:ffff::1".parse::<Ipv6Addr>().unwrap();
|
||||||
|
let (sender, mut receiver) = mpsc::channel(1);
|
||||||
|
let mut state = SharedVirtualNicDispatcherState::default();
|
||||||
|
|
||||||
|
state.register(
|
||||||
|
member_id,
|
||||||
|
member_entry_with_registration(sender, current_registration),
|
||||||
|
);
|
||||||
|
state.unregister(member_id, stale_registration);
|
||||||
|
state
|
||||||
|
.forward_tun_packet_to_member(ipv6_packet(src, dst))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(receiver.try_recv().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn dispatcher_invalidates_shared_nic_when_tun_read_fails() {
|
async fn dispatcher_invalidates_shared_nic_when_tun_read_fails() {
|
||||||
let member_id = uuid::Uuid::from_u128(1);
|
let member_id = uuid::Uuid::from_u128(1);
|
||||||
@@ -800,7 +1178,12 @@ mod tests {
|
|||||||
let (_member_tunnel, shared_tunnel) = create_ring_tunnel_pair();
|
let (_member_tunnel, shared_tunnel) = create_ring_tunnel_pair();
|
||||||
|
|
||||||
member_tunnel_table
|
member_tunnel_table
|
||||||
.register(member_id, shared_tunnel, close_notifier.clone())
|
.register(
|
||||||
|
member_id,
|
||||||
|
uuid::Uuid::from_u128(1),
|
||||||
|
shared_tunnel,
|
||||||
|
close_notifier.clone(),
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
dispatcher
|
dispatcher
|
||||||
.update_sources(member_id, &BTreeSet::new(), &BTreeSet::new())
|
.update_sources(member_id, &BTreeSet::new(), &BTreeSet::new())
|
||||||
@@ -815,4 +1198,45 @@ mod tests {
|
|||||||
assert!(!valid.load(Ordering::Acquire));
|
assert!(!valid.load(Ordering::Acquire));
|
||||||
assert!(member_tunnel_table.dispatcher_channels().is_none());
|
assert!(member_tunnel_table.dispatcher_channels().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn dispatcher_shutdown_for_replacement_keeps_shared_nic_valid() {
|
||||||
|
let member_id = uuid::Uuid::from_u128(1);
|
||||||
|
let (_tun_tx, tun_rx) = mpsc::unbounded_channel();
|
||||||
|
let tun_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(tun_rx);
|
||||||
|
let tun_sink = futures::sink::unfold((), |(), _packet: ZCPacket| async {
|
||||||
|
Ok::<(), TunnelError>(())
|
||||||
|
});
|
||||||
|
let tunnel = TunnelWrapper::new(tun_stream, tun_sink, None);
|
||||||
|
let member_tunnel_table = SharedVirtualNicMemberTunnelTable::default();
|
||||||
|
let valid = Arc::new(AtomicBool::new(true));
|
||||||
|
let dispatcher = SharedVirtualNicDispatcher::start(
|
||||||
|
Box::new(tunnel),
|
||||||
|
member_tunnel_table.clone(),
|
||||||
|
valid.clone(),
|
||||||
|
);
|
||||||
|
let close_notifier = Arc::new(Notify::new());
|
||||||
|
let (_member_tunnel, shared_tunnel) = create_ring_tunnel_pair();
|
||||||
|
|
||||||
|
member_tunnel_table
|
||||||
|
.register(
|
||||||
|
member_id,
|
||||||
|
uuid::Uuid::from_u128(1),
|
||||||
|
shared_tunnel,
|
||||||
|
close_notifier.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
dispatcher
|
||||||
|
.update_sources(member_id, &BTreeSet::new(), &BTreeSet::new())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
dispatcher.shutdown_without_invalidation().await;
|
||||||
|
|
||||||
|
tokio::time::timeout(Duration::from_secs(1), close_notifier.notified())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(valid.load(Ordering::Acquire));
|
||||||
|
assert!(member_tunnel_table.dispatcher_channels().is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -599,6 +599,11 @@ impl VirtualNic {
|
|||||||
Ok(tun::create(&config)?)
|
Ok(tun::create(&config)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
pub fn set_mobile_tun_fd_name(&mut self, tun_fd: std::os::fd::RawFd) {
|
||||||
|
self.ifname = Some(format!("tunfd_{}", tun_fd));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(mobile)]
|
#[cfg(mobile)]
|
||||||
pub async fn create_dev_for_mobile(
|
pub async fn create_dev_for_mobile(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -634,7 +639,7 @@ impl VirtualNic {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.ifname = Some(format!("tunfd_{}", tun_fd));
|
self.set_mobile_tun_fd_name(tun_fd);
|
||||||
|
|
||||||
Ok(Box::new(ft))
|
Ok(Box::new(ft))
|
||||||
}
|
}
|
||||||
@@ -986,6 +991,22 @@ impl NicBackend {
|
|||||||
Self::Shared(member) => member.add_ipv6(ip, cidr).await,
|
Self::Shared(member) => member.add_ipv6(ip, cidr).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
pub async fn add_mobile_source_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> {
|
||||||
|
match self {
|
||||||
|
Self::Dedicated(_) => Ok(()),
|
||||||
|
Self::Shared(member) => member.add_mobile_source_ip(ip, cidr).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(mobile)]
|
||||||
|
pub async fn add_mobile_source_ipv6(&self, ip: Ipv6Addr, cidr: i32) -> Result<(), Error> {
|
||||||
|
match self {
|
||||||
|
Self::Dedicated(_) => Ok(()),
|
||||||
|
Self::Shared(member) => member.add_mobile_source_ipv6(ip, cidr).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NicCtx {
|
pub struct NicCtx {
|
||||||
@@ -1673,16 +1694,14 @@ 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) -> Result<(), Error> {
|
||||||
let tunnel = 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
|
||||||
.backend
|
.backend
|
||||||
.ifname()
|
.ifname()
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| anyhow::anyhow!("tun device has no interface name"))?;
|
.ok_or_else(|| anyhow::anyhow!("tun device has no interface name"))?;
|
||||||
self.global_ctx
|
(ret, ifname)
|
||||||
.issue_event(GlobalCtxEvent::TunDeviceReady(ifname));
|
|
||||||
ret
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.global_ctx
|
self.global_ctx
|
||||||
@@ -1691,6 +1710,20 @@ impl NicCtx {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(ipv4_addr) = self.global_ctx.get_ipv4() {
|
||||||
|
self.backend
|
||||||
|
.add_mobile_source_ip(ipv4_addr.address(), ipv4_addr.network_length() as i32)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
if let Some(ipv6_addr) = self.global_ctx.get_ipv6() {
|
||||||
|
self.backend
|
||||||
|
.add_mobile_source_ipv6(ipv6_addr.address(), ipv6_addr.network_length() as i32)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.global_ctx
|
||||||
|
.issue_event(GlobalCtxEvent::TunDeviceReady(ifname));
|
||||||
|
|
||||||
let (stream, sink) = tunnel.split();
|
let (stream, sink) = tunnel.split();
|
||||||
|
|
||||||
self.do_forward_nic_to_peers_task(stream)?;
|
self.do_forward_nic_to_peers_task(stream)?;
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ class TauriVpnService : VpnService() {
|
|||||||
@JvmField var triggerCallback: (String, JSObject) -> Unit = { _, _ -> }
|
@JvmField var triggerCallback: (String, JSObject) -> Unit = { _, _ -> }
|
||||||
@JvmField var self: TauriVpnService? = null
|
@JvmField var self: TauriVpnService? = null
|
||||||
@JvmField var ipv4Addr: String? = null
|
@JvmField var ipv4Addr: String? = null
|
||||||
|
@JvmField var ipv4Addrs: Array<String> = emptyArray()
|
||||||
@JvmField var routes: Array<String> = emptyArray()
|
@JvmField var routes: Array<String> = emptyArray()
|
||||||
@JvmField var dns: String? = null
|
@JvmField var dns: String? = null
|
||||||
|
|
||||||
const val IPV4_ADDR = "IPV4_ADDR"
|
const val IPV4_ADDR = "IPV4_ADDR"
|
||||||
|
const val IPV4_ADDRS = "IPV4_ADDRS"
|
||||||
const val ROUTES = "ROUTES"
|
const val ROUTES = "ROUTES"
|
||||||
const val DNS = "DNS"
|
const val DNS = "DNS"
|
||||||
const val DISALLOWED_APPLICATIONS = "DISALLOWED_APPLICATIONS"
|
const val DISALLOWED_APPLICATIONS = "DISALLOWED_APPLICATIONS"
|
||||||
@@ -30,7 +32,8 @@ class TauriVpnService : VpnService() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
println("vpn on start command ${intent?.getExtras()} $intent")
|
println("vpn on start command ${intent?.getExtras()} $intent")
|
||||||
var args = intent?.getExtras()
|
var args = intent?.getExtras()
|
||||||
ipv4Addr = args?.getString(IPV4_ADDR)
|
ipv4Addrs = getIpv4Addrs(args)
|
||||||
|
ipv4Addr = ipv4Addrs.firstOrNull()
|
||||||
routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
||||||
dns = args?.getString(DNS)
|
dns = args?.getString(DNS)
|
||||||
|
|
||||||
@@ -74,28 +77,44 @@ class TauriVpnService : VpnService() {
|
|||||||
|
|
||||||
private fun clearStatus() {
|
private fun clearStatus() {
|
||||||
ipv4Addr = null
|
ipv4Addr = null
|
||||||
|
ipv4Addrs = emptyArray()
|
||||||
routes = emptyArray()
|
routes = emptyArray()
|
||||||
dns = null
|
dns = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getIpv4Addrs(args: Bundle?): Array<String> {
|
||||||
|
val ipv4Addrs = args
|
||||||
|
?.getStringArray(IPV4_ADDRS)
|
||||||
|
?.filter { it.isNotBlank() }
|
||||||
|
?.toTypedArray()
|
||||||
|
?: emptyArray()
|
||||||
|
if (ipv4Addrs.isNotEmpty()) {
|
||||||
|
return ipv4Addrs
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrayOf(args?.getString(IPV4_ADDR) ?: "10.126.126.1/24")
|
||||||
|
}
|
||||||
|
|
||||||
private fun createVpnInterface(args: Bundle?): ParcelFileDescriptor {
|
private fun createVpnInterface(args: Bundle?): ParcelFileDescriptor {
|
||||||
var builder = Builder()
|
var builder = Builder()
|
||||||
.setSession("TauriVpnService")
|
.setSession("TauriVpnService")
|
||||||
.setBlocking(false)
|
.setBlocking(false)
|
||||||
|
|
||||||
var mtu = args?.getInt(MTU) ?: 1500
|
var mtu = args?.getInt(MTU) ?: 1500
|
||||||
var ipv4Addr = args?.getString(IPV4_ADDR) ?: "10.126.126.1/24"
|
var ipv4Addrs = getIpv4Addrs(args)
|
||||||
var dns: String? = args?.getString(DNS)
|
var dns: String? = args?.getString(DNS)
|
||||||
var routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
var routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
||||||
var disallowedApplications = args?.getStringArray(DISALLOWED_APPLICATIONS) ?: emptyArray()
|
var disallowedApplications = args?.getStringArray(DISALLOWED_APPLICATIONS) ?: emptyArray()
|
||||||
|
|
||||||
println("vpn create vpn interface. mtu: $mtu, ipv4Addr: $ipv4Addr, dns:" +
|
println("vpn create vpn interface. mtu: $mtu, ipv4Addrs: ${java.util.Arrays.toString(ipv4Addrs)}, dns:" +
|
||||||
"$dns, routes: ${java.util.Arrays.toString(routes)}," +
|
"$dns, routes: ${java.util.Arrays.toString(routes)}," +
|
||||||
"disallowedApplications: ${java.util.Arrays.toString(disallowedApplications)}")
|
"disallowedApplications: ${java.util.Arrays.toString(disallowedApplications)}")
|
||||||
|
|
||||||
|
for (ipv4Addr in ipv4Addrs) {
|
||||||
val ipParts = ipv4Addr.split("/")
|
val ipParts = ipv4Addr.split("/")
|
||||||
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.addAddress("fd00::1", 128)
|
||||||
|
|
||||||
builder.setMtu(mtu)
|
builder.setMtu(mtu)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import app.tauri.plugin.Invoke
|
|||||||
import app.tauri.plugin.JSObject
|
import app.tauri.plugin.JSObject
|
||||||
import app.tauri.plugin.Plugin
|
import app.tauri.plugin.Plugin
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import org.json.JSONArray
|
||||||
|
|
||||||
@InvokeArg
|
@InvokeArg
|
||||||
class PingArgs {
|
class PingArgs {
|
||||||
@@ -21,6 +22,7 @@ class PingArgs {
|
|||||||
@InvokeArg
|
@InvokeArg
|
||||||
class StartVpnArgs {
|
class StartVpnArgs {
|
||||||
var ipv4Addr: String? = null
|
var ipv4Addr: String? = null
|
||||||
|
var ipv4Addrs: Array<String> = emptyArray()
|
||||||
var routes: Array<String> = emptyArray()
|
var routes: Array<String> = emptyArray()
|
||||||
var dns: String? = null
|
var dns: String? = null
|
||||||
var disallowedApplications: Array<String> = emptyArray()
|
var disallowedApplications: Array<String> = emptyArray()
|
||||||
@@ -85,6 +87,7 @@ class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
|||||||
} else {
|
} else {
|
||||||
val intent = Intent(activity, TauriVpnService::class.java)
|
val intent = Intent(activity, TauriVpnService::class.java)
|
||||||
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
|
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
|
||||||
|
intent.putExtra(TauriVpnService.IPV4_ADDRS, args.ipv4Addrs)
|
||||||
intent.putExtra(TauriVpnService.ROUTES, args.routes)
|
intent.putExtra(TauriVpnService.ROUTES, args.routes)
|
||||||
intent.putExtra(TauriVpnService.DNS, args.dns)
|
intent.putExtra(TauriVpnService.DNS, args.dns)
|
||||||
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
|
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
|
||||||
@@ -112,7 +115,8 @@ class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
|||||||
val ret = JSObject()
|
val ret = JSObject()
|
||||||
ret.put("running", TauriVpnService.self != null)
|
ret.put("running", TauriVpnService.self != null)
|
||||||
ret.put("ipv4Addr", TauriVpnService.ipv4Addr)
|
ret.put("ipv4Addr", TauriVpnService.ipv4Addr)
|
||||||
ret.put("routes", TauriVpnService.routes)
|
ret.put("ipv4Addrs", JSONArray(TauriVpnService.ipv4Addrs))
|
||||||
|
ret.put("routes", JSONArray(TauriVpnService.routes))
|
||||||
ret.put("dns", TauriVpnService.dns)
|
ret.put("dns", TauriVpnService.dns)
|
||||||
invoke.resolve(ret)
|
invoke.resolve(ret)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface InvokeResponse {
|
|||||||
|
|
||||||
export interface StartVpnRequest {
|
export interface StartVpnRequest {
|
||||||
ipv4Addr?: string;
|
ipv4Addr?: string;
|
||||||
|
ipv4Addrs?: string[];
|
||||||
routes?: string[];
|
routes?: string[];
|
||||||
dns?: string;
|
dns?: string;
|
||||||
disallowedApplications?: string[];
|
disallowedApplications?: string[];
|
||||||
@@ -24,6 +25,7 @@ export interface StartVpnRequest {
|
|||||||
export interface VpnStatusResponse {
|
export interface VpnStatusResponse {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
ipv4Addr?: string;
|
ipv4Addr?: string;
|
||||||
|
ipv4Addrs?: string[];
|
||||||
routes?: string[];
|
routes?: string[];
|
||||||
dns?: string;
|
dns?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub struct VoidRequest {}
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StartVpnRequest {
|
pub struct StartVpnRequest {
|
||||||
pub ipv4_addr: Option<String>,
|
pub ipv4_addr: Option<String>,
|
||||||
|
pub ipv4_addrs: Option<Vec<String>>,
|
||||||
pub routes: Option<Vec<String>>,
|
pub routes: Option<Vec<String>>,
|
||||||
pub dns: Option<String>,
|
pub dns: Option<String>,
|
||||||
pub disallowed_applications: Option<Vec<String>>,
|
pub disallowed_applications: Option<Vec<String>>,
|
||||||
@@ -39,6 +40,7 @@ pub struct Status {
|
|||||||
pub struct VpnStatus {
|
pub struct VpnStatus {
|
||||||
pub running: bool,
|
pub running: bool,
|
||||||
pub ipv4_addr: Option<String>,
|
pub ipv4_addr: Option<String>,
|
||||||
|
pub ipv4_addrs: Option<Vec<String>>,
|
||||||
pub routes: Option<Vec<String>>,
|
pub routes: Option<Vec<String>>,
|
||||||
pub dns: Option<String>,
|
pub dns: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user