mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-08 10:56:13 -08:00
Configure one mobile VPN from running TUN instances
Gather addresses and routes from running instances in the GUI for VpnService. Attach its single fd to the core shared TUN and refresh the VPN when instance configuration changes. Keep the FFI set_tun_fd call scoped to one instance and report attachment errors. Cover mobile configuration and reconciliation in GUI tests.
This commit is contained in:
1 parent
cc2d58bb00
commit
de77efac60
11 files changed
+343
-293
No files matched your search
@@ -14,6 +14,7 @@ ffi-dataplane = [
|
||||
"easytier/ffi-dataplane",
|
||||
"easytier-core/proxy-smoltcp-stack",
|
||||
]
|
||||
macos-ne = ["easytier/macos-ne"]
|
||||
|
||||
[dependencies]
|
||||
easytier = { workspace = true, default-features = true, features = ["tracing-log"] }
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
let inst_id = match resolve_instance_id_by_name(&inst_name) {
|
||||
Ok(Some(instance_id)) => instance_id,
|
||||
Ok(None) => {
|
||||
set_error_msg("instance not found");
|
||||
set_error_msg(&format!("instance not found: {inst_name}"));
|
||||
return -1;
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -32,7 +32,10 @@ pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
|
||||
match ffi_context().manager.attach_tun_fd(inst_id, fd) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to set tun fd: {}", e));
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
mod elevate;
|
||||
|
||||
use anyhow::Context;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
use easytier::instance::factory::attach_mobile_tun_fd;
|
||||
#[cfg(target_os = "android")]
|
||||
use easytier::instance::factory::subscribe_native_instance_event;
|
||||
use easytier::proto::api::config::{
|
||||
@@ -248,15 +250,24 @@ 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!()?
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
return attach_mobile_tun_fd(instance_manager.as_ref(), fd)
|
||||
.await
|
||||
.map_err(|error| error.to_string());
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
if let Some(instance_id) = get_client_manager!()?
|
||||
.get_enabled_instances_with_tun_ids()
|
||||
.next()
|
||||
{
|
||||
instance_manager
|
||||
.attach_tun_fd(uuid, fd)
|
||||
.map_err(|e| e.to_string())?;
|
||||
.attach_tun_fd(instance_id, fd)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -786,11 +797,6 @@ mod manager {
|
||||
Self::Web => ConfigSource::Web,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, target_os = "android"))]
|
||||
fn is_web_like(self) -> bool {
|
||||
matches!(self, Self::Web)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1009,44 +1015,21 @@ mod manager {
|
||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn get_enabled_instances_with_web_like_tun_ids(
|
||||
&self,
|
||||
) -> impl Iterator<Item = 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_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(super) async fn disable_instances_with_tun(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
web_only: bool,
|
||||
) -> Result<(), easytier_core::management::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 {
|
||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 {
|
||||
#[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(())
|
||||
}
|
||||
|
||||
@@ -1060,28 +1043,6 @@ mod manager {
|
||||
app.emit("pre_run_network_instance", instance_id.to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if !cfg.get_flags().no_tun {
|
||||
match source {
|
||||
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
||||
self.disable_instances_with_tun(app, false)
|
||||
.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() {
|
||||
return Err(
|
||||
"Android only supports one active TUN network; user-managed VPN remains active"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.storage
|
||||
.save_config(
|
||||
app,
|
||||
@@ -1110,7 +1071,8 @@ mod manager {
|
||||
let instance_id_str = instance_id_clone.to_string();
|
||||
loop {
|
||||
match event_receiver.recv().await {
|
||||
Ok(easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Changed(_, _)) => {
|
||||
Ok(easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Changed(_, _)
|
||||
| easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Conflicted(_)) => {
|
||||
let _ = app_clone.emit("dhcp_ip_changed", &instance_id_str);
|
||||
}
|
||||
Ok(easytier::common::global_ctx::GlobalCtxEvent::ProxyCidrsUpdated(_, _)) => {
|
||||
@@ -1193,10 +1155,11 @@ mod manager {
|
||||
) -> anyhow::Result<()> {
|
||||
self.storage.network_configs.clear();
|
||||
for stored in configs {
|
||||
let instance_id = stored.config.instance_id();
|
||||
let config = stored.config;
|
||||
let instance_id = config.instance_id();
|
||||
self.storage.network_configs.insert(
|
||||
instance_id.parse()?,
|
||||
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
||||
GUIConfig::new(instance_id.to_string(), config, stored.source),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1309,13 +1272,6 @@ mod manager {
|
||||
PersistedConfigSource::Web
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_web_configs_are_web_like() {
|
||||
assert!(!PersistedConfigSource::Legacy.is_web_like());
|
||||
assert!(!PersistedConfigSource::User.is_web_like());
|
||||
assert!(PersistedConfigSource::Web.is_web_like());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export async function sendConfigs(enabledNetworks: string[]) {
|
||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||
source,
|
||||
})),
|
||||
enabledNetworks
|
||||
enabledNetworks,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,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',
|
||||
@@ -77,6 +78,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}`);
|
||||
@@ -105,6 +114,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),
|
||||
|
||||
@@ -58,13 +58,15 @@ vi.mock('./backend', () => ({
|
||||
setTunFd: mocks.setTunFd,
|
||||
}))
|
||||
|
||||
function setConfig(instanceId: string, noTun = false) {
|
||||
function setConfig(instanceId: string, noTun = false, devName?: string) {
|
||||
mocks.configs.set(instanceId, {
|
||||
no_tun: noTun,
|
||||
dev_name: devName,
|
||||
dhcp: false,
|
||||
enable_magic_dns: false,
|
||||
routes: [],
|
||||
})
|
||||
mocks.listNetworkInstanceIds.mockResolvedValue({ running_inst_ids: [...mocks.configs.keys()] })
|
||||
}
|
||||
|
||||
function setReady(instanceId: string, ipv4: string) {
|
||||
@@ -106,30 +108,110 @@ beforeEach(() => {
|
||||
mocks.stopVpn.mockClear()
|
||||
})
|
||||
|
||||
describe('mobile VPN reconciliation ownership', () => {
|
||||
it('stops A before retrying an unavailable B, then starts B when it becomes ready', async () => {
|
||||
setConfig('A')
|
||||
setConfig('B')
|
||||
describe('mobile VPN reconciliation', () => {
|
||||
it('keeps attached shared members during a temporary status gap', async () => {
|
||||
setConfig('A', false, 'shared0')
|
||||
setConfig('B', false, 'shared0')
|
||||
setReady('A', '10.0.0.1')
|
||||
setReady('B', '10.0.1.1')
|
||||
const vpn = await loadVpnModule()
|
||||
await vpn.onNetworkInstanceChange('A')
|
||||
mocks.startVpn.mockClear()
|
||||
|
||||
mocks.networkInfo.delete('B')
|
||||
await vpn.onNetworkInstanceUpdate('B')
|
||||
expect(mocks.stopVpn).not.toHaveBeenCalled()
|
||||
expect(mocks.startVpn).not.toHaveBeenCalled()
|
||||
|
||||
setReady('B', '10.0.1.2')
|
||||
await vpn.onNetworkInstanceUpdate('B')
|
||||
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.startVpn).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
ipv4Addrs: ['10.0.0.1/24', '10.0.1.2/24'],
|
||||
}))
|
||||
})
|
||||
|
||||
it('keeps a ready member active while a new shared member awaits an IP', async () => {
|
||||
setConfig('A', false, 'shared0')
|
||||
setReady('A', '10.0.0.1')
|
||||
const vpn = await loadVpnModule()
|
||||
await vpn.onNetworkInstanceChange('A')
|
||||
|
||||
setConfig('B', false, 'shared0')
|
||||
await vpn.onNetworkInstanceChange('B')
|
||||
expect(mocks.stopVpn).not.toHaveBeenCalled()
|
||||
expect(mocks.startVpn).toHaveBeenCalledTimes(1)
|
||||
|
||||
setReady('B', '10.0.1.1')
|
||||
await vpn.onNetworkInstanceUpdate('B')
|
||||
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.startVpn).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
ipv4Addrs: ['10.0.0.1/24', '10.0.1.1/24'],
|
||||
}))
|
||||
})
|
||||
|
||||
it('uses one VPN for different dev_name values and keeps the remaining member', async () => {
|
||||
setConfig('A', false, 'shared0')
|
||||
setConfig('B', false, 'other0')
|
||||
setConfig('C', true)
|
||||
setReady('A', '10.0.0.1')
|
||||
setReady('B', '10.0.1.1')
|
||||
const vpn = await loadVpnModule()
|
||||
|
||||
await vpn.onNetworkInstanceChange('A')
|
||||
expect(mocks.startVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.startVpn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
ipv4Addrs: ['10.0.0.1/24', '10.0.1.1/24'],
|
||||
}))
|
||||
expect(mocks.setTunFd).toHaveBeenCalledWith(1)
|
||||
|
||||
mocks.startVpn.mockClear()
|
||||
await vpn.onNetworkInstanceChange('B')
|
||||
mocks.stopVpn.mockClear()
|
||||
mocks.listNetworkInstanceIds.mockResolvedValue({ running_inst_ids: ['C', 'B'] })
|
||||
await vpn.onNetworkInstanceChange('A')
|
||||
|
||||
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.startVpn).not.toHaveBeenCalled()
|
||||
|
||||
setReady('B', '10.0.0.2')
|
||||
await vpn.onNetworkInstanceUpdate('B')
|
||||
|
||||
expect(mocks.startVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.startVpn).toHaveBeenCalledWith(expect.objectContaining({ ipv4Addr: '10.0.0.2/24' }))
|
||||
expect(mocks.startVpn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
ipv4Addrs: ['10.0.1.1/24'],
|
||||
}))
|
||||
expect(mocks.setTunFd).toHaveBeenLastCalledWith(1)
|
||||
})
|
||||
|
||||
it('stops the previous owner during pre-run even if the new instance never reaches post-run', async () => {
|
||||
it('removes a shared member when its DHCP address is withdrawn', async () => {
|
||||
setConfig('A', false, 'shared0')
|
||||
setConfig('B', false, 'shared0')
|
||||
mocks.configs.get('B')!.dhcp = true
|
||||
setReady('A', '10.0.0.1')
|
||||
setReady('B', '10.0.1.1')
|
||||
const vpn = await loadVpnModule()
|
||||
await vpn.onNetworkInstanceChange('A')
|
||||
|
||||
mocks.networkInfo.set('B', { my_node_info: {}, routes: [] })
|
||||
await vpn.onNetworkInstanceUpdate('B')
|
||||
|
||||
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.startVpn).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
ipv4Addrs: ['10.0.0.1/24'],
|
||||
}))
|
||||
expect(mocks.setTunFd).toHaveBeenLastCalledWith(1)
|
||||
})
|
||||
|
||||
it('uses manual routes instead of peer proxy routes', async () => {
|
||||
setConfig('A')
|
||||
mocks.configs.get('A')!.enable_manual_routes = true
|
||||
mocks.configs.get('A')!.routes = ['192.0.2.0/24']
|
||||
setReady('A', '10.0.0.1')
|
||||
const info = mocks.networkInfo.get('A') as { routes: unknown[] }
|
||||
info.routes = [{ proxy_cidrs: ['10.9.0.0/16'] }]
|
||||
const vpn = await loadVpnModule()
|
||||
|
||||
await vpn.onNetworkInstanceChange('A')
|
||||
|
||||
expect(mocks.startVpn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
routes: ['10.0.0.0/24', '192.0.2.0/24'],
|
||||
}))
|
||||
})
|
||||
|
||||
it('keeps the VPN during pre-run of another instance', async () => {
|
||||
setConfig('A')
|
||||
setConfig('B')
|
||||
setReady('A', '10.0.0.1')
|
||||
@@ -140,7 +222,7 @@ describe('mobile VPN reconciliation ownership', () => {
|
||||
|
||||
await vpn.prepareVpnService('B')
|
||||
|
||||
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.stopVpn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves the VPN while retrying the same instance', async () => {
|
||||
@@ -157,22 +239,25 @@ describe('mobile VPN reconciliation ownership', () => {
|
||||
expect(mocks.stopVpn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores an update from an instance that no longer owns the VPN', async () => {
|
||||
it('reconciles all running members after any member update', async () => {
|
||||
setConfig('A')
|
||||
setConfig('B')
|
||||
setConfig('B', false, 'other0')
|
||||
setReady('A', '10.0.0.1')
|
||||
setReady('B', '10.0.1.1')
|
||||
const vpn = await loadVpnModule()
|
||||
|
||||
await vpn.onNetworkInstanceChange('A')
|
||||
await vpn.onNetworkInstanceChange('B')
|
||||
mocks.collectNetworkInfo.mockClear()
|
||||
mocks.startVpn.mockClear()
|
||||
setReady('B', '10.0.1.2')
|
||||
|
||||
await vpn.onNetworkInstanceUpdate('A')
|
||||
|
||||
expect(mocks.collectNetworkInfo).not.toHaveBeenCalled()
|
||||
expect(mocks.startVpn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
ipv4Addrs: ['10.0.0.1/24', '10.0.1.2/24'],
|
||||
}))
|
||||
})
|
||||
|
||||
it('does not apply an in-flight result after the desired instance changes', async () => {
|
||||
it('does not apply a stale in-flight network result', async () => {
|
||||
setConfig('A')
|
||||
setConfig('B')
|
||||
setReady('A', '10.0.0.1')
|
||||
@@ -195,7 +280,7 @@ describe('mobile VPN reconciliation ownership', () => {
|
||||
|
||||
const staleUpdate = vpn.onNetworkInstanceUpdate('A')
|
||||
await collectStarted
|
||||
const switchToB = vpn.onNetworkInstanceChange('B')
|
||||
const newerUpdate = vpn.onNetworkInstanceChange('B')
|
||||
resolveNetworkInfo({
|
||||
info: {
|
||||
map: {
|
||||
@@ -212,13 +297,13 @@ describe('mobile VPN reconciliation ownership', () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Promise.all([staleUpdate, switchToB])
|
||||
await Promise.all([staleUpdate, newerUpdate])
|
||||
|
||||
expect(mocks.startVpn).not.toHaveBeenCalled()
|
||||
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.stopVpn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stops a native VPN with unknown ownership before retrying the selected instance', async () => {
|
||||
it('preserves a native VPN while network info is unavailable', async () => {
|
||||
setConfig('A')
|
||||
mocks.getVpnStatus.mockResolvedValue({
|
||||
running: true,
|
||||
@@ -230,7 +315,7 @@ describe('mobile VPN reconciliation ownership', () => {
|
||||
|
||||
await vpn.syncMobileVpnService()
|
||||
|
||||
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.stopVpn).not.toHaveBeenCalled()
|
||||
expect(mocks.startVpn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { addPluginListener } from '@tauri-apps/api/core'
|
||||
import { Utils } from 'easytier-frontend-lib'
|
||||
import { IPv4CidrRange } from 'ip-num/IPRange'
|
||||
import {
|
||||
consume_vpn_tile_action,
|
||||
get_vpn_status,
|
||||
@@ -15,8 +16,7 @@ type Route = NetworkTypes.Route
|
||||
|
||||
interface vpnStatus {
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Cidr: number | null | undefined
|
||||
ipv4Addrs: string[]
|
||||
routes: string[]
|
||||
dns: string | null | undefined
|
||||
}
|
||||
@@ -25,8 +25,6 @@ let vpnReconcileTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const VPN_RECONCILE_INTERVAL_MS = 2000
|
||||
const VPN_RECONCILE_MAX_ATTEMPTS = 60
|
||||
|
||||
let desiredVpnInstanceId: string | undefined
|
||||
let activeVpnInstanceId: string | undefined
|
||||
let vpnReconcileGeneration = 0
|
||||
let vpnReconcileAttempts = 0
|
||||
let vpnReconcileQueue: Promise<void> = Promise.resolve()
|
||||
@@ -36,8 +34,7 @@ let vpnTileActionQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
const curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Cidr: undefined,
|
||||
ipv4Addrs: [],
|
||||
routes: [],
|
||||
dns: undefined,
|
||||
}
|
||||
@@ -108,26 +105,24 @@ function clearVpnReconcileTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
function beginVpnReconcile(instanceId?: string) {
|
||||
function beginVpnReconcile() {
|
||||
clearVpnReconcileTimer()
|
||||
desiredVpnInstanceId = instanceId
|
||||
vpnReconcileAttempts = 0
|
||||
vpnReconcileGeneration += 1
|
||||
return vpnReconcileGeneration
|
||||
}
|
||||
|
||||
function isCurrentVpnReconcile(instanceId: string, generation: number) {
|
||||
return desiredVpnInstanceId === (instanceId || undefined) && vpnReconcileGeneration === generation
|
||||
function isCurrentVpnReconcile(generation: number) {
|
||||
return vpnReconcileGeneration === generation
|
||||
}
|
||||
|
||||
function scheduleVpnReconcile(instanceId: string, generation: number, reason: string) {
|
||||
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||
function scheduleVpnReconcile(generation: number, reason: string) {
|
||||
if (!isCurrentVpnReconcile(generation))
|
||||
return
|
||||
|
||||
if (vpnReconcileAttempts >= VPN_RECONCILE_MAX_ATTEMPTS) {
|
||||
console.error(
|
||||
'vpn service reconcile stopped after maximum attempts',
|
||||
instanceId,
|
||||
VPN_RECONCILE_MAX_ATTEMPTS,
|
||||
reason,
|
||||
)
|
||||
@@ -139,7 +134,6 @@ function scheduleVpnReconcile(instanceId: string, generation: number, reason: st
|
||||
console.log(
|
||||
'vpn service is not ready, retrying',
|
||||
JSON.stringify({
|
||||
instanceId,
|
||||
attempt: vpnReconcileAttempts,
|
||||
maxAttempts: VPN_RECONCILE_MAX_ATTEMPTS,
|
||||
delayMs: VPN_RECONCILE_INTERVAL_MS,
|
||||
@@ -148,13 +142,12 @@ function scheduleVpnReconcile(instanceId: string, generation: number, reason: st
|
||||
)
|
||||
vpnReconcileTimer = setTimeout(() => {
|
||||
vpnReconcileTimer = null
|
||||
void enqueueVpnReconcile(instanceId, generation)
|
||||
void enqueueVpnReconcile(generation)
|
||||
}, VPN_RECONCILE_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function resetVpnConfigStatus() {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
curVpnStatus.ipv4Addrs = []
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
}
|
||||
@@ -162,24 +155,11 @@ function resetVpnConfigStatus() {
|
||||
function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) {
|
||||
curVpnStatus.running = status?.running ?? false
|
||||
if (!curVpnStatus.running) {
|
||||
activeVpnInstanceId = undefined
|
||||
resetVpnConfigStatus()
|
||||
return
|
||||
}
|
||||
|
||||
const ipv4WithCidr = status?.ipv4Addr
|
||||
if (ipv4WithCidr?.length) {
|
||||
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
|
||||
const parsedCidr = Number(cidr)
|
||||
curVpnStatus.ipv4Cidr = Number.isInteger(parsedCidr) ? parsedCidr : undefined
|
||||
}
|
||||
else {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
}
|
||||
|
||||
curVpnStatus.ipv4Addrs = [...(status?.ipv4Addrs ?? [])]
|
||||
curVpnStatus.routes = [...(status?.routes ?? [])]
|
||||
curVpnStatus.dns = status?.dns ?? undefined
|
||||
}
|
||||
@@ -194,12 +174,21 @@ async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function detachTunFd() {
|
||||
try {
|
||||
await setTunFd(0)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('detach tun fd failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function doStopVpn(force = false) {
|
||||
const wasRunning = curVpnStatus.running
|
||||
if (!force && !wasRunning) {
|
||||
activeVpnInstanceId = undefined
|
||||
return
|
||||
}
|
||||
await detachTunFd()
|
||||
console.log('stop vpn')
|
||||
const stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
@@ -207,18 +196,21 @@ async function doStopVpn(force = false) {
|
||||
await waitVpnStatus(false, 3)
|
||||
}
|
||||
|
||||
activeVpnInstanceId = undefined
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
|
||||
async function doStartVpn(instanceId: string, ipv4Addr: string, cidr: number, routes: string[], dns?: string) {
|
||||
async function doStartVpn(
|
||||
ipv4Addrs: string[],
|
||||
routes: string[],
|
||||
dns: string | undefined,
|
||||
) {
|
||||
if (curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('start vpn service', ipv4Addr, cidr, routes, dns)
|
||||
console.log('start vpn service', ipv4Addrs, routes, dns)
|
||||
const request = {
|
||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
||||
ipv4Addrs,
|
||||
routes,
|
||||
dns,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
@@ -241,27 +233,26 @@ async function doStartVpn(instanceId: string, ipv4Addr: string, cidr: number, ro
|
||||
}
|
||||
await waitVpnStatus(true, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.ipv4Cidr = cidr
|
||||
curVpnStatus.ipv4Addrs = [...ipv4Addrs]
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.dns = dns
|
||||
activeVpnInstanceId = instanceId
|
||||
}
|
||||
|
||||
async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
await setTunFd(payload.fd).catch((e) => {
|
||||
await setTunFd(payload.fd).catch(async (e) => {
|
||||
console.error('set tun fd failed', e)
|
||||
await doStopVpn(true).catch(stopError => console.error('stop vpn after tun attach failure', stopError))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(payload: any) {
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
await detachTunFd()
|
||||
curVpnStatus.running = false
|
||||
activeVpnInstanceId = undefined
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
|
||||
@@ -292,6 +283,10 @@ async function registerVpnServiceListener() {
|
||||
|
||||
function getRoutesForVpn(routes: Route[] | undefined, node_config: NetworkTypes.NetworkConfig): string[] {
|
||||
const ret = []
|
||||
if (node_config.enable_manual_routes) {
|
||||
ret.push(...(node_config.routes ?? []))
|
||||
}
|
||||
else {
|
||||
for (const r of routes ?? []) {
|
||||
for (let cidr of r.proxy_cidrs ?? []) {
|
||||
if (!cidr.includes('/')) {
|
||||
@@ -300,149 +295,157 @@ function getRoutesForVpn(routes: Route[] | undefined, node_config: NetworkTypes.
|
||||
ret.push(cidr)
|
||||
}
|
||||
}
|
||||
|
||||
for (const route of node_config.routes ?? []) {
|
||||
ret.push(route)
|
||||
}
|
||||
|
||||
if (node_config.enable_magic_dns) {
|
||||
ret.push('100.100.100.101/32')
|
||||
}
|
||||
|
||||
// sort and dedup
|
||||
return Array.from(new Set(ret)).sort()
|
||||
return ret
|
||||
}
|
||||
|
||||
async function stopVpnOwnedByOtherInstance(instanceId: string, generation: number) {
|
||||
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||
return false
|
||||
|
||||
if (curVpnStatus.running && activeVpnInstanceId !== instanceId) {
|
||||
console.warn('vpn service owner changed', activeVpnInstanceId, instanceId)
|
||||
await doStopVpn()
|
||||
function ipv4CidrToRoute(cidr: string): string | undefined {
|
||||
try {
|
||||
const range = IPv4CidrRange.fromCidr(cidr)
|
||||
return `${range.getFirst()}/${range.getPrefix()}`
|
||||
}
|
||||
catch {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return isCurrentVpnReconcile(instanceId, generation)
|
||||
}
|
||||
|
||||
async function reconcileNetworkInstance(instanceId: string, generation: number) {
|
||||
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||
async function reconcileNetworkInstance(generation: number) {
|
||||
if (!isCurrentVpnReconcile(generation))
|
||||
return
|
||||
|
||||
clearVpnReconcileTimer()
|
||||
|
||||
if (!instanceId) {
|
||||
console.warn('vpn service skipped because instance id is empty')
|
||||
if (curVpnStatus.running) {
|
||||
let instances: Awaited<ReturnType<typeof findRunningTunInstances>>
|
||||
try {
|
||||
instances = await findRunningTunInstances()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('vpn service instance query failed', error)
|
||||
scheduleVpnReconcile(generation, 'instance_list_unavailable')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isCurrentVpnReconcile(generation))
|
||||
return
|
||||
|
||||
if (!instances.length) {
|
||||
if (curVpnStatus.running)
|
||||
await doStopVpn()
|
||||
}
|
||||
return
|
||||
}
|
||||
const config = await getConfig(instanceId)
|
||||
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||
return
|
||||
|
||||
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)
|
||||
if (activeVpnInstanceId === instanceId) {
|
||||
await doStopVpn()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!await stopVpnOwnedByOtherInstance(instanceId, generation))
|
||||
return
|
||||
const ipv4Addrs: string[] = []
|
||||
const routes = new Set<string>()
|
||||
let dns: string | undefined
|
||||
let retryReason: string | undefined
|
||||
let networkInfoUnavailable = false
|
||||
|
||||
for (const { instanceId: memberId, config } of instances) {
|
||||
let curNetworkInfo
|
||||
try {
|
||||
curNetworkInfo = (await collectNetworkInfo(instanceId))?.info?.map?.[instanceId]
|
||||
curNetworkInfo = (await collectNetworkInfo(memberId)).info.map[memberId]
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('vpn service network info query failed', instanceId, e)
|
||||
scheduleVpnReconcile(instanceId, generation, 'network_info_query_failed')
|
||||
return
|
||||
catch (error) {
|
||||
console.warn('vpn service network info query failed', memberId, error)
|
||||
retryReason ??= 'network_info_query_failed'
|
||||
networkInfoUnavailable = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||
if (!isCurrentVpnReconcile(generation))
|
||||
return
|
||||
|
||||
if (!curNetworkInfo) {
|
||||
scheduleVpnReconcile(instanceId, generation, 'network_info_unavailable')
|
||||
return
|
||||
console.warn('vpn service network info unavailable', memberId)
|
||||
retryReason ??= 'network_info_unavailable'
|
||||
networkInfoUnavailable = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (curNetworkInfo.error_msg?.length) {
|
||||
console.warn('vpn service skipped because network instance failed', instanceId, curNetworkInfo.error_msg)
|
||||
vpnReconcileAttempts = 0
|
||||
console.warn('vpn service network failed', memberId, curNetworkInfo.error_msg)
|
||||
retryReason ??= 'network_failed'
|
||||
continue
|
||||
}
|
||||
|
||||
const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4
|
||||
const virtualIp = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined
|
||||
if (!virtualIp) {
|
||||
retryReason ??= config.dhcp ? 'dhcp_ipv4_unavailable' : 'static_ipv4_unavailable'
|
||||
if (!config.dhcp)
|
||||
networkInfoUnavailable = true
|
||||
continue
|
||||
}
|
||||
|
||||
const networkLength = virtualIpv4?.network_length || 24
|
||||
const sourceIpv4 = virtualIp + '/' + networkLength
|
||||
ipv4Addrs.push(sourceIpv4)
|
||||
const localRoute = ipv4CidrToRoute(sourceIpv4)
|
||||
if (localRoute)
|
||||
routes.add(localRoute)
|
||||
getRoutesForVpn(curNetworkInfo.routes, config).forEach((route) => {
|
||||
routes.add(route)
|
||||
})
|
||||
if (config.enable_magic_dns)
|
||||
dns = '100.100.100.101'
|
||||
}
|
||||
|
||||
if (!isCurrentVpnReconcile(generation))
|
||||
return
|
||||
|
||||
if (networkInfoUnavailable && curVpnStatus.running) {
|
||||
scheduleVpnReconcile(generation, retryReason || 'network_info_unavailable')
|
||||
return
|
||||
}
|
||||
|
||||
if (!ipv4Addrs.length) {
|
||||
if (retryReason)
|
||||
scheduleVpnReconcile(generation, retryReason)
|
||||
if (curVpnStatus.running)
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4
|
||||
const virtual_ip = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined
|
||||
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
scheduleVpnReconcile(
|
||||
instanceId,
|
||||
generation,
|
||||
config.dhcp ? 'dhcp_ipv4_unavailable' : 'static_ipv4_unavailable',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (retryReason)
|
||||
scheduleVpnReconcile(generation, retryReason)
|
||||
else
|
||||
vpnReconcileAttempts = 0
|
||||
const sortedIpv4Addrs = [...ipv4Addrs].sort()
|
||||
const sortedRoutes = [...routes].sort()
|
||||
const configChanged
|
||||
= JSON.stringify(sortedIpv4Addrs) !== JSON.stringify(curVpnStatus.ipv4Addrs)
|
||||
|| JSON.stringify(sortedRoutes) !== JSON.stringify(curVpnStatus.routes)
|
||||
|| dns !== curVpnStatus.dns
|
||||
|
||||
let network_length = virtualIpv4?.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 dnsChanged = dns != curVpnStatus.dns
|
||||
const configChanged = ipChanged || cidrChanged || routesChanged || dnsChanged
|
||||
const shouldStartVpn = !curVpnStatus.running
|
||||
|
||||
if (shouldStartVpn || configChanged) {
|
||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
if (!curVpnStatus.running || configChanged) {
|
||||
if (curVpnStatus.running) {
|
||||
try {
|
||||
await doStopVpn()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
catch (error) {
|
||||
console.error('stop vpn service failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCurrentVpnReconcile(generation))
|
||||
return
|
||||
|
||||
try {
|
||||
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||
return
|
||||
|
||||
await doStartVpn(instanceId, virtual_ip, network_length, routes, dns)
|
||||
if (!isCurrentVpnReconcile(instanceId, generation) && activeVpnInstanceId === instanceId) {
|
||||
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns)
|
||||
if (!isCurrentVpnReconcile(generation))
|
||||
await doStopVpn()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error && e.message === 'need_prepare') {
|
||||
console.info('vpn permission is required before starting the Android VPN service')
|
||||
return
|
||||
}
|
||||
if (e instanceof Error && e.message === 'vpn_permission_denied') {
|
||||
catch (error) {
|
||||
if (error instanceof Error && error.message === 'vpn_permission_denied') {
|
||||
console.info('vpn permission request was denied or dismissed')
|
||||
return
|
||||
}
|
||||
console.error('start vpn service failed', e)
|
||||
console.error('start vpn service failed', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,42 +462,18 @@ function enqueueVpnTask(task: () => Promise<void>) {
|
||||
return run
|
||||
}
|
||||
|
||||
function enqueueVpnReconcile(instanceId: string, generation: number) {
|
||||
return enqueueVpnTask(() => reconcileNetworkInstance(instanceId, generation))
|
||||
function enqueueVpnReconcile(generation: number) {
|
||||
return enqueueVpnTask(() => reconcileNetworkInstance(generation))
|
||||
}
|
||||
|
||||
export async function onNetworkInstanceChange(instanceId: string) {
|
||||
const generation = beginVpnReconcile(instanceId || undefined)
|
||||
|
||||
if (instanceId && await isNoTunEnabled(instanceId)) {
|
||||
if (vpnReconcileGeneration !== generation)
|
||||
return
|
||||
|
||||
if (activeVpnInstanceId === instanceId) {
|
||||
desiredVpnInstanceId = undefined
|
||||
await enqueueVpnReconcile('', generation)
|
||||
return
|
||||
}
|
||||
|
||||
desiredVpnInstanceId = activeVpnInstanceId
|
||||
if (activeVpnInstanceId) {
|
||||
await enqueueVpnReconcile(activeVpnInstanceId, generation)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (vpnReconcileGeneration !== generation)
|
||||
return
|
||||
|
||||
await enqueueVpnReconcile(instanceId, generation)
|
||||
export async function onNetworkInstanceChange(_instanceId: string) {
|
||||
await enqueueVpnReconcile(beginVpnReconcile())
|
||||
}
|
||||
|
||||
export async function onNetworkInstanceUpdate(instanceId: string) {
|
||||
if (!instanceId || instanceId !== desiredVpnInstanceId)
|
||||
if (!instanceId)
|
||||
return
|
||||
|
||||
const generation = beginVpnReconcile(instanceId)
|
||||
await enqueueVpnReconcile(instanceId, generation)
|
||||
await onNetworkInstanceChange(instanceId)
|
||||
}
|
||||
|
||||
async function isNoTunEnabled(instanceId: string | undefined) {
|
||||
@@ -504,20 +483,19 @@ async function isNoTunEnabled(instanceId: string | undefined) {
|
||||
return (await getConfig(instanceId)).no_tun ?? false
|
||||
}
|
||||
|
||||
async function findRunningTunInstanceId() {
|
||||
async function findRunningTunInstances() {
|
||||
const instanceIds = await listNetworkInstanceIds()
|
||||
const runningIds = (instanceIds.running_inst_ids ?? []).map(Utils.UuidToStr)
|
||||
console.log('vpn service sync running instances', JSON.stringify(runningIds))
|
||||
const runningTunInstances = []
|
||||
|
||||
for (const instanceId of runningIds) {
|
||||
if (await isNoTunEnabled(instanceId)) {
|
||||
const config = await getConfig(instanceId)
|
||||
if (config.no_tun)
|
||||
continue
|
||||
runningTunInstances.push({ instanceId, config })
|
||||
}
|
||||
|
||||
return instanceId
|
||||
}
|
||||
|
||||
return undefined
|
||||
return runningTunInstances
|
||||
}
|
||||
|
||||
export async function initMobileVpnService() {
|
||||
@@ -529,21 +507,10 @@ export async function prepareVpnService(instanceId: string) {
|
||||
return
|
||||
}
|
||||
|
||||
const generation = beginVpnReconcile(instanceId)
|
||||
const stopPreviousOwner = enqueueVpnTask(async () => {
|
||||
await stopVpnOwnedByOtherInstance(instanceId, generation)
|
||||
})
|
||||
await Promise.all([requestVpnPermission(), stopPreviousOwner])
|
||||
await requestVpnPermission()
|
||||
}
|
||||
|
||||
export async function syncMobileVpnService() {
|
||||
syncVpnStatusFromNative(await get_vpn_status())
|
||||
const instanceId = await findRunningTunInstanceId()
|
||||
if (instanceId) {
|
||||
console.log('vpn service sync selected instance', instanceId)
|
||||
await onNetworkInstanceChange(instanceId)
|
||||
return
|
||||
}
|
||||
|
||||
await onNetworkInstanceChange('')
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import android.os.ParcelFileDescriptor
|
||||
import android.os.Bundle
|
||||
import android.content.pm.ServiceInfo
|
||||
import androidx.core.app.NotificationCompat
|
||||
import android.system.OsConstants.AF_INET6
|
||||
import java.net.InetAddress
|
||||
import java.util.Arrays
|
||||
|
||||
@@ -20,10 +21,12 @@ class TauriVpnService : VpnService() {
|
||||
@JvmField var triggerCallback: (String, JSObject) -> Unit = { _, _ -> }
|
||||
@JvmField var self: TauriVpnService? = null
|
||||
@JvmField var ipv4Addr: String? = null
|
||||
@JvmField var ipv4Addrs: Array<String> = emptyArray()
|
||||
@JvmField var routes: Array<String> = emptyArray()
|
||||
@JvmField var dns: String? = null
|
||||
|
||||
const val IPV4_ADDR = "IPV4_ADDR"
|
||||
const val IPV4_ADDRS = "IPV4_ADDRS"
|
||||
const val ROUTES = "ROUTES"
|
||||
const val DNS = "DNS"
|
||||
const val DISALLOWED_APPLICATIONS = "DISALLOWED_APPLICATIONS"
|
||||
@@ -39,7 +42,8 @@ class TauriVpnService : VpnService() {
|
||||
println("vpn on start command ${intent?.getExtras()} $intent")
|
||||
startVpnForegroundService()
|
||||
var args = intent?.getExtras()
|
||||
ipv4Addr = args?.getString(IPV4_ADDR)
|
||||
ipv4Addrs = getIpv4Addrs(args)
|
||||
ipv4Addr = ipv4Addrs.firstOrNull()
|
||||
routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
||||
dns = args?.getString(DNS)
|
||||
|
||||
@@ -90,6 +94,7 @@ class TauriVpnService : VpnService() {
|
||||
|
||||
private fun clearStatus() {
|
||||
ipv4Addr = null
|
||||
ipv4Addrs = emptyArray()
|
||||
routes = emptyArray()
|
||||
dns = null
|
||||
}
|
||||
@@ -159,25 +164,40 @@ class TauriVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
var builder = Builder()
|
||||
.setSession("TauriVpnService")
|
||||
.setBlocking(false)
|
||||
|
||||
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 routes = args?.getStringArray(ROUTES) ?: 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)}," +
|
||||
"disallowedApplications: ${java.util.Arrays.toString(disallowedApplications)}")
|
||||
|
||||
for (ipv4Addr in ipv4Addrs) {
|
||||
val ipParts = ipv4Addr.split("/")
|
||||
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
||||
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
||||
builder.addAddress("fd00::1", 128)
|
||||
}
|
||||
builder.allowFamily(AF_INET6)
|
||||
|
||||
builder.setMtu(mtu)
|
||||
dns?.let { builder.addDnsServer(it) }
|
||||
|
||||
@@ -12,6 +12,7 @@ import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import android.webkit.WebView
|
||||
import org.json.JSONArray
|
||||
|
||||
@InvokeArg
|
||||
class PingArgs {
|
||||
@@ -21,6 +22,7 @@ class PingArgs {
|
||||
@InvokeArg
|
||||
class StartVpnArgs {
|
||||
var ipv4Addr: String? = null
|
||||
var ipv4Addrs: Array<String> = emptyArray()
|
||||
var routes: Array<String> = emptyArray()
|
||||
var dns: String? = null
|
||||
var disallowedApplications: Array<String> = emptyArray()
|
||||
@@ -106,6 +108,7 @@ class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
||||
} else {
|
||||
val intent = Intent(activity, TauriVpnService::class.java)
|
||||
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
|
||||
intent.putExtra(TauriVpnService.IPV4_ADDRS, args.ipv4Addrs)
|
||||
intent.putExtra(TauriVpnService.ROUTES, args.routes)
|
||||
intent.putExtra(TauriVpnService.DNS, args.dns)
|
||||
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
|
||||
@@ -137,7 +140,8 @@ class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
||||
val ret = JSObject()
|
||||
ret.put("running", TauriVpnService.self != null)
|
||||
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)
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface InvokeResponse {
|
||||
|
||||
export interface StartVpnRequest {
|
||||
ipv4Addr?: string;
|
||||
ipv4Addrs?: string[];
|
||||
routes?: string[];
|
||||
dns?: string;
|
||||
disallowedApplications?: string[];
|
||||
@@ -24,6 +25,7 @@ export interface StartVpnRequest {
|
||||
export interface VpnStatusResponse {
|
||||
running: boolean;
|
||||
ipv4Addr?: string;
|
||||
ipv4Addrs?: string[];
|
||||
routes?: string[];
|
||||
dns?: string;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct VoidRequest {}
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartVpnRequest {
|
||||
pub ipv4_addr: Option<String>,
|
||||
pub ipv4_addrs: Option<Vec<String>>,
|
||||
pub routes: Option<Vec<String>>,
|
||||
pub dns: Option<String>,
|
||||
pub disallowed_applications: Option<Vec<String>>,
|
||||
@@ -39,6 +40,7 @@ pub struct Status {
|
||||
pub struct VpnStatus {
|
||||
pub running: bool,
|
||||
pub ipv4_addr: Option<String>,
|
||||
pub ipv4_addrs: Option<Vec<String>>,
|
||||
pub routes: Option<Vec<String>>,
|
||||
pub dns: Option<String>,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user