Files
EasyTier/easytier-gui/src/composables/event.ts
T
KKRainbow ba12da011e Configure one mobile VPN from running TUN instances
Use admitted native members to configure VpnService and attach its
single fd to the core shared TUN. Wait for attachment completion and
record the actual attached members before refreshing VPN state. Report
rejected members in the GUI and preserve the VPN for unchanged healthy
members.

Reconcile ordinary configuration notifications without invalidating an
arbitrary member. Invalidate attachments on instance lifecycle changes,
including restarts that reuse an instance ID. Forward public IPv6
changes alongside proxy route changes to refresh the platform VPN
configuration.

Exclude confirmed IPv4-less members from readiness gating while
retaining retries for unavailable status and pending addresses. Quick
Settings stops all running TUN members and restores that group on the
next start, skipping removed configurations and no_tun networks.

Keep the FFI set_tun_fd call scoped to one instance and report
attachment errors. Cover mobile configuration, member rejection and
replacement, attachment completion, collection notifications, IPv4-less
members, and tile stop/start restoration in GUI regression tests.

Validate with 36 mobile VPN tests, vue-tsc --noEmit, Android GUI cargo
check, workspace clippy, and shared TUN network namespace tests.
2026-10-13 00:44:35 +08:00

126 lines
4.4 KiB
TypeScript

import { Event, listen } from "@tauri-apps/api/event";
import { type } from "@tauri-apps/plugin-os";
import { NetworkTypes } from "easytier-frontend-lib"
import { Utils } from "easytier-frontend-lib";
import { normalizeConfigSource } from './config_source'
import { onNetworkInstancesChange, onNetworkInstanceUpdate } from './mobile_vpn'
interface StoredGuiConfig {
config: NetworkTypes.NetworkConfig
source?: unknown
}
const EVENTS = Object.freeze({
SAVE_CONFIGS: 'save_configs',
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',
});
function onSaveConfigs(event: Event<StoredGuiConfig[]>) {
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
localStorage.setItem(
'networkList',
JSON.stringify(event.payload.map(({ config, source }) => ({
config: NetworkTypes.normalizeNetworkConfig(config),
source: normalizeConfigSource(source),
}))),
);
}
function normalizeInstanceIdPayload(payload: unknown): string {
if (typeof payload === 'string') {
return payload
}
if (payload && typeof payload === 'object') {
const uuid = payload as Partial<Utils.UUID>
if (
typeof uuid.part1 === 'number'
&& typeof uuid.part2 === 'number'
&& typeof uuid.part3 === 'number'
&& typeof uuid.part4 === 'number'
) {
return Utils.UuidToStr(uuid as Utils.UUID)
}
}
if (payload == null) {
return ''
}
const fallback = String(payload)
return fallback === '[object Object]' ? '' : fallback
}
async function onPreRunNetworkInstance(event: Event<unknown>) {
const instanceId = normalizeInstanceIdPayload(event.payload)
console.log(`Received event '${EVENTS.PRE_RUN_NETWORK_INSTANCE}', raw payload:`, event.payload, 'normalized:', instanceId)
if (type() === 'android') {
await prepareVpnService(instanceId);
}
}
async function onPostRunNetworkInstance(event: Event<unknown>) {
const instanceId = normalizeInstanceIdPayload(event.payload)
console.log(`Received event '${EVENTS.POST_RUN_NETWORK_INSTANCE}', raw payload:`, event.payload, 'normalized:', instanceId)
if (type() === 'android') {
await onNetworkInstanceChange(instanceId);
}
}
async function onVpnServiceStop(event: Event<unknown>) {
console.log(`Received event '${EVENTS.VPN_SERVICE_STOP}', raw payload:`, event.payload)
await syncMobileVpnService();
}
async function onVpnServiceConfigChanged(event: Event<unknown>) {
console.log(`Received event '${EVENTS.VPN_SERVICE_CONFIG_CHANGED}'`, event.payload)
if (type() === 'android') {
await onNetworkInstancesChange();
}
}
async function onDhcpIpChanged(event: Event<unknown>) {
const instanceId = normalizeInstanceIdPayload(event.payload)
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${instanceId}`);
if (type() === 'android') {
await onNetworkInstanceUpdate(instanceId);
}
}
async function onProxyCidrsUpdated(event: Event<unknown>) {
const instanceId = normalizeInstanceIdPayload(event.payload)
console.log(`Received event '${EVENTS.PROXY_CIDRS_UPDATED}' for instance: ${instanceId}`);
if (type() === 'android') {
await onNetworkInstanceUpdate(instanceId);
}
}
async function onEventLagged(event: Event<unknown>) {
if (type() === 'android') {
await onNetworkInstanceUpdate(normalizeInstanceIdPayload(event.payload));
}
}
export async function listenGlobalEvents() {
const unlisteners = [
await listen(EVENTS.SAVE_CONFIGS, onSaveConfigs),
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),
];
return () => {
unlisteners.forEach(unlisten => unlisten());
};
}