mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-02 01:03:54 +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 {
|
||||
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()
|
||||
.next()
|
||||
{
|
||||
instance_manager
|
||||
.set_tun_fd(&uuid, fd)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
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) {
|
||||
Ok(()) => {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -918,32 +935,98 @@ mod manager {
|
||||
.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")]
|
||||
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,
|
||||
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||
web_only: bool,
|
||||
shared_dev_name: Option<&str>,
|
||||
) -> Vec<uuid::Uuid> {
|
||||
self.storage
|
||||
.network_configs
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.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())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(super) async fn disable_instances_with_tun(
|
||||
pub(super) async fn disable_incompatible_instances_with_tun(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
web_only: bool,
|
||||
shared_dev_name: Option<&str>,
|
||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||
{
|
||||
let inst_ids: Vec<uuid::Uuid> = if web_only {
|
||||
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 {
|
||||
for inst_id in self.enabled_incompatible_tun_ids(web_only, shared_dev_name) {
|
||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||
.await?;
|
||||
}
|
||||
@@ -951,11 +1034,20 @@ mod manager {
|
||||
}
|
||||
|
||||
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);
|
||||
if !has_tun {
|
||||
app.emit("vpn_service_stop", "")
|
||||
#[cfg(target_os = "android")]
|
||||
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", "")
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -971,19 +1063,31 @@ mod manager {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if !cfg.get_flags().no_tun {
|
||||
let shared_dev_name = Self::runtime_shared_tun_dev_name(cfg);
|
||||
match source {
|
||||
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
||||
self.disable_instances_with_tun(app, false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.disable_incompatible_instances_with_tun(
|
||||
app,
|
||||
false,
|
||||
shared_dev_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
PersistedConfigSource::Web => {
|
||||
self.disable_instances_with_tun(app, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||
self.disable_incompatible_instances_with_tun(
|
||||
app,
|
||||
true,
|
||||
shared_dev_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !self
|
||||
.enabled_incompatible_tun_ids(false, shared_dev_name.as_deref())
|
||||
.is_empty()
|
||||
{
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -52,6 +52,7 @@ declare global {
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const normalizeConfigSource: typeof import('./composables/config_source')['normalizeConfigSource']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
@@ -177,6 +178,7 @@ declare module 'vue' {
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly normalizeConfigSource: UnwrapRef<typeof import('./composables/config_source')['normalizeConfigSource']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
|
||||
@@ -14,6 +14,7 @@ const EVENTS = Object.freeze({
|
||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||
POST_RUN_NETWORK_INSTANCE: 'post_run_network_instance',
|
||||
VPN_SERVICE_STOP: 'vpn_service_stop',
|
||||
VPN_SERVICE_CONFIG_CHANGED: 'vpn_service_config_changed',
|
||||
DHCP_IP_CHANGED: 'dhcp_ip_changed',
|
||||
PROXY_CIDRS_UPDATED: 'proxy_cidrs_updated',
|
||||
EVENT_LAGGED: 'event_lagged',
|
||||
@@ -76,6 +77,14 @@ async function onVpnServiceStop(event: Event<unknown>) {
|
||||
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>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
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.POST_RUN_NETWORK_INSTANCE, onPostRunNetworkInstance),
|
||||
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.PROXY_CIDRS_UPDATED, onProxyCidrsUpdated),
|
||||
await listen(EVENTS.EVENT_LAGGED, onEventLagged),
|
||||
|
||||
@@ -8,17 +8,21 @@ type Route = NetworkTypes.Route
|
||||
interface vpnStatus {
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Addrs: string[]
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
dns: string | null | undefined
|
||||
}
|
||||
|
||||
let dhcpPollingTimer: NodeJS.Timeout | null = null
|
||||
let vpnConfigSyncTask: Promise<void> | null = null
|
||||
let pendingVpnConfigInstanceId: string | null = null
|
||||
const DHCP_POLLING_INTERVAL = 2000 // 2秒后重试
|
||||
|
||||
const curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Addrs: [],
|
||||
ipv4Cidr: undefined,
|
||||
routes: [],
|
||||
dns: undefined,
|
||||
@@ -42,6 +46,7 @@ async function requestVpnPermission() {
|
||||
|
||||
function resetVpnConfigStatus() {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Addrs = []
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
@@ -54,6 +59,12 @@ function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_statu
|
||||
return
|
||||
}
|
||||
|
||||
curVpnStatus.ipv4Addrs = status?.ipv4Addrs?.length
|
||||
? [...status.ipv4Addrs]
|
||||
: status?.ipv4Addr
|
||||
? [status.ipv4Addr]
|
||||
: []
|
||||
|
||||
const ipv4WithCidr = status?.ipv4Addr
|
||||
if (ipv4WithCidr?.length) {
|
||||
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
||||
@@ -96,14 +107,17 @@ async function doStopVpn(force = false) {
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
|
||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?: string) {
|
||||
async function doStartVpn(ipv4Addrs: string[], routes: string[], dns?: string) {
|
||||
if (curVpnStatus.running) {
|
||||
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 = {
|
||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
||||
ipv4Addr: primaryIpv4,
|
||||
ipv4Addrs,
|
||||
routes,
|
||||
dns,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
@@ -127,11 +141,25 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?
|
||||
await waitVpnStatus(true, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.ipv4Cidr = cidr
|
||||
curVpnStatus.ipv4Addrs = [...ipv4Addrs]
|
||||
curVpnStatus.ipv4Cidr = Number(cidr)
|
||||
curVpnStatus.routes = routes
|
||||
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) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
@@ -170,7 +198,7 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
|
||||
|
||||
const ret = []
|
||||
for (const r of routes) {
|
||||
for (let cidr of r.proxy_cidrs) {
|
||||
for (let cidr of r.proxy_cidrs ?? []) {
|
||||
if (!cidr.includes('/')) {
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -190,7 +218,32 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
|
||||
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) {
|
||||
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)
|
||||
|
||||
if (dhcpPollingTimer) {
|
||||
@@ -201,60 +254,95 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
if (!instanceId) {
|
||||
console.warn('vpn service skipped because instance id is empty')
|
||||
if (curVpnStatus.running) {
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
return
|
||||
}
|
||||
await doStopVpn()
|
||||
}
|
||||
return
|
||||
}
|
||||
const config = await getConfig(instanceId)
|
||||
console.log('vpn service loaded config', instanceId, JSON.stringify({
|
||||
no_tun: config.no_tun,
|
||||
dhcp: config.dhcp,
|
||||
enable_magic_dns: config.enable_magic_dns,
|
||||
}))
|
||||
if (config.no_tun) {
|
||||
console.log('vpn service skipped because no_tun is enabled', instanceId)
|
||||
return
|
||||
}
|
||||
const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
console.warn('vpn service skipped because network info is unavailable', instanceId, curNetworkInfo?.error_msg)
|
||||
|
||||
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 virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
||||
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({
|
||||
no_tun: config.no_tun,
|
||||
dhcp: config.dhcp,
|
||||
enable_magic_dns: config.enable_magic_dns,
|
||||
dev_name: config.dev_name,
|
||||
}))
|
||||
|
||||
if (config.dhcp && (!virtual_ip || !virtual_ip.length)) {
|
||||
console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms')
|
||||
dhcpPollingTimer = setTimeout(() => {
|
||||
onNetworkInstanceChange(instanceId)
|
||||
}, DHCP_POLLING_INTERVAL)
|
||||
return
|
||||
const curNetworkInfo = getCollectedNetworkInfo(await collectNetworkInfo(instanceId), instanceId)
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
console.warn('vpn service skipped because network info is unavailable, will retry', instanceId, curNetworkInfo?.error_msg)
|
||||
retryInstanceIds.push(instanceId)
|
||||
continue
|
||||
}
|
||||
|
||||
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
|
||||
if (config.dhcp && (!virtual_ip || !virtual_ip.length)) {
|
||||
console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms')
|
||||
retryInstanceIds.push(instanceId)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
retryInstanceIds.push(instanceId)
|
||||
continue
|
||||
}
|
||||
|
||||
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
||||
if (!network_length) {
|
||||
network_length = 24
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
if (retryInstanceIds.length) {
|
||||
scheduleVpnConfigRetry(retryInstanceIds[0])
|
||||
}
|
||||
|
||||
if (!ipv4Addrs.length) {
|
||||
console.warn('vpn service skipped because no healthy tun instance info is available', instanceId)
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
return
|
||||
}
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
||||
if (!network_length) {
|
||||
network_length = 24
|
||||
}
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes, config)
|
||||
|
||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined
|
||||
|
||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
const cidrChanged = network_length !== curVpnStatus.ipv4Cidr
|
||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
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 configChanged = ipChanged || cidrChanged || routesChanged || dnsChanged
|
||||
const configChanged = ipChanged || routesChanged || dnsChanged
|
||||
const shouldStartVpn = !curVpnStatus.running
|
||||
|
||||
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) {
|
||||
try {
|
||||
await doStopVpn()
|
||||
@@ -262,10 +350,14 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
console.info('vpn service skipped stale config start because a newer change is queued')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(virtual_ip, network_length, routes, dns)
|
||||
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns)
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error && e.message === 'need_prepare') {
|
||||
@@ -304,6 +396,33 @@ async function findRunningTunInstanceId() {
|
||||
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() {
|
||||
await registerVpnServiceListener()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user