import type { NetworkTypes } from 'easytier-frontend-lib' import { addPluginListener } from '@tauri-apps/api/core' import { Utils } from 'easytier-frontend-lib' import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api' type Route = NetworkTypes.Route interface vpnStatus { running: boolean ipv4Addr: string | null | undefined ipv4Cidr: number | null | undefined routes: string[] dns: string | null | undefined } let dhcpPollingTimer: NodeJS.Timeout | null = null const DHCP_POLLING_INTERVAL = 2000 // 2秒后重试 const curVpnStatus: vpnStatus = { running: false, ipv4Addr: undefined, ipv4Cidr: undefined, routes: [], dns: undefined, } async function waitVpnStatus(target_status: boolean, timeout_sec: number) { const start_time = Date.now() while (curVpnStatus.running !== target_status) { if (Date.now() - start_time > timeout_sec * 1000) { throw new Error('wait vpn status timeout') } await new Promise(r => setTimeout(r, 50)) } } async function doStopVpn() { if (!curVpnStatus.running) { return } console.log('stop vpn') const stop_ret = await stop_vpn() console.log('stop vpn', JSON.stringify((stop_ret))) await waitVpnStatus(false, 3) curVpnStatus.ipv4Addr = undefined curVpnStatus.routes = [] curVpnStatus.dns = undefined } async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?: string) { if (curVpnStatus.running) { return } console.log('start vpn service', ipv4Addr, cidr, routes, dns) const start_ret = await start_vpn({ ipv4Addr: `${ipv4Addr}/${cidr}`, routes, dns, disallowedApplications: ['com.kkrainbow.easytier'], mtu: 1300, }) if (start_ret?.errorMsg?.length) { throw new Error(start_ret.errorMsg) } await waitVpnStatus(true, 3) curVpnStatus.ipv4Addr = ipv4Addr curVpnStatus.routes = routes curVpnStatus.dns = dns } async function onVpnServiceStart(payload: any) { console.log('vpn service start', JSON.stringify(payload)) curVpnStatus.running = true if (payload.fd) { setTunFd(payload.fd) } } async function onVpnServiceStop(payload: any) { console.log('vpn service stop', JSON.stringify(payload)) curVpnStatus.running = false } async function registerVpnServiceListener() { console.log('register vpn service listener') await addPluginListener( 'vpnservice', 'vpn_service_start', onVpnServiceStart, ) await addPluginListener( 'vpnservice', 'vpn_service_stop', onVpnServiceStop, ) } function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfig): string[] { if (!routes) { return [] } const ret = [] for (const r of routes) { for (let cidr of r.proxy_cidrs) { if (!cidr.includes('/')) { cidr += '/32' } ret.push(cidr) } } node_config.routes.forEach(r => { ret.push(r) }) if (node_config.enable_magic_dns) { ret.push('100.100.100.101/32') } // sort and dedup return Array.from(new Set(ret)).sort() } export async function onNetworkInstanceChange(instanceId: string) { console.error('vpn service network instance change id', instanceId) if (dhcpPollingTimer) { clearTimeout(dhcpPollingTimer) dhcpPollingTimer = null } if (!instanceId) { await doStopVpn() return } const config = await getConfig(instanceId) if (config.no_tun) { return } const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId] if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) { await doStopVpn() return } 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') dhcpPollingTimer = setTimeout(() => { onNetworkInstanceChange(instanceId) }, DHCP_POLLING_INTERVAL) return } if (!virtual_ip || !virtual_ip.length) { 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 routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes) const dnsChanged = dns != curVpnStatus.dns if (ipChanged || routesChanged || dnsChanged) { console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip) try { await doStopVpn() } catch (e) { console.error(e) } try { await doStartVpn(virtual_ip, network_length, routes, dns) } catch (e) { console.error('start vpn service failed, stop all other network insts.', e) await runNetworkInstance(config, true); //on android config should always be saved } } } async function isNoTunEnabled(instanceId: string | undefined) { if (!instanceId) { return false } return (await getConfig(instanceId)).no_tun ?? false } export async function initMobileVpnService() { await registerVpnServiceListener() } export async function prepareVpnService(instanceId: string) { if (await isNoTunEnabled(instanceId)) { return } console.log('prepare vpn') const prepare_ret = await prepare_vpn() console.log('prepare vpn', JSON.stringify((prepare_ret))) if (prepare_ret?.errorMsg?.length) { throw new Error(prepare_ret.errorMsg) } }