Compare commits

..
Author SHA1 Message Date
KKRainbow a661829886 Connect shared TUN groups to mobile VPN and GUI
Attach a mobile TUN fd with per-instance address and route sources
through the FFI and Tauri APIs. Group compatible Android instances
by device name and configure VpnService with their combined addresses
and routes.

Reconcile group starts, stops, fd replacement, and route changes.
Guard legacy FFI attachment when shared mobile sources are absent,
and cover mobile configuration in GUI tests.
2026-09-23 21:43:27 +08:00
KKRainbow d3bf71c259 Add shared virtual NIC core with per-member routing
Share a named TUN across instances while keeping unnamed devices
dedicated. Track member addresses, routes, and MTU, and reject
overlapping destinations except the common Magic DNS host route.

Dispatch packets between the physical TUN and member tunnels. Keep
IPv4 and ordinary IPv6 source translation for mobile wrong-source
traffic. Update platform configuration and runtime host integration,
with unit and network namespace coverage for routing and lifecycle.

Add the packet dependency and keep core CI matrix jobs independent.
2026-09-23 21:43:05 +08:00
36 changed files with 7355 additions and 417 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
easytier-web/frontend/dist/* easytier-web/frontend/dist/*
build: build:
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
include: include:
- TARGET: x86_64-unknown-linux-musl - TARGET: x86_64-unknown-linux-musl
Generated
+34
View File
@@ -2557,6 +2557,7 @@ dependencies = [
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"pnet_datalink", "pnet_datalink",
"pnet_packet",
"prost", "prost",
"quanta", "quanta",
"quinn", "quinn",
@@ -6717,6 +6718,39 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "pnet_macros"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.119",
]
[[package]]
name = "pnet_macros_support"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab"
dependencies = [
"pnet_base",
]
[[package]]
name = "pnet_packet"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f"
dependencies = [
"glob",
"pnet_base",
"pnet_macros",
"pnet_macros_support",
]
[[package]] [[package]]
name = "pnet_sys" name = "pnet_sys"
version = "0.35.0" version = "0.35.0"
+1
View File
@@ -13,6 +13,7 @@ ffi-dataplane = [
"easytier/ffi-dataplane", "easytier/ffi-dataplane",
"easytier-core/proxy-smoltcp-stack", "easytier-core/proxy-smoltcp-stack",
] ]
macos-ne = ["easytier/macos-ne"]
[dependencies] [dependencies]
easytier = { workspace = true, default-features = true, features = ["tracing-log"] } easytier = { workspace = true, default-features = true, features = ["tracing-log"] }
@@ -1,5 +1,12 @@
use std::ffi::{CString, c_char, c_int}; use std::ffi::{CString, c_char, c_int};
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
use easytier::common::config::ConfigLoader as _;
use easytier::common::config::{ConfigFileControl, TomlConfigLoader}; use easytier::common::config::{ConfigFileControl, TomlConfigLoader};
use crate::{ use crate::{
@@ -9,6 +16,28 @@ use crate::{
types::KeyValuePair, types::KeyValuePair,
}; };
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
fn mobile_tun_sources_for_legacy_set_tun_fd(inst_id: uuid::Uuid) -> Result<(), String> {
let config = ffi_context()
.manager
.config(inst_id)
.ok_or_else(|| format!("instance config unavailable: {inst_id}"))?;
let flags = config.get_flags();
if flags.dev_name.is_empty() {
return Ok(());
}
Err(format!(
"set_tun_fd legacy API cannot attach shared mobile TUN dev_name={} without tun sources",
flags.dev_name
))
}
/// # Safety /// # Safety
/// Set the tun fd /// Set the tun fd
pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int { pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
@@ -21,7 +50,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) { let inst_id = match resolve_instance_id_by_name(&inst_name) {
Ok(Some(instance_id)) => instance_id, Ok(Some(instance_id)) => instance_id,
Ok(None) => { Ok(None) => {
set_error_msg("instance not found"); set_error_msg(&format!("instance not found: {inst_name}"));
return -1; return -1;
} }
Err(error) => { Err(error) => {
@@ -30,9 +59,23 @@ pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
} }
}; };
#[cfg(any(
target_os = "android",
target_os = "ios",
all(target_os = "macos", feature = "macos-ne"),
target_env = "ohos"
))]
if let Err(error) = mobile_tun_sources_for_legacy_set_tun_fd(inst_id) {
set_error_msg(&error);
return -1;
}
match ffi_context().manager.attach_tun_fd(inst_id, fd) { match ffi_context().manager.attach_tun_fd(inst_id, fd) {
Ok(_) => 0, Ok(_) => 0,
Err(_) => -1, Err(e) => {
set_error_msg(&format!("failed to set tun fd: {}", e));
-1
}
} }
} }
+338 -28
View File
@@ -4,6 +4,8 @@
mod elevate; mod elevate;
use anyhow::Context; use anyhow::Context;
#[cfg(any(target_os = "android", target_os = "ios"))]
use easytier::instance::factory::attach_mobile_tun_fd;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use easytier::instance::factory::subscribe_native_instance_event; use easytier::instance::factory::subscribe_native_instance_event;
use easytier::proto::api::config::{ use easytier::proto::api::config::{
@@ -78,6 +80,9 @@ static RPC_SERVER: once_cell::sync::Lazy<Mutex<Option<RpcServer>>> =
static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> = static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> =
once_cell::sync::Lazy::new(|| RwLock::new(None)); once_cell::sync::Lazy::new(|| RwLock::new(None));
#[cfg(any(target_os = "android", test))]
const ANDROID_SHARED_TUN_DEV_NAME: &str = "easytier-shared";
macro_rules! get_client_manager { macro_rules! get_client_manager {
() => {{ () => {{
let guard = CLIENT_MANAGER let guard = CLIENT_MANAGER
@@ -88,6 +93,20 @@ macro_rules! get_client_manager {
}}; }};
} }
fn normalize_network_config_for_runtime(cfg: &mut NetworkConfig) {
#[cfg(target_os = "android")]
normalize_android_network_config(cfg);
#[cfg(not(target_os = "android"))]
let _ = cfg;
}
#[cfg(any(target_os = "android", test))]
fn normalize_android_network_config(cfg: &mut NetworkConfig) {
if !cfg.no_tun() && cfg.dev_name.as_deref().map(str::is_empty).unwrap_or(true) {
cfg.dev_name = Some(ANDROID_SHARED_TUN_DEV_NAME.to_owned());
}
}
#[tauri::command] #[tauri::command]
fn easytier_version() -> Result<String, String> { fn easytier_version() -> Result<String, String> {
Ok(easytier::VERSION.to_string()) Ok(easytier::VERSION.to_string())
@@ -112,6 +131,8 @@ fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), Strin
#[tauri::command] #[tauri::command]
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> { fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
let mut cfg = cfg;
normalize_network_config_for_runtime(&mut cfg);
let toml = cfg.gen_config().map_err(|e| e.to_string())?; let toml = cfg.gen_config().map_err(|e| e.to_string())?;
Ok(toml.dump()) Ok(toml.dump())
} }
@@ -129,6 +150,8 @@ async fn run_network_instance(
cfg: NetworkConfig, cfg: NetworkConfig,
save: bool, save: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let mut cfg = cfg;
normalize_network_config_for_runtime(&mut cfg);
let client_manager = get_client_manager!()?; let client_manager = get_client_manager!()?;
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?; let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
client_manager client_manager
@@ -243,20 +266,191 @@ async fn set_logging_level(level: String) -> Result<(), String> {
Ok(()) Ok(())
} }
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
struct TunFdInstanceSources {
instance_id: String,
ipv4_addrs: Vec<String>,
#[serde(default)]
ipv6_addrs: Vec<String>,
#[serde(default)]
ipv4_routes: Vec<String>,
#[serde(default)]
ipv6_routes: Vec<String>,
}
#[cfg(any(target_os = "android", target_os = "ios"))]
fn parse_tun_fd_instance_sources(
instance_sources: Vec<TunFdInstanceSources>,
) -> Result<
std::collections::HashMap<uuid::Uuid, easytier::instance::virtual_nic::MobileTunSources>,
String,
> {
let mut parsed = std::collections::HashMap::with_capacity(instance_sources.len());
for source in instance_sources {
let instance_id = source
.instance_id
.parse::<uuid::Uuid>()
.map_err(|err| format!("invalid instance id {}: {err}", source.instance_id))?;
let sources = easytier::instance::virtual_nic::MobileTunSources::parse(
source.ipv4_addrs,
source.ipv6_addrs,
source.ipv4_routes,
source.ipv6_routes,
)
.map_err(|err| err.to_string())?;
if parsed.insert(instance_id, sources).is_some() {
return Err(format!("duplicate tun sources for instance {instance_id}"));
}
}
Ok(parsed)
}
#[tauri::command] #[tauri::command]
async fn set_tun_fd(fd: i32) -> Result<(), String> { async fn set_tun_fd(
fd: i32,
instance_ids: Option<Vec<String>>,
instance_sources: Option<Vec<TunFdInstanceSources>>,
) -> 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 target_ids = match instance_ids {
.next() Some(instance_ids) if !instance_ids.is_empty() => instance_ids
{ .into_iter()
instance_manager .map(|id| {
.attach_tun_fd(uuid, fd) id.parse::<uuid::Uuid>()
.map_err(|e| e.to_string())?; .map_err(|err| format!("invalid instance id {id}: {err}"))
})
.collect::<Result<Vec<_>, _>>()?,
_ => get_client_manager!()?.get_enabled_instances_for_tun_fd(),
};
if target_ids.is_empty() {
return Err("no TUN-enabled instance is available for fd attachment".to_string());
} }
let target_id_set = target_ids
.iter()
.copied()
.collect::<std::collections::HashSet<_>>();
if target_id_set.len() != target_ids.len() {
return Err("duplicate instance id in TUN fd attachment group".to_string());
}
#[cfg(any(target_os = "android", target_os = "ios"))]
if fd <= 0 {
let mut errors = Vec::new();
for instance_id in target_ids {
if let Err(error) = attach_mobile_tun_fd(
instance_manager.as_ref(),
instance_id,
fd,
easytier::instance::virtual_nic::MobileTunSources::default(),
)
.await
{
errors.push(format!("{instance_id}: {error}"));
}
}
return if errors.is_empty() {
Ok(()) Ok(())
} else {
Err(format!(
"failed to detach tun fd from the instance group: {}",
errors.join("; ")
))
};
}
#[cfg(any(target_os = "android", target_os = "ios"))]
let mut source_map = parse_tun_fd_instance_sources(instance_sources.unwrap_or_default())?;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let _ = instance_sources;
#[cfg(any(target_os = "android", target_os = "ios"))]
{
for instance_id in &target_ids {
if instance_manager.instance(*instance_id).is_none() {
return Err(format!("instance {instance_id} not found"));
}
if !source_map.contains_key(instance_id) {
return Err(format!("missing tun sources for instance {instance_id}"));
}
}
if let Some(unexpected_id) = source_map
.keys()
.find(|instance_id| !target_id_set.contains(instance_id))
{
return Err(format!(
"received tun sources for unexpected instance {unexpected_id}"
));
}
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
let mut attach_error = None;
for instance_id in target_ids.iter().copied() {
let result = attach_mobile_tun_fd(
instance_manager.as_ref(),
instance_id,
fd,
source_map
.remove(&instance_id)
.expect("tun sources were validated before attachment"),
)
.await;
if let Err(error) = result {
attach_error = Some(format!("{instance_id}: {error}"));
break;
}
}
if let Some(attach_error) = attach_error {
let mut rollback_errors = Vec::new();
for instance_id in target_ids {
if let Err(error) = attach_mobile_tun_fd(
instance_manager.as_ref(),
instance_id,
0,
easytier::instance::virtual_nic::MobileTunSources::default(),
)
.await
{
rollback_errors.push(format!("{instance_id}: {error}"));
}
}
let rollback = if rollback_errors.is_empty() {
String::new()
} else {
format!("; rollback failed for {}", rollback_errors.join("; "))
};
return Err(format!(
"failed to set tun fd for the instance group: {attach_error}{rollback}"
));
}
return Ok(());
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let mut errors = Vec::new();
for instance_id in target_ids {
if let Err(error) = instance_manager.attach_tun_fd(instance_id, fd) {
errors.push(format!("{instance_id}: {error}"));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(format!(
"failed to set tun fd for the instance group: {}",
errors.join("; ")
))
}
}
} }
#[tauri::command] #[tauri::command]
@@ -872,9 +1066,10 @@ mod manager {
&self, &self,
app: &AppHandle, app: &AppHandle,
inst_id: Uuid, inst_id: Uuid,
cfg: NetworkConfig, mut cfg: NetworkConfig,
source: PersistedConfigSource, source: PersistedConfigSource,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
normalize_network_config_for_runtime(&mut cfg);
let source = self let source = self
.network_configs .network_configs
.get(&inst_id) .get(&inst_id)
@@ -1009,32 +1204,99 @@ 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, shared_dev_name) = first;
let Some(shared_dev_name) = shared_dev_name else {
return vec![first_id];
};
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_core::management::remote_client::RemoteClientError<anyhow::Error>> ) -> Result<(), easytier_core::management::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?;
} }
@@ -1042,11 +1304,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(())
} }
@@ -1062,19 +1333,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(),
); );
} }
@@ -1193,10 +1476,12 @@ mod manager {
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
self.storage.network_configs.clear(); self.storage.network_configs.clear();
for stored in configs { for stored in configs {
let instance_id = stored.config.instance_id(); let mut config = stored.config;
normalize_network_config_for_runtime(&mut config);
let instance_id = config.instance_id();
self.storage.network_configs.insert( self.storage.network_configs.insert(
instance_id.parse()?, instance_id.parse()?,
GUIConfig::new(instance_id.to_string(), stored.config, stored.source), GUIConfig::new(instance_id.to_string(), config, stored.source),
); );
} }
@@ -1263,8 +1548,33 @@ mod manager {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{PersistedConfigSource, StoredGuiConfig}; use super::{PersistedConfigSource, StoredGuiConfig};
use crate::{ANDROID_SHARED_TUN_DEV_NAME, normalize_android_network_config};
use easytier::proto::api::manage::NetworkConfig; use easytier::proto::api::manage::NetworkConfig;
#[test]
fn android_default_tun_config_uses_shared_device_name() {
let mut config = NetworkConfig::default();
normalize_android_network_config(&mut config);
assert_eq!(
config.dev_name.as_deref(),
Some(ANDROID_SHARED_TUN_DEV_NAME)
);
}
#[test]
fn android_no_tun_config_keeps_empty_device_name() {
let mut config = NetworkConfig {
no_tun: Some(true),
..Default::default()
};
normalize_android_network_config(&mut config);
assert!(config.dev_name.is_none());
}
#[test] #[test]
fn stored_gui_config_defaults_missing_source_to_legacy() { fn stored_gui_config_defaults_missing_source_to_legacy() {
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({ let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
+18 -3
View File
@@ -87,8 +87,23 @@ export async function setLoggingLevel(level: string) {
return await invoke('set_logging_level', { level }) return await invoke('set_logging_level', { level })
} }
export async function setTunFd(fd: number) { export interface TunFdInstanceSources {
return await invoke('set_tun_fd', { fd }) instanceId: string
ipv4Addrs: string[]
ipv6Addrs?: string[]
ipv4Routes?: string[]
ipv6Routes?: string[]
}
export async function setTunFd(fd: number, instanceIds?: string[], instanceSources?: TunFdInstanceSources[]) {
const args: { fd: number, instanceIds?: string[], instanceSources?: TunFdInstanceSources[] } = { fd }
if (instanceIds?.length) {
args.instanceIds = instanceIds
}
if (instanceSources?.length) {
args.instanceSources = instanceSources
}
return await invoke('set_tun_fd', args)
} }
export async function getEasytierVersion() { export async function getEasytierVersion() {
@@ -127,7 +142,7 @@ export async function sendConfigs(enabledNetworks: string[]) {
config: NetworkTypes.toBackendNetworkConfig(config), config: NetworkTypes.toBackendNetworkConfig(config),
source, source,
})), })),
enabledNetworks enabledNetworks,
}) })
} }
+10
View File
@@ -15,6 +15,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',
@@ -77,6 +78,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}`);
@@ -105,6 +114,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),
@@ -58,13 +58,15 @@ vi.mock('./backend', () => ({
setTunFd: mocks.setTunFd, setTunFd: mocks.setTunFd,
})) }))
function setConfig(instanceId: string, noTun = false) { function setConfig(instanceId: string, noTun = false, devName?: string) {
mocks.configs.set(instanceId, { mocks.configs.set(instanceId, {
no_tun: noTun, no_tun: noTun,
dev_name: devName,
dhcp: false, dhcp: false,
enable_magic_dns: false, enable_magic_dns: false,
routes: [], routes: [],
}) })
mocks.listNetworkInstanceIds.mockResolvedValue({ running_inst_ids: [...mocks.configs.keys()] })
} }
function setReady(instanceId: string, ipv4: string) { function setReady(instanceId: string, ipv4: string) {
@@ -107,6 +109,76 @@ beforeEach(() => {
}) })
describe('mobile VPN reconciliation ownership', () => { describe('mobile VPN reconciliation ownership', () => {
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('keeps the shared group attached when one member stops', async () => {
setConfig('A', false, 'shared0')
setConfig('B', false, 'shared0')
setConfig('C')
setReady('A', '10.0.0.1')
setReady('B', '10.0.1.1')
const vpn = await loadVpnModule()
await vpn.onNetworkInstanceChange('A')
expect(mocks.startVpn).toHaveBeenCalledWith(expect.objectContaining({
ipv4Addrs: ['10.0.0.1/24', '10.0.1.1/24'],
}))
expect(mocks.setTunFd).toHaveBeenCalledWith(1, ['A', 'B'], expect.arrayContaining([
expect.objectContaining({ instanceId: 'A' }),
expect.objectContaining({ instanceId: 'B' }),
]))
mocks.startVpn.mockClear()
mocks.stopVpn.mockClear()
mocks.listNetworkInstanceIds.mockResolvedValue({ running_inst_ids: ['C', 'B'] })
await vpn.onNetworkInstanceChange('A')
expect(mocks.stopVpn).toHaveBeenCalledTimes(1)
expect(mocks.startVpn).toHaveBeenCalledWith(expect.objectContaining({
ipv4Addrs: ['10.0.1.1/24'],
}))
expect(mocks.setTunFd).toHaveBeenLastCalledWith(1, ['B'], [expect.objectContaining({ instanceId: 'B' })])
})
it('stops A before retrying an unavailable B, then starts B when it becomes ready', async () => { it('stops A before retrying an unavailable B, then starts B when it becomes ready', async () => {
setConfig('A') setConfig('A')
setConfig('B') setConfig('B')
+281 -94
View File
@@ -1,4 +1,5 @@
import type { NetworkTypes } from 'easytier-frontend-lib' import type { NetworkTypes } from 'easytier-frontend-lib'
import type { TunFdInstanceSources } from './backend'
import { addPluginListener } from '@tauri-apps/api/core' import { addPluginListener } from '@tauri-apps/api/core'
import { Utils } from 'easytier-frontend-lib' import { Utils } from 'easytier-frontend-lib'
import { import {
@@ -16,9 +17,12 @@ 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
instanceIds: string[]
instanceSources: TunFdInstanceSources[]
} }
let vpnReconcileTimer: ReturnType<typeof setTimeout> | null = null let vpnReconcileTimer: ReturnType<typeof setTimeout> | null = null
@@ -37,9 +41,12 @@ let vpnTileActionQueue: Promise<void> = Promise.resolve()
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,
instanceIds: [],
instanceSources: [],
} }
export function setMobileVpnTileActionHandler( export function setMobileVpnTileActionHandler(
@@ -154,9 +161,12 @@ function scheduleVpnReconcile(instanceId: string, generation: number, reason: st
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
curVpnStatus.instanceIds = []
curVpnStatus.instanceSources = []
} }
function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) { function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) {
@@ -167,7 +177,13 @@ function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_statu
return return
} }
const ipv4WithCidr = status?.ipv4Addr const ipv4Addrs = status?.ipv4Addrs?.length
? [...status.ipv4Addrs]
: status?.ipv4Addr
? [status.ipv4Addr]
: []
curVpnStatus.ipv4Addrs = ipv4Addrs
const ipv4WithCidr = ipv4Addrs[0]
if (ipv4WithCidr?.length) { if (ipv4WithCidr?.length) {
const [ipv4Addr, cidr] = ipv4WithCidr.split('/') const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
curVpnStatus.ipv4Addr = ipv4Addr curVpnStatus.ipv4Addr = ipv4Addr
@@ -194,12 +210,24 @@ async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
} }
} }
async function detachTunFd(instanceIds: string[], instanceSources: TunFdInstanceSources[]) {
try {
await setTunFd(0, instanceIds, instanceSources)
}
catch (e) {
console.error('detach tun fd failed', e)
}
}
async function doStopVpn(force = false) { async function doStopVpn(force = false) {
const wasRunning = curVpnStatus.running const wasRunning = curVpnStatus.running
if (!force && !wasRunning) { if (!force && !wasRunning) {
activeVpnInstanceId = undefined activeVpnInstanceId = undefined
return return
} }
const instanceIds = [...curVpnStatus.instanceIds]
const instanceSources = [...curVpnStatus.instanceSources]
await detachTunFd(instanceIds, instanceSources)
console.log('stop vpn') console.log('stop vpn')
const stop_ret = await stop_vpn() const stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret))) console.log('stop vpn', JSON.stringify((stop_ret)))
@@ -211,14 +239,24 @@ async function doStopVpn(force = false) {
resetVpnConfigStatus() resetVpnConfigStatus()
} }
async function doStartVpn(instanceId: string, ipv4Addr: string, cidr: number, routes: string[], dns?: string) { async function doStartVpn(
ipv4Addrs: string[],
routes: string[],
dns: string | undefined,
instanceIds: string[],
instanceSources: TunFdInstanceSources[],
) {
if (curVpnStatus.running) { if (curVpnStatus.running) {
return return
} }
console.log('start vpn service', ipv4Addr, cidr, routes, dns) const [ipv4Addr, cidr] = ipv4Addrs[0].split('/')
curVpnStatus.instanceIds = [...instanceIds]
curVpnStatus.instanceSources = [...instanceSources]
console.log('start vpn service', ipv4Addrs, routes, dns, instanceIds)
const request = { const request = {
ipv4Addr: `${ipv4Addr}/${cidr}`, ipv4Addr: ipv4Addrs[0],
ipv4Addrs,
routes, routes,
dns, dns,
disallowedApplications: ['com.kkrainbow.easytier'], disallowedApplications: ['com.kkrainbow.easytier'],
@@ -242,24 +280,29 @@ async function doStartVpn(instanceId: string, ipv4Addr: string, cidr: number, ro
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
activeVpnInstanceId = instanceId activeVpnInstanceId = instanceIds[0]
} }
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
if (payload.fd) { if (payload.fd) {
await setTunFd(payload.fd).catch((e) => { await setTunFd(payload.fd, curVpnStatus.instanceIds, curVpnStatus.instanceSources).catch(async (e) => {
console.error('set tun fd failed', 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) { async function onVpnServiceStop(payload: any) {
console.log('vpn service stop', JSON.stringify(payload)) console.log('vpn service stop', JSON.stringify(payload))
const instanceIds = [...curVpnStatus.instanceIds]
const instanceSources = [...curVpnStatus.instanceSources]
await detachTunFd(instanceIds, instanceSources)
curVpnStatus.running = false curVpnStatus.running = false
activeVpnInstanceId = undefined activeVpnInstanceId = undefined
resetVpnConfigStatus() resetVpnConfigStatus()
@@ -313,14 +356,99 @@ function getRoutesForVpn(routes: Route[] | undefined, node_config: NetworkTypes.
return Array.from(new Set(ret)).sort() return Array.from(new Set(ret)).sort()
} }
function ipv4CidrToRoute(cidr: string): string | undefined {
const [address, prefixText] = cidr.split('/')
const prefix = Number(prefixText)
const octets = address?.split('.').map(octet => Number(octet))
if (
octets?.length !== 4
|| !Number.isInteger(prefix)
|| prefix < 0
|| prefix > 32
|| octets.some((octet) => {
return !Number.isInteger(octet) || octet < 0 || octet > 255
})
) {
return undefined
}
const ip = (
octets[0] * 0x1000000
+ octets[1] * 0x10000
+ octets[2] * 0x100
+ octets[3]
) >>> 0
const mask = prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0
const network = (ip & mask) >>> 0
const route = [
(network >>> 24) & 0xFF,
(network >>> 16) & 0xFF,
(network >>> 8) & 0xFF,
network & 0xFF,
].join('.')
return `${route}/${prefix}`
}
function getCollectedNetworkInfo(response: Awaited<ReturnType<typeof collectNetworkInfo>>, instanceId: string) {
const info = response.info as any
const map = info?.map ?? info
return map?.[instanceId]
}
function sortInstanceSources(sources: TunFdInstanceSources[]): TunFdInstanceSources[] {
return sources
.map(source => ({
instanceId: source.instanceId,
ipv4Addrs: [...source.ipv4Addrs].sort(),
ipv6Addrs: [...(source.ipv6Addrs ?? [])].sort(),
ipv4Routes: [...(source.ipv4Routes ?? [])].sort(),
ipv6Routes: [...(source.ipv6Routes ?? [])].sort(),
}))
.sort((a, b) => a.instanceId.localeCompare(b.instanceId))
}
function splitRoutesByFamily(routes: string[]) {
const ipv4Routes: string[] = []
const ipv6Routes: string[] = []
routes.forEach((route) => {
if (route.includes(':')) {
ipv6Routes.push(route)
}
else {
ipv4Routes.push(route)
}
})
return { ipv4Routes, ipv6Routes }
}
async function stopVpnOwnedByOtherInstance(instanceId: string, generation: number) { async function stopVpnOwnedByOtherInstance(instanceId: string, generation: number) {
if (!isCurrentVpnReconcile(instanceId, generation)) if (!isCurrentVpnReconcile(instanceId, generation))
return false return false
if (curVpnStatus.running && activeVpnInstanceId !== instanceId) { if (curVpnStatus.running && activeVpnInstanceId !== instanceId) {
let sameSharedDevice = curVpnStatus.instanceIds.includes(instanceId)
if (!sameSharedDevice && activeVpnInstanceId) {
try {
const [activeConfig, nextConfig] = await Promise.all([
getConfig(activeVpnInstanceId),
getConfig(instanceId),
])
sameSharedDevice = !!nextConfig.dev_name?.length
&& nextConfig.dev_name === activeConfig.dev_name
}
catch (error) {
console.warn('vpn service owner config unavailable', error)
}
}
if (!isCurrentVpnReconcile(instanceId, generation))
return false
if (!sameSharedDevice) {
console.warn('vpn service owner changed', activeVpnInstanceId, instanceId) console.warn('vpn service owner changed', activeVpnInstanceId, instanceId)
await doStopVpn() await doStopVpn()
} }
}
return isCurrentVpnReconcile(instanceId, generation) return isCurrentVpnReconcile(instanceId, generation)
} }
@@ -331,118 +459,161 @@ async function reconcileNetworkInstance(instanceId: string, generation: number)
clearVpnReconcileTimer() clearVpnReconcileTimer()
if (!instanceId) { let group: Awaited<ReturnType<typeof findRunningTunInstanceGroup>>
console.warn('vpn service skipped because instance id is empty') try {
if (curVpnStatus.running) { group = await findRunningTunInstanceGroup(instanceId || undefined)
await doStopVpn()
} }
catch (error) {
console.warn('vpn service instance group query failed', instanceId, error)
scheduleVpnReconcile(instanceId, generation, 'instance_group_unavailable')
return return
} }
const config = await getConfig(instanceId)
if (!isCurrentVpnReconcile(instanceId, generation)) if (!isCurrentVpnReconcile(instanceId, generation))
return return
console.log('vpn service loaded config', instanceId, JSON.stringify({ if (!group.length) {
no_tun: config.no_tun, if (curVpnStatus.running)
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() await doStopVpn()
}
return return
} }
if (!await stopVpnOwnedByOtherInstance(instanceId, generation)) if (!await stopVpnOwnedByOtherInstance(instanceId, generation))
return return
const ipv4Addrs: string[] = []
const instanceSources: TunFdInstanceSources[] = []
const routes = new Set<string>()
let dns: string | undefined
let retryReason: string | undefined
const retainCurrentSource = (memberId: string, enableMagicDns?: boolean) => {
const source = curVpnStatus.running
? curVpnStatus.instanceSources.find(source => source.instanceId === memberId)
: undefined
if (!source)
return
ipv4Addrs.push(...source.ipv4Addrs)
for (const route of [...(source.ipv4Routes ?? []), ...(source.ipv6Routes ?? [])])
routes.add(route)
instanceSources.push(source)
if (enableMagicDns)
dns = '100.100.100.101'
}
for (const { instanceId: memberId, config } of group) {
let curNetworkInfo let curNetworkInfo
try { try {
curNetworkInfo = (await collectNetworkInfo(instanceId))?.info?.map?.[instanceId] curNetworkInfo = getCollectedNetworkInfo(await collectNetworkInfo(memberId), memberId)
} }
catch (e) { catch (error) {
console.warn('vpn service network info query failed', instanceId, e) console.warn('vpn service network info query failed', memberId, error)
scheduleVpnReconcile(instanceId, generation, 'network_info_query_failed') retryReason ??= 'network_info_query_failed'
return retainCurrentSource(memberId, config.enable_magic_dns)
continue
} }
if (!isCurrentVpnReconcile(instanceId, generation)) if (!isCurrentVpnReconcile(instanceId, generation))
return return
if (!curNetworkInfo) { if (!curNetworkInfo) {
scheduleVpnReconcile(instanceId, generation, 'network_info_unavailable') console.warn('vpn service network info unavailable', memberId, curNetworkInfo?.error_msg)
return retryReason ??= 'network_info_unavailable'
retainCurrentSource(memberId, config.enable_magic_dns)
continue
} }
if (curNetworkInfo.error_msg?.length) { if (curNetworkInfo.error_msg?.length) {
console.warn('vpn service skipped because network instance failed', instanceId, curNetworkInfo.error_msg) console.warn('vpn service network failed', memberId, curNetworkInfo.error_msg)
vpnReconcileAttempts = 0 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'
retainCurrentSource(memberId, config.enable_magic_dns)
continue
}
const networkLength = virtualIpv4?.network_length || 24
const sourceIpv4 = virtualIp + '/' + networkLength
ipv4Addrs.push(sourceIpv4)
const instanceRoutes = new Set<string>()
const localRoute = ipv4CidrToRoute(sourceIpv4)
if (localRoute) {
routes.add(localRoute)
instanceRoutes.add(localRoute)
}
getRoutesForVpn(curNetworkInfo.routes, config).forEach((route) => {
routes.add(route)
instanceRoutes.add(route)
})
const { ipv4Routes, ipv6Routes } = splitRoutesByFamily([...instanceRoutes])
instanceSources.push({
instanceId: memberId,
ipv4Addrs: [sourceIpv4],
ipv6Addrs: [],
ipv4Routes,
ipv6Routes,
})
if (config.enable_magic_dns)
dns = '100.100.100.101'
}
if (!isCurrentVpnReconcile(instanceId, generation))
return
if (!ipv4Addrs.length) {
if (retryReason)
scheduleVpnReconcile(instanceId, generation, retryReason)
const selectedIds = group.map(({ instanceId }) => instanceId).sort()
const activeIds = curVpnStatus.instanceIds
if (curVpnStatus.running && (!activeIds.length || activeIds.some(id => !selectedIds.includes(id))))
await doStopVpn() await doStopVpn()
return return
} }
const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4 if (retryReason)
const virtual_ip = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined scheduleVpnReconcile(instanceId, generation, retryReason)
else
if (!virtual_ip || !virtual_ip.length) {
scheduleVpnReconcile(
instanceId,
generation,
config.dhcp ? 'dhcp_ipv4_unavailable' : 'static_ipv4_unavailable',
)
return
}
vpnReconcileAttempts = 0 vpnReconcileAttempts = 0
const sortedIpv4Addrs = [...ipv4Addrs].sort()
const sortedRoutes = [...routes].sort()
const sortedInstanceIds = instanceSources.map(({ instanceId }) => instanceId).sort()
const sortedInstanceSources = sortInstanceSources(instanceSources)
const configChanged
= JSON.stringify(sortedIpv4Addrs) !== JSON.stringify(curVpnStatus.ipv4Addrs)
|| JSON.stringify(sortedRoutes) !== JSON.stringify(curVpnStatus.routes)
|| dns !== curVpnStatus.dns
|| JSON.stringify(sortedInstanceIds) !== JSON.stringify(curVpnStatus.instanceIds)
|| JSON.stringify(sortedInstanceSources) !== JSON.stringify(sortInstanceSources(curVpnStatus.instanceSources))
let network_length = virtualIpv4?.network_length if (!curVpnStatus.running || configChanged) {
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) { if (curVpnStatus.running) {
try { try {
await doStopVpn() await doStopVpn()
} }
catch (e) { catch (error) {
console.error(e) console.error('stop vpn service failed', error)
} }
} }
try {
if (!isCurrentVpnReconcile(instanceId, generation)) if (!isCurrentVpnReconcile(instanceId, generation))
return return
await doStartVpn(instanceId, virtual_ip, network_length, routes, dns) try {
if (!isCurrentVpnReconcile(instanceId, generation) && activeVpnInstanceId === instanceId) { await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns, sortedInstanceIds, sortedInstanceSources)
if (!isCurrentVpnReconcile(instanceId, generation) && activeVpnInstanceId === sortedInstanceIds[0])
await doStopVpn() await doStopVpn()
} }
} catch (error) {
catch (e) { if (error instanceof Error && error.message === 'vpn_permission_denied') {
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') {
console.info('vpn permission request was denied or dismissed') console.info('vpn permission request was denied or dismissed')
return return
} }
console.error('start vpn service failed', e) console.error('start vpn service failed', error)
} }
} }
} }
@@ -465,36 +636,27 @@ function enqueueVpnReconcile(instanceId: string, generation: number) {
export async function onNetworkInstanceChange(instanceId: string) { export async function onNetworkInstanceChange(instanceId: string) {
const generation = beginVpnReconcile(instanceId || undefined) const generation = beginVpnReconcile(instanceId || undefined)
const group = await findRunningTunInstanceGroup(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) if (vpnReconcileGeneration !== generation)
return return
await enqueueVpnReconcile(instanceId, generation) const selectedId = group[0]?.instanceId
desiredVpnInstanceId = selectedId
await enqueueVpnReconcile(selectedId || '', generation)
} }
export async function onNetworkInstanceUpdate(instanceId: string) { export async function onNetworkInstanceUpdate(instanceId: string) {
if (!instanceId || instanceId !== desiredVpnInstanceId) if (!instanceId)
return return
const generation = beginVpnReconcile(instanceId) if (instanceId !== desiredVpnInstanceId && !curVpnStatus.instanceIds.includes(instanceId)) {
await enqueueVpnReconcile(instanceId, generation) const group = await findRunningTunInstanceGroup(desiredVpnInstanceId)
if (!group.some(inst => inst.instanceId === instanceId))
return
}
await onNetworkInstanceChange(desiredVpnInstanceId || instanceId)
} }
async function isNoTunEnabled(instanceId: string | undefined) { async function isNoTunEnabled(instanceId: string | undefined) {
@@ -520,6 +682,31 @@ 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.find(inst => curVpnStatus.instanceIds.includes(inst.instanceId))
?? 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()
} }
+1
View File
@@ -153,6 +153,7 @@ rand.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
pnet_datalink = { version = "0.35.0", optional = true } pnet_datalink = { version = "0.35.0", optional = true }
pnet_packet = "0.35.0"
smoltcp = { workspace = true, optional = true, features = [ smoltcp = { workspace = true, optional = true, features = [
"std", "std",
"medium-ethernet", "medium-ethernet",
+91 -14
View File
@@ -1,10 +1,42 @@
use std::net::Ipv4Addr; use std::{
collections::{BTreeMap, BTreeSet},
net::Ipv4Addr,
sync::Arc,
};
use super::{Error, IfConfiguerTrait, cidr_to_subnet_mask, run_shell_cmd}; use super::{Error, IfConfiguerTrait, cidr_to_subnet_mask, run_shell_cmd};
use async_trait::async_trait; use async_trait::async_trait;
use cidr::{Ipv4Inet, Ipv6Inet}; use cidr::{Ipv4Inet, Ipv6Inet};
use tokio::sync::Mutex;
#[derive(Default)]
pub struct MacIfConfiger {
configured_ipv4: Arc<Mutex<BTreeMap<String, BTreeSet<Ipv4Inet>>>>,
}
impl MacIfConfiger {
fn build_add_ipv4_cmd(name: &str, addr: Ipv4Inet, has_configured_ipv4: bool) -> String {
let address = addr.address();
if has_configured_ipv4 {
format!(
"ifconfig {} alias {:?} {:?} netmask {}",
name,
address,
address,
cidr_to_subnet_mask(addr.network_length())
)
} else {
format!(
"ifconfig {} {:?}/{:?} {:?} up",
name,
address,
addr.network_length(),
address,
)
}
}
}
pub struct MacIfConfiger {}
#[async_trait] #[async_trait]
impl IfConfiguerTrait for MacIfConfiger { impl IfConfiguerTrait for MacIfConfiger {
async fn add_ipv4_route( async fn add_ipv4_route(
@@ -14,12 +46,28 @@ impl IfConfiguerTrait for MacIfConfiger {
cidr_prefix: u8, cidr_prefix: u8,
cost: Option<i32>, cost: Option<i32>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.add_ipv4_route_with_source_hint(name, address, cidr_prefix, cost, None)
.await
}
async fn add_ipv4_route_with_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
let source_hint = source_hint
.map(|source| format!(" -ifa {}", source))
.unwrap_or_default();
run_shell_cmd( run_shell_cmd(
format!( format!(
"route -n add {} -netmask {} -interface {} -hopcount {}", "route -n add {} -netmask {} -interface {}{} -hopcount {}",
address, address,
cidr_to_subnet_mask(cidr_prefix), cidr_to_subnet_mask(cidr_prefix),
name, name,
source_hint,
cost.unwrap_or(7) cost.unwrap_or(7)
) )
.as_str(), .as_str(),
@@ -51,14 +99,21 @@ impl IfConfiguerTrait for MacIfConfiger {
address: Ipv4Addr, address: Ipv4Addr,
cidr_prefix: u8, cidr_prefix: u8,
) -> Result<(), Error> { ) -> Result<(), Error> {
run_shell_cmd( let addr = Ipv4Inet::new(address, cidr_prefix).map_err(|err| {
format!( anyhow::anyhow!("invalid IPv4 address {address}/{cidr_prefix}: {err:?}")
"ifconfig {} {:?}/{:?} {:?} up", })?;
name, address, cidr_prefix, address, let mut configured_ipv4 = self.configured_ipv4.lock().await;
) let has_configured_ipv4 = configured_ipv4
.as_str(), .get(name)
) .is_some_and(|addresses| !addresses.is_empty());
.await let cmd = Self::build_add_ipv4_cmd(name, addr, has_configured_ipv4);
run_shell_cmd(cmd.as_str()).await?;
configured_ipv4
.entry(name.to_owned())
.or_default()
.insert(addr);
Ok(())
} }
async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> {
@@ -67,12 +122,34 @@ impl IfConfiguerTrait for MacIfConfiger {
} }
async fn remove_ip(&self, name: &str, ip: Option<Ipv4Inet>) -> Result<(), Error> { async fn remove_ip(&self, name: &str, ip: Option<Ipv4Inet>) -> Result<(), Error> {
let mut configured_ipv4 = self.configured_ipv4.lock().await;
if let Some(ip) = ip { if let Some(ip) = ip {
run_shell_cmd(format!("ifconfig {} inet {} delete", name, ip.address()).as_str()).await run_shell_cmd(format!("ifconfig {} inet {} delete", name, ip.address()).as_str())
} else { .await?;
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await if let Some(addresses) = configured_ipv4.get_mut(name) {
addresses.remove(&ip);
if addresses.is_empty() {
configured_ipv4.remove(name);
} }
} }
} else {
if let Some(addresses) = configured_ipv4.get(name).cloned() {
for ip in addresses {
run_shell_cmd(
format!("ifconfig {} inet {} delete", name, ip.address()).as_str(),
)
.await?;
if let Some(addresses) = configured_ipv4.get_mut(name) {
addresses.remove(&ip);
}
}
configured_ipv4.remove(name);
} else {
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await?;
}
}
Ok(())
}
async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> {
run_shell_cmd(format!("ifconfig {} mtu {}", name, mtu).as_str()).await run_shell_cmd(format!("ifconfig {} mtu {}", name, mtu).as_str()).await
+21
View File
@@ -40,6 +40,16 @@ pub trait IfConfiguerTrait: Send + Sync {
) -> Result<(), Error> { ) -> Result<(), Error> {
Ok(()) Ok(())
} }
async fn add_ipv4_route_with_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
_source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
self.add_ipv4_route(name, address, cidr_prefix, cost).await
}
async fn remove_ipv4_route( async fn remove_ipv4_route(
&self, &self,
_name: &str, _name: &str,
@@ -48,6 +58,16 @@ pub trait IfConfiguerTrait: Send + Sync {
) -> Result<(), Error> { ) -> Result<(), Error> {
Ok(()) Ok(())
} }
async fn remove_ipv4_route_with_cost_and_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
_cost: Option<i32>,
_source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
self.remove_ipv4_route(name, address, cidr_prefix).await
}
async fn add_ipv4_ip( async fn add_ipv4_ip(
&self, &self,
_name: &str, _name: &str,
@@ -157,6 +177,7 @@ async fn run_shell_cmd(cmd: &str) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[derive(Default)]
pub struct DummyIfConfiger {} pub struct DummyIfConfiger {}
#[async_trait] #[async_trait]
impl IfConfiguerTrait for DummyIfConfiger {} impl IfConfiguerTrait for DummyIfConfiger {}
+203 -17
View File
@@ -133,6 +133,7 @@ fn dump_netlink_messages<T: NetlinkDecode>(
receive_netlink_dump(builder) receive_netlink_dump(builder)
} }
#[derive(Default)]
pub struct NetlinkIfConfiger {} pub struct NetlinkIfConfiger {}
impl NetlinkIfConfiger { impl NetlinkIfConfiger {
@@ -335,6 +336,53 @@ impl NetlinkIfConfiger {
}) })
.collect()) .collect())
} }
fn ipv4_route_message(
ifindex: u32,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> RouteMessage {
let mut builder = RouteMessageBuilder::new(libc::AF_INET as u8)
.destination(IpAddr::V4(address), cidr_prefix)
.oif(ifindex)
.priority(cost.unwrap_or(65535) as u32)
.table(libc::RT_TABLE_MAIN.into())
.static_protocol()
.universe_scope()
.route_type(RouteType::Unicast);
if let Some(source_hint) = source_hint {
builder = builder.preferred_source(IpAddr::V4(source_hint));
}
builder.build()
}
fn ipv4_route_target_matches(
route: &RouteMessage,
address: Ipv4Addr,
cidr_prefix: u8,
ifidx: u32,
) -> bool {
(route.destination().copied() == Some(IpAddr::V4(address))
|| (cidr_prefix == 0 && address.is_unspecified() && route.destination().is_none()))
&& route.dst_len() == cidr_prefix
&& route.oif() == Some(ifidx)
}
fn ipv4_route_exact_matches(
route: &RouteMessage,
address: Ipv4Addr,
cidr_prefix: u8,
ifidx: u32,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> bool {
Self::ipv4_route_target_matches(route, address, cidr_prefix, ifidx)
&& route.table() == u32::from(libc::RT_TABLE_MAIN)
&& route.priority() == Some(cost.unwrap_or(65535) as u32)
&& route.preferred_source().copied() == source_hint.map(IpAddr::V4)
}
} }
#[async_trait] #[async_trait]
@@ -346,15 +394,25 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
cidr_prefix: u8, cidr_prefix: u8,
cost: Option<i32>, cost: Option<i32>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let message = RouteMessageBuilder::new(libc::AF_INET as u8) self.add_ipv4_route_with_source_hint(name, address, cidr_prefix, cost, None)
.destination(IpAddr::V4(address), cidr_prefix) .await
.oif(Self::get_interface_index(name)?) }
.priority(cost.unwrap_or(65535) as u32)
.table(libc::RT_TABLE_MAIN.into()) async fn add_ipv4_route_with_source_hint(
.static_protocol() &self,
.universe_scope() name: &str,
.route_type(RouteType::Unicast) address: Ipv4Addr,
.build(); cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
let message = NetlinkIfConfiger::ipv4_route_message(
NetlinkIfConfiger::get_interface_index(name)?,
address,
cidr_prefix,
cost,
source_hint,
);
let request = message_request( let request = message_request(
RTM_NEWROUTE, RTM_NEWROUTE,
NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL | NLM_F_REQUEST, NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL | NLM_F_REQUEST,
@@ -373,14 +431,36 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
let ifidx = NetlinkIfConfiger::get_interface_index(name)?; let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
for msg in routes { for msg in routes {
let destination = msg if NetlinkIfConfiger::ipv4_route_target_matches(&msg, address, cidr_prefix, ifidx) {
.destination() let request = message_request(RTM_DELROUTE, NLM_F_ACK | NLM_F_REQUEST, &msg)?;
.copied() send_netlink_req_and_wait_ack(request)?;
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); return Ok(());
if destination == IpAddr::V4(address) }
&& msg.dst_len() == cidr_prefix }
&& msg.oif() == Some(ifidx)
{ Ok(())
}
async fn remove_ipv4_route_with_cost_and_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
let routes = Self::list_routes()?;
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
for msg in routes {
if NetlinkIfConfiger::ipv4_route_exact_matches(
&msg,
address,
cidr_prefix,
ifidx,
cost,
source_hint,
) {
let request = message_request(RTM_DELROUTE, NLM_F_ACK | NLM_F_REQUEST, &msg)?; let request = message_request(RTM_DELROUTE, NLM_F_ACK | NLM_F_REQUEST, &msg)?;
send_netlink_req_and_wait_ack(request)?; send_netlink_req_and_wait_ack(request)?;
return Ok(()); return Ok(());
@@ -601,6 +681,112 @@ mod tests {
} }
} }
#[test]
fn ipv4_route_message_includes_pref_source_when_source_hint_is_set() {
let source_hint = Ipv4Addr::new(10, 231, 1, 1);
let message = NetlinkIfConfiger::ipv4_route_message(
7,
Ipv4Addr::new(10, 99, 0, 0),
24,
Some(123),
Some(source_hint),
);
assert_eq!(message.dst_len(), 24);
assert_eq!(message.preferred_source(), Some(&IpAddr::V4(source_hint)));
assert_eq!(message.priority(), Some(123));
assert_eq!(message.oif(), Some(7));
}
#[test]
fn ipv4_route_exact_match_distinguishes_metric_and_pref_source() {
let address = Ipv4Addr::new(10, 99, 0, 0);
let source_hint = Ipv4Addr::new(10, 99, 0, 1);
let other_source_hint = Ipv4Addr::new(10, 99, 0, 2);
let route =
NetlinkIfConfiger::ipv4_route_message(7, address, 24, Some(123), Some(source_hint));
assert!(NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(123),
Some(source_hint),
));
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(124),
Some(source_hint),
));
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(123),
Some(other_source_hint),
));
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
address,
24,
7,
Some(123),
None,
));
let non_main_table_route = RouteMessageBuilder::new(libc::AF_INET as u8)
.destination(IpAddr::V4(address), 24)
.preferred_source(IpAddr::V4(source_hint))
.oif(7)
.priority(123)
.table(100)
.static_protocol()
.universe_scope()
.route_type(RouteType::Unicast)
.build();
assert!(!NetlinkIfConfiger::ipv4_route_exact_matches(
&non_main_table_route,
address,
24,
7,
Some(123),
Some(source_hint),
));
}
#[test]
fn ipv4_default_route_matches_without_destination_attribute() {
let route = RouteMessageBuilder::new(libc::AF_INET as u8)
.oif(7)
.priority(123)
.table(libc::RT_TABLE_MAIN.into())
.static_protocol()
.universe_scope()
.route_type(RouteType::Unicast)
.build();
assert!(route.destination().is_none());
assert!(NetlinkIfConfiger::ipv4_route_exact_matches(
&route,
Ipv4Addr::UNSPECIFIED,
0,
7,
Some(123),
None,
));
assert!(!NetlinkIfConfiger::ipv4_route_target_matches(
&route,
Ipv4Addr::UNSPECIFIED,
24,
7,
));
}
struct PrepareEnv {} struct PrepareEnv {}
impl PrepareEnv { impl PrepareEnv {
fn new() -> Self { fn new() -> Self {
+72
View File
@@ -37,6 +37,7 @@ const RTA_DST: u16 = 1;
const RTA_SRC: u16 = 2; const RTA_SRC: u16 = 2;
const RTA_OIF: u16 = 4; const RTA_OIF: u16 = 4;
const RTA_PRIORITY: u16 = 6; const RTA_PRIORITY: u16 = 6;
const RTA_PREFSRC: u16 = 7;
const RTA_TABLE: u16 = 15; const RTA_TABLE: u16 = 15;
const NDA_DST: u16 = 1; const NDA_DST: u16 = 1;
@@ -341,7 +342,9 @@ pub(crate) struct RouteMessage {
attributes: Vec<Attribute>, attributes: Vec<Attribute>,
destination: Option<IpAddr>, destination: Option<IpAddr>,
source: Option<IpAddr>, source: Option<IpAddr>,
preferred_source: Option<IpAddr>,
oif: Option<u32>, oif: Option<u32>,
priority: Option<u32>,
} }
impl RouteMessage { impl RouteMessage {
@@ -369,9 +372,25 @@ impl RouteMessage {
self.source.as_ref() self.source.as_ref()
} }
pub(crate) fn preferred_source(&self) -> Option<&IpAddr> {
self.preferred_source.as_ref()
}
pub(crate) fn oif(&self) -> Option<u32> { pub(crate) fn oif(&self) -> Option<u32> {
self.oif self.oif
} }
pub(crate) fn table(&self) -> u32 {
self.attributes
.iter()
.find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_TABLE)
.and_then(|attribute| read_u32(&attribute.value).ok())
.unwrap_or(self.table.into())
}
pub(crate) fn priority(&self) -> Option<u32> {
self.priority
}
} }
impl RouteMessage { impl RouteMessage {
@@ -397,10 +416,18 @@ impl NetlinkDecode for RouteMessage {
.iter() .iter()
.find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_SRC) .find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_SRC)
.and_then(|attribute| parse_ip(family, &attribute.value)); .and_then(|attribute| parse_ip(family, &attribute.value));
let preferred_source = attributes
.iter()
.find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_PREFSRC)
.and_then(|attribute| parse_ip(family, &attribute.value));
let oif = attributes let oif = attributes
.iter() .iter()
.find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_OIF) .find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_OIF)
.and_then(|attribute| read_u32(&attribute.value).ok()); .and_then(|attribute| read_u32(&attribute.value).ok());
let priority = attributes
.iter()
.find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_PRIORITY)
.and_then(|attribute| read_u32(&attribute.value).ok());
Ok(Self { Ok(Self {
family, family,
@@ -415,7 +442,9 @@ impl NetlinkDecode for RouteMessage {
attributes, attributes,
destination, destination,
source, source,
preferred_source,
oif, oif,
priority,
}) })
} }
} }
@@ -458,7 +487,9 @@ impl RouteMessageBuilder {
attributes: Vec::new(), attributes: Vec::new(),
destination: None, destination: None,
source: None, source: None,
preferred_source: None,
oif: None, oif: None,
priority: None,
}, },
} }
} }
@@ -481,6 +512,7 @@ impl RouteMessageBuilder {
} }
pub(crate) fn priority(mut self, priority: u32) -> Self { pub(crate) fn priority(mut self, priority: u32) -> Self {
self.message.priority = Some(priority);
self.message.attributes.push(Attribute::new( self.message.attributes.push(Attribute::new(
RTA_PRIORITY, RTA_PRIORITY,
priority.to_ne_bytes().to_vec(), priority.to_ne_bytes().to_vec(),
@@ -488,6 +520,14 @@ impl RouteMessageBuilder {
self self
} }
pub(crate) fn preferred_source(mut self, address: IpAddr) -> Self {
self.message.preferred_source = Some(address);
self.message
.attributes
.push(Attribute::new(RTA_PREFSRC, ip_bytes(address)));
self
}
pub(crate) fn table(mut self, table: u32) -> Self { pub(crate) fn table(mut self, table: u32) -> Self {
if let Ok(table) = u8::try_from(table) { if let Ok(table) = u8::try_from(table) {
self.message.table = table; self.message.table = table;
@@ -632,6 +672,38 @@ mod tests {
assert_eq!(encode(&decoded), bytes); assert_eq!(encode(&decoded), bytes);
} }
#[test]
fn route_round_trip_preserves_preferred_source_and_priority() {
let source = "10.231.1.1".parse().unwrap();
let message = RouteMessageBuilder::new(libc::AF_INET as u8)
.destination("10.99.0.0".parse().unwrap(), 24)
.preferred_source(source)
.oif(7)
.priority(123)
.table(libc::RT_TABLE_MAIN.into())
.static_protocol()
.universe_scope()
.route_type(RouteType::Unicast)
.build();
let decoded = RouteMessage::from_bytes(&encode(&message)).unwrap();
assert_eq!(decoded.preferred_source(), Some(&source));
assert_eq!(decoded.priority(), Some(123));
assert_eq!(decoded.table(), u32::from(libc::RT_TABLE_MAIN));
}
#[test]
fn route_parser_reads_extended_table_attribute() {
let message = RouteMessageBuilder::new(libc::AF_INET as u8)
.destination("10.99.0.0".parse().unwrap(), 24)
.table(1000)
.route_type(RouteType::Unicast)
.build();
let decoded = RouteMessage::from_bytes(&encode(&message)).unwrap();
assert_eq!(decoded.table(), 1000);
}
#[test] #[test]
fn route_parser_reads_ipv6_source_prefix() { fn route_parser_reads_ipv6_source_prefix() {
let mut bytes = vec![ let mut bytes = vec![
+1
View File
@@ -22,6 +22,7 @@ use winreg::{
}; };
use super::{Error, IfConfiguerTrait}; use super::{Error, IfConfiguerTrait};
#[derive(Default)]
pub struct WindowsIfConfiger {} pub struct WindowsIfConfiger {}
fn format_win_error(error: u32) -> String { fn format_win_error(error: u32) -> String {
+8 -1
View File
@@ -32,6 +32,8 @@ use crate::{
}; };
use super::host::{NativeInstanceHost, native_instance_host}; use super::host::{NativeInstanceHost, native_instance_host};
#[cfg(feature = "tun")]
use super::shared_virtual_nic::ArcSharedVirtualNicRegistry;
#[cfg(feature = "kcp")] #[cfg(feature = "kcp")]
use crate::gateway::kcp_proxy::KcpProxyService; use crate::gateway::kcp_proxy::KcpProxyService;
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
@@ -45,6 +47,7 @@ pub(crate) type NativeCoreInstance = CoreInstance<NativeInstanceHost>;
pub(crate) fn compose_native_core_instance( pub(crate) fn compose_native_core_instance(
toml_config: TomlConfig, toml_config: TomlConfig,
process_runtime: Arc<CoreProcessRuntime>, process_runtime: Arc<CoreProcessRuntime>,
#[cfg(feature = "tun")] shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
compact_runtime: bool, compact_runtime: bool,
) -> anyhow::Result<Arc<NativeCoreInstance>> { ) -> anyhow::Result<Arc<NativeCoreInstance>> {
let host_config = if compact_runtime { let host_config = if compact_runtime {
@@ -58,7 +61,11 @@ pub(crate) fn compose_native_core_instance(
&normalized, &normalized,
&host_config, &host_config,
)); ));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone()); let runtime_host = NativeInstanceRuntimeHost::new(
global_ctx.clone(),
#[cfg(feature = "tun")]
shared_virtual_nic_registry,
);
let mut adapters = runtime_core_host_adapters_with_packet_egress_and_config( let mut adapters = runtime_core_host_adapters_with_packet_egress_and_config(
global_ctx.clone(), global_ctx.clone(),
process_runtime, process_runtime,
+104 -3
View File
@@ -5,7 +5,15 @@ use std::{net::Ipv4Addr, sync::Arc, time::Duration};
use easytier_core::instance::CorePacketPlane; use easytier_core::instance::CorePacketPlane;
use crate::common::global_ctx::ArcGlobalCtx; use crate::{
common::{
error::Error as EtError,
global_ctx::ArcGlobalCtx,
ifcfg::{IfConfiger, IfConfiguerTrait},
netns::NetNS,
},
instance::virtual_nic::NicBackend,
};
use super::{client_instance::MagicDnsClientInstance, server_instance::MagicDnsServerInstance}; use super::{client_instance::MagicDnsClientInstance, server_instance::MagicDnsServerInstance};
@@ -17,6 +25,52 @@ pub struct DnsRunner {
tun_dev: Option<String>, tun_dev: Option<String>,
tun_inet: Ipv4Inet, tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr, fake_ip: Ipv4Addr,
shared_route_backend: Option<NicBackend>,
}
#[derive(Clone)]
struct MagicDnsFakeIpRouteClaim {
tun_dev: Option<String>,
net_ns: NetNS,
fake_ip: Ipv4Addr,
route_backend: NicBackend,
}
impl MagicDnsFakeIpRouteClaim {
async fn add(&self) -> anyhow::Result<()> {
let cost = if cfg!(target_os = "windows") {
Some(4)
} else {
None
};
match self
.route_backend
.add_route_with_cost(self.fake_ip, 32, cost)
.await
{
Err(EtError::IOError(err))
if err.kind() == std::io::ErrorKind::AlreadyExists && self.tun_dev.is_some() =>
{
let ifcfg = IfConfiger::default();
let _guard = self.net_ns.guard();
ifcfg
.remove_ipv4_route(self.tun_dev.as_deref().unwrap(), self.fake_ip, 32)
.await?;
self.route_backend
.add_route_with_cost(self.fake_ip, 32, cost)
.await?;
Ok(())
}
result => result.map_err(Into::into),
}
}
async fn remove(&self) {
if let Err(err) = self.route_backend.remove_route(self.fake_ip, 32).await {
tracing::warn!(?err, fake_ip = ?self.fake_ip, "remove magic dns route failed");
}
}
} }
impl DnsRunner { impl DnsRunner {
@@ -35,9 +89,15 @@ impl DnsRunner {
tun_dev, tun_dev,
tun_inet, tun_inet,
fake_ip, fake_ip,
shared_route_backend: None,
} }
} }
pub(crate) fn with_shared_route_backend(mut self, route_backend: Option<NicBackend>) -> Self {
self.shared_route_backend = route_backend;
self
}
async fn clean_env(&mut self) { async fn clean_env(&mut self) {
if let Some(server) = self.server.take() { if let Some(server) = self.server.take() {
server.clean_env().await; server.clean_env().await;
@@ -45,9 +105,34 @@ impl DnsRunner {
self.client.take(); self.client.take();
} }
fn should_manage_fake_ip_route_externally(&self) -> bool {
self.shared_route_backend.is_some() && !self.tun_inet.contains(&self.fake_ip)
}
fn fake_ip_route_claim(&self) -> Option<MagicDnsFakeIpRouteClaim> {
if !self.should_manage_fake_ip_route_externally() {
return None;
}
Some(MagicDnsFakeIpRouteClaim {
tun_dev: self.tun_dev.clone(),
net_ns: self.global_ctx.net_ns.clone(),
fake_ip: self.fake_ip,
route_backend: self.shared_route_backend.clone()?,
})
}
async fn run_once(&mut self) -> anyhow::Result<()> { async fn run_once(&mut self) -> anyhow::Result<()> {
if let Some(claim) = self.fake_ip_route_claim() {
claim
.add()
.await
.map_err(|err| anyhow::anyhow!("failed to add magic dns fake-ip route: {err}"))?;
}
// try server first // try server first
match MagicDnsServerInstance::new( let server_result = if self.should_manage_fake_ip_route_externally() {
MagicDnsServerInstance::new_with_external_fake_ip_route(
self.packet_plane.clone(), self.packet_plane.clone(),
self.global_ctx.clone(), self.global_ctx.clone(),
self.tun_dev.clone(), self.tun_dev.clone(),
@@ -55,7 +140,18 @@ impl DnsRunner {
self.fake_ip, self.fake_ip,
) )
.await .await
{ } else {
MagicDnsServerInstance::new(
self.packet_plane.clone(),
self.global_ctx.clone(),
self.tun_dev.clone(),
self.tun_inet,
self.fake_ip,
)
.await
};
match server_result {
Ok(server) => { Ok(server) => {
self.server = Some(server); self.server = Some(server);
tracing::info!("DnsRunner::run_once: server started"); tracing::info!("DnsRunner::run_once: server started");
@@ -74,11 +170,16 @@ impl DnsRunner {
} }
pub async fn run(&mut self, canel_token: CancellationToken) { pub async fn run(&mut self, canel_token: CancellationToken) {
let fake_ip_route_claim = self.fake_ip_route_claim();
loop { loop {
tracing::info!("DnsRunner::run: start"); tracing::info!("DnsRunner::run: start");
tokio::select! { tokio::select! {
_ = canel_token.cancelled() => { _ = canel_token.cancelled() => {
self.clean_env().await; self.clean_env().await;
if let Some(claim) = &fake_ip_route_claim {
claim.remove().await;
}
tracing::info!("DnsRunner::run: cancelled"); tracing::info!("DnsRunner::run: cancelled");
return; return;
} }
@@ -14,8 +14,10 @@ use super::{
}; };
use crate::{ use crate::{
common::{ common::{
error::Error as EtError,
global_ctx::ArcGlobalCtx, global_ctx::ArcGlobalCtx,
ifcfg::{IfConfiger, IfConfiguerTrait}, ifcfg::{IfConfiger, IfConfiguerTrait},
netns::NetNS,
}, },
instance::dns_server::{ instance::dns_server::{
config::{Record, RecordBuilder, RecordType}, config::{Record, RecordBuilder, RecordType},
@@ -51,7 +53,9 @@ use std::{collections::BTreeMap, io, net::Ipv4Addr, str::FromStr, sync::Arc, tim
pub(super) struct MagicDnsServerInstanceData { pub(super) struct MagicDnsServerInstanceData {
dns_server: Server, dns_server: Server,
tun_dev: Option<String>, tun_dev: Option<String>,
net_ns: NetNS,
fake_ip: Ipv4Addr, fake_ip: Ipv4Addr,
manage_fake_ip_route: bool,
route_store: MagicDnsRecordStore, route_store: MagicDnsRecordStore,
record_apply: tokio::sync::Mutex<()>, record_apply: tokio::sync::Mutex<()>,
@@ -356,12 +360,66 @@ fn get_system_config(
} }
impl MagicDnsServerInstance { impl MagicDnsServerInstance {
async fn add_fake_ip_route(
tun_dev_name: &str,
fake_ip: Ipv4Addr,
net_ns: &NetNS,
cost: Option<i32>,
) -> Result<(), anyhow::Error> {
let ifcfg = IfConfiger::default();
let _guard = net_ns.guard();
match ifcfg.add_ipv4_route(tun_dev_name, fake_ip, 32, cost).await {
Err(EtError::IOError(err)) if err.kind() == io::ErrorKind::AlreadyExists => {
ifcfg.remove_ipv4_route(tun_dev_name, fake_ip, 32).await?;
ifcfg
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
.await?;
Ok(())
}
ret => ret.map_err(Into::into),
}
}
async fn remove_fake_ip_route(tun_dev_name: &str, fake_ip: Ipv4Addr, net_ns: &NetNS) {
let ifcfg = IfConfiger::default();
let _guard = net_ns.guard();
if let Err(err) = ifcfg.remove_ipv4_route(tun_dev_name, fake_ip, 32).await {
tracing::warn!(
?err,
?tun_dev_name,
?fake_ip,
"remove magic dns route failed"
);
}
}
pub(crate) async fn new( pub(crate) async fn new(
packet_plane: Arc<CorePacketPlane>, packet_plane: Arc<CorePacketPlane>,
global_ctx: ArcGlobalCtx, global_ctx: ArcGlobalCtx,
tun_dev: Option<String>, tun_dev: Option<String>,
tun_inet: Ipv4Inet, tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr, fake_ip: Ipv4Addr,
) -> Result<Self, anyhow::Error> {
Self::new_inner(packet_plane, global_ctx, tun_dev, tun_inet, fake_ip, true).await
}
pub(crate) async fn new_with_external_fake_ip_route(
packet_plane: Arc<CorePacketPlane>,
global_ctx: ArcGlobalCtx,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
) -> Result<Self, anyhow::Error> {
Self::new_inner(packet_plane, global_ctx, tun_dev, tun_inet, fake_ip, false).await
}
async fn new_inner(
packet_plane: Arc<CorePacketPlane>,
global_ctx: ArcGlobalCtx,
tun_dev: Option<String>,
tun_inet: Ipv4Inet,
fake_ip: Ipv4Addr,
manage_fake_ip_route: bool,
) -> Result<Self, anyhow::Error> { ) -> Result<Self, anyhow::Error> {
let tcp_listener = runtime_rpc_listener(MAGIC_DNS_INSTANCE_SOCKET_ADDR.parse()?); let tcp_listener = runtime_rpc_listener(MAGIC_DNS_INSTANCE_SOCKET_ADDR.parse()?);
let mut rpc_server = StandAloneServer::new(tcp_listener); let mut rpc_server = StandAloneServer::new(tcp_listener);
@@ -374,7 +432,8 @@ impl MagicDnsServerInstance {
let mut dns_server = Server::new(dns_config); let mut dns_server = Server::new(dns_config);
dns_server.run().await?; dns_server.run().await?;
if !tun_inet.contains(&fake_ip) if manage_fake_ip_route
&& !tun_inet.contains(&fake_ip)
&& let Some(tun_dev_name) = &tun_dev && let Some(tun_dev_name) = &tun_dev
{ {
let cost = if cfg!(target_os = "windows") { let cost = if cfg!(target_os = "windows") {
@@ -382,16 +441,15 @@ impl MagicDnsServerInstance {
} else { } else {
None None
}; };
let ifcfg = IfConfiger {}; Self::add_fake_ip_route(tun_dev_name, fake_ip, &global_ctx.net_ns, cost).await?;
ifcfg
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
.await?;
} }
let data = Arc::new(MagicDnsServerInstanceData { let data = Arc::new(MagicDnsServerInstanceData {
dns_server, dns_server,
tun_dev: tun_dev.clone(), tun_dev: tun_dev.clone(),
net_ns: global_ctx.net_ns.clone(),
fake_ip, fake_ip,
manage_fake_ip_route,
route_store: MagicDnsRecordStore::default(), route_store: MagicDnsRecordStore::default(),
record_apply: tokio::sync::Mutex::new(()), record_apply: tokio::sync::Mutex::new(()),
system_config: get_system_config(tun_dev.as_deref())?, system_config: get_system_config(tun_dev.as_deref())?,
@@ -436,14 +494,13 @@ impl MagicDnsServerInstance {
if let Err(e) = ret { if let Err(e) = ret {
tracing::error!("Failed to close system config: {:?}", e); tracing::error!("Failed to close system config: {:?}", e);
} }
if !self.tun_inet.contains(&self.data.fake_ip) }
if self.data.manage_fake_ip_route
&& !self.tun_inet.contains(&self.data.fake_ip)
&& let Some(tun_dev_name) = &self.data.tun_dev && let Some(tun_dev_name) = &self.data.tun_dev
{ {
let ifcfg = IfConfiger {}; Self::remove_fake_ip_route(tun_dev_name, self.data.fake_ip, &self.data.net_ns).await;
let _ = ifcfg
.remove_ipv4_route(tun_dev_name, self.data.fake_ip, 32)
.await;
}
} }
self.packet_filter.close().await; self.packet_filter.close().await;
+30
View File
@@ -1,5 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "tun")]
use tokio::sync::Mutex;
#[cfg(any(feature = "management-rpc", test))] #[cfg(any(feature = "management-rpc", test))]
use easytier_core::instance::manager::InstanceManager; use easytier_core::instance::manager::InstanceManager;
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]
@@ -12,6 +15,10 @@ use easytier_core::{
use crate::common::global_ctx::EventBusSubscriber; use crate::common::global_ctx::EventBusSubscriber;
#[cfg(feature = "tun")]
use super::shared_virtual_nic::{ArcSharedVirtualNicRegistry, SharedVirtualNicRegistry};
#[cfg(all(feature = "tun", mobile))]
use super::virtual_nic::MobileTunSources;
use super::{ use super::{
composition::compose_native_core_instance, host::NativeInstanceHost, composition::compose_native_core_instance, host::NativeInstanceHost,
runtime_host::NativeInstanceRuntimeHost, runtime_host::NativeInstanceRuntimeHost,
@@ -51,6 +58,22 @@ pub fn subscribe_native_instance_event(
.map(NativeInstanceRuntimeHost::subscribe_event) .map(NativeInstanceRuntimeHost::subscribe_event)
} }
#[cfg(all(feature = "management-rpc", feature = "tun", mobile))]
pub async fn attach_mobile_tun_fd(
manager: &NativeInstanceManager,
instance_id: uuid::Uuid,
fd: i32,
sources: MobileTunSources,
) -> anyhow::Result<()> {
let instance = manager
.instance(instance_id)
.ok_or_else(|| anyhow::anyhow!("instance {instance_id} not found"))?;
let runtime = instance
.runtime_host::<NativeInstanceRuntimeHost>()
.ok_or_else(|| anyhow::anyhow!("native runtime host is unavailable"))?;
runtime.attach_mobile_tun_fd(fd, sources).await
}
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]
pub fn native_instance_manager_with_runtime( pub fn native_instance_manager_with_runtime(
runtime_handle: tokio::runtime::Handle, runtime_handle: tokio::runtime::Handle,
@@ -97,6 +120,8 @@ fn native_instance_manager_with_optional_runtime(
/// Native construction Adapter for the canonical core InstanceManager. /// Native construction Adapter for the canonical core InstanceManager.
pub struct NativeInstanceFactory { pub struct NativeInstanceFactory {
process_runtime: Arc<CoreProcessRuntime>, process_runtime: Arc<CoreProcessRuntime>,
#[cfg(feature = "tun")]
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
runtime_handle: Option<tokio::runtime::Handle>, runtime_handle: Option<tokio::runtime::Handle>,
compact_runtime: bool, compact_runtime: bool,
#[cfg(feature = "logging")] #[cfg(feature = "logging")]
@@ -107,6 +132,8 @@ impl NativeInstanceFactory {
pub fn new(process_runtime: Arc<CoreProcessRuntime>) -> Self { pub fn new(process_runtime: Arc<CoreProcessRuntime>) -> Self {
Self { Self {
process_runtime, process_runtime,
#[cfg(feature = "tun")]
shared_virtual_nic_registry: Arc::new(Mutex::new(SharedVirtualNicRegistry::new())),
runtime_handle: None, runtime_handle: None,
compact_runtime: false, compact_runtime: false,
#[cfg(feature = "logging")] #[cfg(feature = "logging")]
@@ -126,6 +153,7 @@ impl NativeInstanceFactory {
self self
} }
#[cfg(feature = "management-rpc")]
fn with_compact_runtime(mut self) -> Self { fn with_compact_runtime(mut self) -> Self {
self.compact_runtime = true; self.compact_runtime = true;
self self
@@ -149,6 +177,8 @@ impl InstanceFactory for NativeInstanceFactory {
let instance = compose_native_core_instance( let instance = compose_native_core_instance(
config, config,
self.process_runtime.clone(), self.process_runtime.clone(),
#[cfg(feature = "tun")]
self.shared_virtual_nic_registry.clone(),
self.compact_runtime, self.compact_runtime,
)?; )?;
#[cfg(feature = "logging")] #[cfg(feature = "logging")]
+3
View File
@@ -18,6 +18,9 @@ pub(crate) mod listeners;
#[cfg(feature = "public-ipv6-provider")] #[cfg(feature = "public-ipv6-provider")]
pub(crate) mod public_ipv6_provider; pub(crate) mod public_ipv6_provider;
#[cfg(feature = "tun")]
pub mod shared_virtual_nic;
#[cfg(feature = "tun")] #[cfg(feature = "tun")]
pub mod virtual_nic; pub mod virtual_nic;
+39 -7
View File
@@ -1,13 +1,18 @@
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "web-client")]
use easytier_core::config::runtime::CoreInstanceRuntimeConfig;
use easytier_core::{ use easytier_core::{
config::runtime::CoreInstanceRuntimeConfig, gateway::dhcp::DhcpIpv4Host, gateway::dhcp::DhcpIpv4Host, host::packet::HostPacketReceiver, instance::CorePacketPlane,
host::packet::HostPacketReceiver, instance::CorePacketPlane,
}; };
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::common::global_ctx::ArcGlobalCtx; use crate::common::global_ctx::ArcGlobalCtx;
#[cfg(feature = "tun")]
use crate::instance::shared_virtual_nic::ArcSharedVirtualNicRegistry;
#[cfg(all(feature = "tun", mobile))]
use crate::instance::virtual_nic::MobileTunSources;
mod event_journal; mod event_journal;
mod implementation; mod implementation;
@@ -39,9 +44,17 @@ pub(crate) struct NativeInstanceRuntimeHost {
} }
impl NativeInstanceRuntimeHost { impl NativeInstanceRuntimeHost {
pub(crate) fn new(global_ctx: ArcGlobalCtx) -> Arc<Self> { pub(crate) fn new(
global_ctx: ArcGlobalCtx,
#[cfg(feature = "tun")] shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
) -> Arc<Self> {
let cancel = CancellationToken::new(); let cancel = CancellationToken::new();
let tun = NativeTunRuntime::new(global_ctx.clone(), cancel.clone()); let tun = NativeTunRuntime::new(
global_ctx.clone(),
cancel.clone(),
#[cfg(feature = "tun")]
shared_virtual_nic_registry,
);
let event_journal = EventJournal::new(&global_ctx); let event_journal = EventJournal::new(&global_ctx);
Arc::new(Self { Arc::new(Self {
global_ctx, global_ctx,
@@ -121,6 +134,15 @@ impl NativeInstanceRuntimeHost {
self.tun.attach_fd(fd) self.tun.attach_fd(fd)
} }
#[cfg(all(feature = "tun", mobile))]
pub(crate) async fn attach_mobile_tun_fd(
&self,
fd: i32,
sources: MobileTunSources,
) -> anyhow::Result<()> {
self.tun.attach_mobile_fd(fd, sources).await
}
fn install_packet_receiver(&self, receiver: HostPacketReceiver) -> anyhow::Result<()> { fn install_packet_receiver(&self, receiver: HostPacketReceiver) -> anyhow::Result<()> {
self.tun.install_packet_receiver(receiver) self.tun.install_packet_receiver(receiver)
} }
@@ -134,6 +156,16 @@ mod tests {
global_ctx::{GlobalCtx, GlobalCtxEvent}, global_ctx::{GlobalCtx, GlobalCtxEvent},
}; };
fn runtime_host(global_ctx: ArcGlobalCtx) -> Arc<NativeInstanceRuntimeHost> {
NativeInstanceRuntimeHost::new(
global_ctx,
#[cfg(feature = "tun")]
Arc::new(tokio::sync::Mutex::new(
crate::instance::shared_virtual_nic::SharedVirtualNicRegistry::new(),
)),
)
}
#[cfg(feature = "web-client")] #[cfg(feature = "web-client")]
fn runtime_config(config: &TomlConfig) -> CoreInstanceRuntimeConfig { fn runtime_config(config: &TomlConfig) -> CoreInstanceRuntimeConfig {
let normalized = easytier_core::instance::CoreInstanceConfig::from_toml(config).unwrap(); let normalized = easytier_core::instance::CoreInstanceConfig::from_toml(config).unwrap();
@@ -146,7 +178,7 @@ mod tests {
#[test] #[test]
fn runtime_host_owns_event_subscription_context() { fn runtime_host_owns_event_subscription_context() {
let global_ctx = Arc::new(GlobalCtx::new(TomlConfig::default())); let global_ctx = Arc::new(GlobalCtx::new(TomlConfig::default()));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone()); let runtime_host = runtime_host(global_ctx.clone());
let mut events = runtime_host.subscribe_event(); let mut events = runtime_host.subscribe_event();
global_ctx.issue_event(GlobalCtxEvent::CredentialChanged); global_ctx.issue_event(GlobalCtxEvent::CredentialChanged);
@@ -167,7 +199,7 @@ mod tests {
config.set_ipv4(Some("10.20.0.1/24".parse().unwrap())); config.set_ipv4(Some("10.20.0.1/24".parse().unwrap()));
config.set_ipv6(Some("fd00::1/64".parse().unwrap())); config.set_ipv6(Some("fd00::1/64".parse().unwrap()));
let global_ctx = Arc::new(GlobalCtx::new(config.clone())); let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone()); let runtime_host = runtime_host(global_ctx.clone());
assert_eq!(global_ctx.get_hostname(), "before"); assert_eq!(global_ctx.get_hostname(), "before");
assert_eq!(global_ctx.get_ipv4(), Some("10.20.0.1/24".parse().unwrap())); assert_eq!(global_ctx.get_ipv4(), Some("10.20.0.1/24".parse().unwrap()));
@@ -209,7 +241,7 @@ mod tests {
let config = TomlConfig::default(); let config = TomlConfig::default();
config.set_dhcp(true); config.set_dhcp(true);
let global_ctx = Arc::new(GlobalCtx::new(config.clone())); let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone()); let runtime_host = runtime_host(global_ctx.clone());
let lease = "10.20.0.7/24".parse().unwrap(); let lease = "10.20.0.7/24".parse().unwrap();
global_ctx.set_ipv4(Some(lease)); global_ctx.set_ipv4(Some(lease));
@@ -3,9 +3,9 @@ use easytier_core::instance::CorePacketPlane;
#[cfg(feature = "magic-dns")] #[cfg(feature = "magic-dns")]
use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle}; use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
use crate::common::global_ctx::ArcGlobalCtx;
#[cfg(feature = "magic-dns")] #[cfg(feature = "magic-dns")]
use crate::instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner}; use crate::instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
use crate::{common::global_ctx::ArcGlobalCtx, instance::virtual_nic::NicBackend};
#[derive(Default)] #[derive(Default)]
pub(super) struct MagicDnsRuntime { pub(super) struct MagicDnsRuntime {
@@ -26,6 +26,7 @@ impl MagicDnsRuntime {
packet_plane: std::sync::Arc<CorePacketPlane>, packet_plane: std::sync::Arc<CorePacketPlane>,
tun_dev: Option<String>, tun_dev: Option<String>,
tun_ip: Ipv4Inet, tun_ip: Ipv4Inet,
shared_route_backend: Option<NicBackend>,
) -> Self { ) -> Self {
let active = global_ctx.get_flags().accept_dns.then(|| { let active = global_ctx.get_flags().accept_dns.then(|| {
let mut runner = DnsRunner::new( let mut runner = DnsRunner::new(
@@ -34,7 +35,8 @@ impl MagicDnsRuntime {
tun_dev, tun_dev,
tun_ip, tun_ip,
MAGIC_DNS_FAKE_IP.parse().unwrap(), MAGIC_DNS_FAKE_IP.parse().unwrap(),
); )
.with_shared_route_backend(shared_route_backend);
let cancel = CancellationToken::new(); let cancel = CancellationToken::new();
let task_cancel = cancel.clone(); let task_cancel = cancel.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
@@ -54,6 +56,7 @@ impl MagicDnsRuntime {
_packet_plane: std::sync::Arc<CorePacketPlane>, _packet_plane: std::sync::Arc<CorePacketPlane>,
_tun_dev: Option<String>, _tun_dev: Option<String>,
_tun_ip: Ipv4Inet, _tun_ip: Ipv4Inet,
_shared_route_backend: Option<NicBackend>,
) -> Self { ) -> Self {
Self::default() Self::default()
} }
@@ -3,11 +3,42 @@ use std::{
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
}; };
use easytier_core::host::packet::HostPacketReceiver; use easytier_core::{host::packet::HostPacketReceiver, instance::CorePacketPlane};
use tokio::{sync::Mutex, task::JoinSet}; use tokio::{sync::Mutex, task::JoinSet};
use super::MagicDnsRuntime; use super::MagicDnsRuntime;
use crate::instance::virtual_nic::NicCtx; use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx},
instance::{shared_virtual_nic::ArcSharedVirtualNicRegistry, virtual_nic::NicCtx},
};
pub(super) async fn create_nic_ctx(
global_ctx: ArcGlobalCtx,
packet_plane: Arc<CorePacketPlane>,
receiver: Arc<Mutex<HostPacketReceiver>>,
close_notifier: Arc<tokio::sync::Notify>,
registry: ArcSharedVirtualNicRegistry,
) -> Result<NicCtx, Error> {
if global_ctx.get_flags().dev_name.is_empty() {
return Ok(NicCtx::new(
global_ctx,
packet_plane,
receiver,
close_notifier,
));
}
let member_id = global_ctx.get_id();
NicCtx::new_shared(
global_ctx,
packet_plane,
receiver,
close_notifier,
registry,
member_id,
)
.await
}
struct NicCtxContainer { struct NicCtxContainer {
_nic_ctx: Option<Box<dyn Any + Send>>, _nic_ctx: Option<Box<dyn Any + Send>>,
@@ -14,28 +14,37 @@ use tokio::{
}; };
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use super::{MagicDnsRuntime, tun_common::TunNicState}; use super::{
MagicDnsRuntime,
tun_common::{TunNicState, create_nic_ctx},
};
use crate::{ use crate::{
common::{ common::{
error::Error, error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
}, },
instance::virtual_nic::NicCtx, instance::shared_virtual_nic::ArcSharedVirtualNicRegistry,
}; };
pub(super) struct NativeTunRuntime { pub(super) struct NativeTunRuntime {
global_ctx: ArcGlobalCtx, global_ctx: ArcGlobalCtx,
cancel: CancellationToken, cancel: CancellationToken,
nic: TunNicState, nic: TunNicState,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
static_ip_task: Mutex<Option<JoinHandle<()>>>, static_ip_task: Mutex<Option<JoinHandle<()>>>,
} }
impl NativeTunRuntime { impl NativeTunRuntime {
pub(super) fn new(global_ctx: ArcGlobalCtx, cancel: CancellationToken) -> Self { pub(super) fn new(
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
) -> Self {
Self { Self {
global_ctx, global_ctx,
cancel, cancel,
nic: TunNicState::empty(), nic: TunNicState::empty(),
shared_virtual_nic_registry,
static_ip_task: Mutex::new(None), static_ip_task: Mutex::new(None),
} }
} }
@@ -67,6 +76,7 @@ impl NativeTunRuntime {
let cancel = self.cancel.clone(); let cancel = self.cancel.clone();
let global_ctx = self.global_ctx.clone(); let global_ctx = self.global_ctx.clone();
let receiver = self.nic.receiver(); let receiver = self.nic.receiver();
let shared_virtual_nic_registry = self.shared_virtual_nic_registry.clone();
let (output, first_round) = oneshot::channel(); let (output, first_round) = oneshot::channel();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut output = Some(output); let mut output = Some(output);
@@ -76,12 +86,29 @@ impl NativeTunRuntime {
return; return;
} }
let closed = Arc::new(Notify::new()); let closed = Arc::new(Notify::new());
let mut nic = NicCtx::new( let mut nic = match create_nic_ctx(
global_ctx.clone(), global_ctx.clone(),
packet_plane.clone(), packet_plane.clone(),
receiver.clone(), receiver.clone(),
closed.clone(), closed.clone(),
); shared_virtual_nic_registry.clone(),
)
.await
{
Ok(nic) => nic,
Err(error) => {
if let Some(output) = output.take() {
let _ = output.send(Err(error));
return;
}
tracing::error!(?error, "failed to create native interface context");
tokio::select! {
_ = cancel.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(1)) => {}
}
continue;
}
};
let result = tokio::select! { let result = tokio::select! {
biased; biased;
_ = cancel.cancelled() => { _ = cancel.cancelled() => {
@@ -104,11 +131,13 @@ impl NativeTunRuntime {
} }
let magic_dns = if let Some(ip) = ipv4 { let magic_dns = if let Some(ip) = ipv4 {
let shared_route_backend = nic.shared_route_backend_for_dns();
MagicDnsRuntime::start( MagicDnsRuntime::start(
global_ctx.clone(), global_ctx.clone(),
packet_plane.clone(), packet_plane.clone(),
nic.ifname().await, nic.ifname().await,
ip, ip,
shared_route_backend,
) )
} else { } else {
MagicDnsRuntime::default() MagicDnsRuntime::default()
@@ -161,6 +190,7 @@ impl NativeTunRuntime {
nic: self.nic.clone(), nic: self.nic.clone(),
closed: Arc::new(Notify::new()), closed: Arc::new(Notify::new()),
packet_plane, packet_plane,
shared_virtual_nic_registry: self.shared_virtual_nic_registry.clone(),
}) })
} }
} }
@@ -172,6 +202,7 @@ struct NativeDhcpIpv4Host {
nic: TunNicState, nic: TunNicState,
closed: Arc<Notify>, closed: Arc<Notify>,
packet_plane: Arc<CorePacketPlane>, packet_plane: Arc<CorePacketPlane>,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
} }
impl NativeDhcpIpv4Host { impl NativeDhcpIpv4Host {
@@ -199,21 +230,25 @@ impl NativeDhcpIpv4Host {
return Ok(Some(ip)); return Ok(Some(ip));
} }
let mut nic = NicCtx::new( let mut nic = create_nic_ctx(
self.global_ctx.clone(), self.global_ctx.clone(),
self.packet_plane.clone(), self.packet_plane.clone(),
self.nic.receiver(), self.nic.receiver(),
self.closed.clone(), self.closed.clone(),
); self.shared_virtual_nic_registry.clone(),
)
.await?;
tokio::select! { tokio::select! {
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; DHCP update cancelled"), _ = self.cancel.cancelled() => anyhow::bail!("instance is closing; DHCP update cancelled"),
result = nic.run(Some(ip), self.global_ctx.get_ipv6()) => result?, result = nic.run(Some(ip), self.global_ctx.get_ipv6()) => result?,
} }
let shared_route_backend = nic.shared_route_backend_for_dns();
let magic_dns = MagicDnsRuntime::start( let magic_dns = MagicDnsRuntime::start(
self.global_ctx.clone(), self.global_ctx.clone(),
self.packet_plane.clone(), self.packet_plane.clone(),
nic.ifname().await, nic.ifname().await,
ip, ip,
shared_route_backend,
); );
self.nic.install(nic, magic_dns).await; self.nic.install(nic, magic_dns).await;
self.global_ctx.set_ipv4(Some(ip)); self.global_ctx.set_ipv4(Some(ip));
@@ -8,26 +8,40 @@ use easytier_core::{
instance::CorePacketPlane, instance::CorePacketPlane,
}; };
use futures::FutureExt as _; use futures::FutureExt as _;
use tokio::sync::{Mutex, Notify, mpsc}; use tokio::sync::{Mutex, Notify, mpsc, oneshot};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use super::{MagicDnsRuntime, tun_common::TunNicState}; use super::{
MagicDnsRuntime,
tun_common::{TunNicState, create_nic_ctx},
};
use crate::{ use crate::{
common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
instance::virtual_nic::NicCtx, instance::{shared_virtual_nic::ArcSharedVirtualNicRegistry, virtual_nic::MobileTunSources},
}; };
struct MobileTunAttachment {
fd: i32,
sources: MobileTunSources,
completion: Option<oneshot::Sender<anyhow::Result<()>>>,
}
pub(super) struct NativeTunRuntime { pub(super) struct NativeTunRuntime {
global_ctx: ArcGlobalCtx, global_ctx: ArcGlobalCtx,
cancel: CancellationToken, cancel: CancellationToken,
nic: TunNicState, nic: TunNicState,
tun_fd: mpsc::Sender<i32>, tun_fd: mpsc::Sender<MobileTunAttachment>,
tun_fd_receiver: Mutex<Option<mpsc::Receiver<i32>>>, tun_fd_receiver: Mutex<Option<mpsc::Receiver<MobileTunAttachment>>>,
task: Mutex<Option<tokio::task::JoinHandle<()>>>, task: Mutex<Option<tokio::task::JoinHandle<()>>>,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
} }
impl NativeTunRuntime { impl NativeTunRuntime {
pub(super) fn new(global_ctx: ArcGlobalCtx, cancel: CancellationToken) -> Self { pub(super) fn new(
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
) -> Self {
let (tun_fd, tun_fd_receiver) = mpsc::channel(16); let (tun_fd, tun_fd_receiver) = mpsc::channel(16);
Self { Self {
global_ctx, global_ctx,
@@ -36,6 +50,7 @@ impl NativeTunRuntime {
tun_fd, tun_fd,
tun_fd_receiver: Mutex::new(Some(tun_fd_receiver)), tun_fd_receiver: Mutex::new(Some(tun_fd_receiver)),
task: Mutex::new(None), task: Mutex::new(None),
shared_virtual_nic_registry,
} }
} }
@@ -51,23 +66,31 @@ impl NativeTunRuntime {
global_ctx: ArcGlobalCtx, global_ctx: ArcGlobalCtx,
packet_plane: Arc<CorePacketPlane>, packet_plane: Arc<CorePacketPlane>,
fd: i32, fd: i32,
sources: MobileTunSources,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
nic_state.drain().await; nic_state.drain().await;
if fd <= 0 { if fd <= 0 {
return Ok(()); return Ok(());
} }
let closed = Arc::new(Notify::new()); let closed = Arc::new(Notify::new());
let mut nic = NicCtx::new( let mut nic = create_nic_ctx(
global_ctx.clone(), global_ctx.clone(),
packet_plane.clone(), packet_plane.clone(),
nic_state.receiver(), nic_state.receiver(),
closed, closed,
); shared_virtual_nic_registry,
nic.run_for_mobile(fd).await.context("add ip failed")?; )
let magic_dns = global_ctx .await?;
.get_ipv4() nic.run_for_mobile(fd, sources)
.map(|ip| MagicDnsRuntime::start(global_ctx, packet_plane, None, ip)) .await
.unwrap_or_default(); .context("add ip failed")?;
let magic_dns = if let Some(ip) = global_ctx.get_ipv4() {
let shared_route_backend = nic.shared_route_backend_for_dns();
MagicDnsRuntime::start(global_ctx, packet_plane, None, ip, shared_route_backend)
} else {
MagicDnsRuntime::default()
};
nic_state.install(nic, magic_dns).await; nic_state.install(nic, magic_dns).await;
Ok(()) Ok(())
} }
@@ -80,28 +103,43 @@ impl NativeTunRuntime {
let nic_state = self.nic.clone(); let nic_state = self.nic.clone();
let global_ctx = self.global_ctx.clone(); let global_ctx = self.global_ctx.clone();
let cancel = self.cancel.clone(); let cancel = self.cancel.clone();
let shared_virtual_nic_registry = self.shared_virtual_nic_registry.clone();
self.task.lock().await.replace(tokio::spawn(async move { self.task.lock().await.replace(tokio::spawn(async move {
loop { loop {
let fd = tokio::select! { let attachment = tokio::select! {
_ = cancel.cancelled() => return, _ = cancel.cancelled() => return,
fd = tun_fds.recv() => match fd { Some(fd) => fd, None => return }, attachment = tun_fds.recv() => match attachment {
Some(attachment) => attachment,
None => return,
},
}; };
if let Err(error) = Self::install_mobile_tun( let result = if attachment.fd <= 0 {
nic_state.drain().await;
Ok(())
} else {
Self::install_mobile_tun(
nic_state.clone(), nic_state.clone(),
global_ctx.clone(), global_ctx.clone(),
packet_plane.clone(), packet_plane.clone(),
fd, attachment.fd,
attachment.sources,
shared_virtual_nic_registry.clone(),
) )
.await .await
{ };
if let Err(error) = &result {
tracing::error!(?error, "failed to attach mobile TUN fd"); tracing::error!(?error, "failed to attach mobile TUN fd");
} }
if let Some(completion) = attachment.completion {
let _ = completion.send(result);
}
} }
})); }));
Ok(()) Ok(())
} }
pub(super) async fn shutdown(&self) { pub(super) async fn shutdown(&self) {
self.cancel.cancel();
if let Some(task) = self.task.lock().await.take() { if let Some(task) = self.task.lock().await.take() {
let _ = task.await; let _ = task.await;
} }
@@ -109,11 +147,46 @@ impl NativeTunRuntime {
} }
pub(super) fn attach_fd(&self, fd: i32) -> anyhow::Result<()> { pub(super) fn attach_fd(&self, fd: i32) -> anyhow::Result<()> {
if !self.global_ctx.get_flags().dev_name.is_empty() {
anyhow::bail!(
"shared mobile TUN attachment requires per-instance address and route sources"
);
}
self.tun_fd self.tun_fd
.try_send(fd) .try_send(MobileTunAttachment {
fd,
sources: MobileTunSources::default(),
completion: None,
})
.map_err(|error| anyhow::anyhow!("failed to send TUN fd: {error}")) .map_err(|error| anyhow::anyhow!("failed to send TUN fd: {error}"))
} }
pub(super) async fn attach_mobile_fd(
&self,
fd: i32,
sources: MobileTunSources,
) -> anyhow::Result<()> {
if self.task.lock().await.is_none() {
anyhow::bail!("mobile TUN runtime is not running");
}
let (completion, result) = oneshot::channel();
tokio::select! {
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; TUN attachment cancelled"),
send_result = self.tun_fd.send(MobileTunAttachment {
fd,
sources,
completion: Some(completion),
}) => send_result.map_err(|error| anyhow::anyhow!("failed to send TUN fd: {error}"))?,
}
tokio::select! {
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; TUN attachment cancelled"),
result = result => result
.map_err(|_| anyhow::anyhow!("mobile TUN runtime stopped before attachment completed"))?,
}
}
pub(super) fn dhcp_host( pub(super) fn dhcp_host(
&self, &self,
operation: Arc<Mutex<()>>, operation: Arc<Mutex<()>>,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+39 -4
View File
@@ -7,6 +7,8 @@ use easytier_core::{
process_runtime::CoreProcessRuntime, process_runtime::CoreProcessRuntime,
}; };
#[cfg(feature = "tun")]
use crate::instance::shared_virtual_nic::{ArcSharedVirtualNicRegistry, SharedVirtualNicRegistry};
use crate::{ use crate::{
common::global_ctx::{ArcGlobalCtx, GlobalCtx}, common::global_ctx::{ArcGlobalCtx, GlobalCtx},
instance::{ instance::{
@@ -15,6 +17,8 @@ use crate::{
}, },
socket::udp::RuntimeUdpSocket, socket::udp::RuntimeUdpSocket,
}; };
#[cfg(feature = "tun")]
use tokio::sync::Mutex;
pub(crate) struct TestInstance { pub(crate) struct TestInstance {
core: Arc<NativeCoreInstance>, core: Arc<NativeCoreInstance>,
@@ -26,7 +30,27 @@ impl TestInstance {
config: TomlConfig, config: TomlConfig,
process_runtime: Arc<CoreProcessRuntime>, process_runtime: Arc<CoreProcessRuntime>,
) -> Self { ) -> Self {
Self::compose(config, process_runtime, |_| {}) Self::compose(
config,
process_runtime,
#[cfg(feature = "tun")]
Arc::new(Mutex::new(SharedVirtualNicRegistry::new())),
|_| {},
)
}
#[cfg(feature = "tun")]
pub fn new_with_process_runtime_and_shared_virtual_nic_registry(
config: TomlConfig,
process_runtime: Arc<CoreProcessRuntime>,
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
) -> Self {
Self::compose(config, process_runtime, shared_virtual_nic_registry, |_| {})
}
#[cfg(feature = "tun")]
pub fn new_shared_virtual_nic_registry() -> ArcSharedVirtualNicRegistry {
Arc::new(Mutex::new(SharedVirtualNicRegistry::new()))
} }
pub fn new_with_process_runtime_and_stun_provider( pub fn new_with_process_runtime_and_stun_provider(
@@ -35,14 +59,21 @@ impl TestInstance {
provider: Box<dyn StunSocketMapper<RuntimeUdpSocket>>, provider: Box<dyn StunSocketMapper<RuntimeUdpSocket>>,
) -> Self { ) -> Self {
let provider: Arc<dyn StunSocketMapper<RuntimeUdpSocket>> = Arc::from(provider); let provider: Arc<dyn StunSocketMapper<RuntimeUdpSocket>> = Arc::from(provider);
Self::compose(config, process_runtime, move |adapters| { Self::compose(
config,
process_runtime,
#[cfg(feature = "tun")]
Arc::new(Mutex::new(SharedVirtualNicRegistry::new())),
move |adapters| {
adapters.replace_stun_provider(provider); adapters.replace_stun_provider(provider);
}) },
)
} }
fn compose( fn compose(
config: TomlConfig, config: TomlConfig,
process_runtime: Arc<CoreProcessRuntime>, process_runtime: Arc<CoreProcessRuntime>,
#[cfg(feature = "tun")] shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
customize: impl FnOnce( customize: impl FnOnce(
&mut easytier_core::instance::CoreHostAdapters< &mut easytier_core::instance::CoreHostAdapters<
crate::instance::host::NativeInstanceHost, crate::instance::host::NativeInstanceHost,
@@ -50,7 +81,11 @@ impl TestInstance {
), ),
) -> Self { ) -> Self {
let global_ctx = Arc::new(GlobalCtx::new(config.clone())); let global_ctx = Arc::new(GlobalCtx::new(config.clone()));
let runtime_host = NativeInstanceRuntimeHost::new(global_ctx.clone()); let runtime_host = NativeInstanceRuntimeHost::new(
global_ctx.clone(),
#[cfg(feature = "tun")]
shared_virtual_nic_registry,
);
let mut adapters = runtime_core_host_adapters_with_packet_egress( let mut adapters = runtime_core_host_adapters_with_packet_egress(
global_ctx.clone(), global_ctx.clone(),
process_runtime, process_runtime,
File diff suppressed because it is too large Load Diff
+3
View File
@@ -1,6 +1,9 @@
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod three_node; mod three_node;
#[cfg(all(target_os = "linux", feature = "tun"))]
mod shared_virtual_nic;
mod ipv6_test; mod ipv6_test;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
+476
View File
@@ -0,0 +1,476 @@
use std::{net::Ipv4Addr, process::Command, sync::Arc, time::Duration};
use easytier_core::{config::PeerId, process_runtime::CoreProcessRuntime};
use super::{
InstanceTestExt as _, add_ns_to_bridge, create_netns, del_netns, drop_insts, ping_test,
prepare_bridge,
};
use crate::{
common::{
config::{ConfigLoader, NetworkIdentity, TomlConfigLoader},
netns::{NetNS, ROOT_NETNS_NAME},
},
instance::{
shared_virtual_nic::{ArcSharedVirtualNicRegistry, SharedIpv4Route},
test_instance::TestInstance as Instance,
},
tunnel::common::tests::wait_for_condition,
};
const PROXY_CIDR: &str = "10.1.2.0/24";
const WAIT: Duration = Duration::from_secs(10);
#[derive(Clone)]
struct SharedTestRuntime {
process: Arc<CoreProcessRuntime>,
registry: ArcSharedVirtualNicRegistry,
}
impl SharedTestRuntime {
fn new() -> Self {
Self {
process: CoreProcessRuntime::new(),
registry: Instance::new_shared_virtual_nic_registry(),
}
}
fn instance(&self, config: TomlConfigLoader) -> Instance {
Instance::new_with_process_runtime_and_shared_virtual_nic_registry(
config,
self.process.clone(),
self.registry.clone(),
)
}
}
fn test_config(
instance_name: &str,
network_name: &str,
network_secret: &str,
netns: Option<&str>,
dev_name: Option<&str>,
ipv4: &str,
) -> TomlConfigLoader {
let config = TomlConfigLoader::default();
config.set_inst_name(instance_name.to_owned());
config.set_network_identity(NetworkIdentity::new(
network_name.to_owned(),
network_secret.to_owned(),
));
config.set_netns(netns.map(str::to_owned));
config.set_ipv4(Some(ipv4.parse().unwrap()));
config.set_ipv6(None);
config.set_dhcp(false);
config.set_listeners(vec![]);
config.set_socks5_portal(None);
let mut flags = config.get_flags();
flags.dev_name = dev_name.unwrap_or_default().to_owned();
flags.enable_ipv6 = false;
config.set_flags(flags);
config
}
fn test_dev_name() -> String {
format!("st{:08x}", rand::random::<u32>())
}
fn short_name(prefix: &str) -> String {
format!("{prefix}{:04x}", rand::random::<u16>())
}
struct TestNetnsGuard {
name: String,
}
impl TestNetnsGuard {
fn new(name: String, ipv4: &str, ipv6: &str) -> Self {
let guard = Self { name };
del_netns(&guard.name);
create_netns(&guard.name, ipv4, ipv6);
guard
}
}
impl Drop for TestNetnsGuard {
fn drop(&mut self) {
del_netns(&self.name);
}
}
struct ProxyLab {
source_ns: String,
owner_ns: String,
target_ns: String,
bridge: String,
}
impl ProxyLab {
fn new() -> Self {
let suffix = format!("{:04x}", rand::random::<u16>());
let lab = Self {
source_ns: format!("svs{suffix}"),
owner_ns: format!("svo{suffix}"),
target_ns: format!("svt{suffix}"),
bridge: format!("svb{suffix}"),
};
lab.cleanup();
create_netns(&lab.source_ns, "10.1.1.1/24", "fd11::1/64");
create_netns(&lab.owner_ns, "10.1.2.3/24", "fd12::3/64");
create_netns(&lab.target_ns, "10.1.2.4/24", "fd12::4/64");
prepare_bridge(&lab.bridge);
add_ns_to_bridge(&lab.bridge, &lab.owner_ns);
add_ns_to_bridge(&lab.bridge, &lab.target_ns);
lab
}
fn cleanup(&self) {
del_netns(&self.source_ns);
del_netns(&self.owner_ns);
del_netns(&self.target_ns);
let _ = Command::new("ip")
.args(["link", "del", &self.bridge])
.output();
}
}
impl Drop for ProxyLab {
fn drop(&mut self) {
self.cleanup();
}
}
async fn wait_tun_ready(instance: &Instance, expected: &str) {
wait_for_condition(
|| async { instance.get_global_ctx().get_tun_device_name().as_deref() == Some(expected) },
WAIT,
)
.await;
}
fn proxy_route_exists(
routes: &[easytier_proto::core_peer::peer::Route],
peer_id: PeerId,
proxy_cidr: &str,
) -> bool {
routes
.iter()
.any(|route| route.peer_id == peer_id && route.proxy_cidrs.iter().any(|c| c == proxy_cidr))
}
async fn wait_proxy_route(instance: &Instance, peer_id: PeerId, proxy_cidr: &str) {
wait_for_condition(
|| async {
proxy_route_exists(
&instance.get_core_instance().route_snapshots().await,
peer_id,
proxy_cidr,
)
},
WAIT,
)
.await;
}
async fn wait_proxy_route_absent(instance: &Instance, peer_id: PeerId, proxy_cidr: &str) {
wait_for_condition(
|| async {
!proxy_route_exists(
&instance.get_core_instance().route_snapshots().await,
peer_id,
proxy_cidr,
)
},
WAIT,
)
.await;
}
fn ipv4_route_exists_in_ns(ns: &str, needle: &str) -> bool {
let _root = NetNS::new(Some(ROOT_NETNS_NAME.to_owned())).guard();
let output = Command::new("ip")
.args(["netns", "exec", ns, "ip", "route", "show"])
.output()
.unwrap();
assert!(
output.status.success(),
"failed to list IPv4 routes in {ns}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout)
.lines()
.any(|line| line.contains(needle))
}
#[cfg(feature = "proxy-cidr-monitor")]
async fn patch_proxy_cidr(
instance: &Instance,
action: crate::proto::api::config::ConfigPatchAction,
) {
use crate::proto::api::config::{InstanceConfigPatch, ProxyNetworkPatch};
instance
.get_config_patcher()
.apply_patch(InstanceConfigPatch {
proxy_networks: vec![ProxyNetworkPatch {
action: action as i32,
cidr: Some(PROXY_CIDR.parse().unwrap()),
mapped_cidr: None,
}],
..Default::default()
})
.await
.unwrap();
}
async fn shared_route_owner_count(
registry: &ArcSharedVirtualNicRegistry,
dev_name: &str,
route: &SharedIpv4Route,
) -> usize {
let nic = {
let registry = registry.lock().await;
registry.get_by_dev_name_for_test(dev_name)
};
let Some(nic) = nic else {
return 0;
};
nic.lock().await.ifcfg().owners_of_ipv4_route(route).len()
}
#[tokio::test]
#[serial_test::serial]
async fn same_namespace_members_share_tun_across_independent_networks() {
let dev_name = test_dev_name();
let first_peer_ns = TestNetnsGuard::new(short_name("sva"), "10.231.1.2/24", "fd31::2/64");
let second_peer_ns = TestNetnsGuard::new(short_name("svb"), "10.231.2.2/24", "fd32::2/64");
let runtime = SharedTestRuntime::new();
let mut first = runtime.instance(test_config(
"shared_tun_first",
"shared_tun_network_a",
"shared_tun_secret_a",
None,
Some(&dev_name),
"10.144.250.1/24",
));
let mut second = runtime.instance(test_config(
"shared_tun_second",
"shared_tun_network_b",
"shared_tun_secret_b",
None,
Some(&dev_name),
"10.144.251.1/24",
));
let mut first_peer = runtime.instance(test_config(
"shared_tun_first_peer",
"shared_tun_network_a",
"shared_tun_secret_a",
Some(&first_peer_ns.name),
None,
"10.144.250.2/24",
));
let mut second_peer = runtime.instance(test_config(
"shared_tun_second_peer",
"shared_tun_network_b",
"shared_tun_secret_b",
Some(&second_peer_ns.name),
None,
"10.144.251.2/24",
));
first.run().await.unwrap();
second.run().await.unwrap();
first_peer.run().await.unwrap();
second_peer.run().await.unwrap();
wait_tun_ready(&first, &dev_name).await;
wait_tun_ready(&second, &dev_name).await;
assert_eq!(
first.get_global_ctx().get_tun_device_name(),
second.get_global_ctx().get_tun_device_name()
);
first_peer.add_connector_url(first.ring_listener_url());
second_peer.add_connector_url(second.ring_listener_url());
wait_for_condition(
|| async {
first
.get_core_instance()
.route_snapshots()
.await
.iter()
.any(|route| route.peer_id == first_peer.peer_id())
&& second
.get_core_instance()
.route_snapshots()
.await
.iter()
.any(|route| route.peer_id == second_peer.peer_id())
},
WAIT,
)
.await;
wait_for_condition(
|| async { ping_test(&first_peer_ns.name, "10.144.250.1", None).await },
WAIT,
)
.await;
wait_for_condition(
|| async { ping_test(&second_peer_ns.name, "10.144.251.1", None).await },
WAIT,
)
.await;
drop_insts(vec![first, second, first_peer, second_peer]).await;
}
#[cfg(feature = "proxy-cidr-monitor")]
#[tokio::test]
#[serial_test::serial]
async fn runtime_proxy_patch_adds_and_removes_os_route() {
use crate::proto::api::config::ConfigPatchAction;
let lab = ProxyLab::new();
let source_dev = test_dev_name();
let destination_dev = test_dev_name();
let runtime = SharedTestRuntime::new();
let mut source = runtime.instance(test_config(
"shared_patch_source",
"shared_patch_network",
"shared_patch_secret",
Some(&lab.source_ns),
Some(&source_dev),
"10.144.244.1/24",
));
let mut destination = runtime.instance(test_config(
"shared_patch_destination",
"shared_patch_network",
"shared_patch_secret",
Some(&lab.owner_ns),
Some(&destination_dev),
"10.144.244.2/24",
));
source.run().await.unwrap();
destination.run().await.unwrap();
wait_tun_ready(&source, &source_dev).await;
wait_tun_ready(&destination, &destination_dev).await;
destination.add_connector_url(source.ring_listener_url());
wait_for_condition(
|| async {
source
.get_core_instance()
.route_snapshots()
.await
.iter()
.any(|route| route.peer_id == destination.peer_id())
},
WAIT,
)
.await;
assert!(!ipv4_route_exists_in_ns(
&lab.source_ns,
&format!("{PROXY_CIDR} dev {source_dev}")
));
patch_proxy_cidr(&destination, ConfigPatchAction::Add).await;
wait_proxy_route(&source, destination.peer_id(), PROXY_CIDR).await;
wait_for_condition(
|| async {
ipv4_route_exists_in_ns(&lab.source_ns, &format!("{PROXY_CIDR} dev {source_dev}"))
},
WAIT,
)
.await;
patch_proxy_cidr(&destination, ConfigPatchAction::Remove).await;
wait_proxy_route_absent(&source, destination.peer_id(), PROXY_CIDR).await;
wait_for_condition(
|| async {
!ipv4_route_exists_in_ns(&lab.source_ns, &format!("{PROXY_CIDR} dev {source_dev}"))
},
WAIT,
)
.await;
drop_insts(vec![source, destination]).await;
}
#[cfg(feature = "magic-dns")]
#[tokio::test]
#[serial_test::serial]
async fn magic_dns_route_lives_until_last_shared_owner_leaves() {
use crate::instance::dns_server::MAGIC_DNS_FAKE_IP;
let netns = TestNetnsGuard::new(short_name("svd"), "10.232.1.2/24", "fd42::2/64");
let dev_name = test_dev_name();
let runtime = SharedTestRuntime::new();
let first_config = test_config(
"shared_dns_first",
"shared_dns_network",
"shared_dns_secret",
Some(&netns.name),
Some(&dev_name),
"10.144.243.1/24",
);
let mut flags = first_config.get_flags();
flags.accept_dns = true;
first_config.set_flags(flags.clone());
let second_config = test_config(
"shared_dns_second",
"shared_dns_network",
"shared_dns_secret",
Some(&netns.name),
Some(&dev_name),
"10.144.242.2/24",
);
second_config.set_flags(flags);
let mut first = runtime.instance(first_config);
let mut second = runtime.instance(second_config);
first.run().await.unwrap();
second.run().await.unwrap();
wait_tun_ready(&first, &dev_name).await;
wait_tun_ready(&second, &dev_name).await;
let route = SharedIpv4Route::new(MAGIC_DNS_FAKE_IP.parse::<Ipv4Addr>().unwrap(), 32, None);
wait_for_condition(
|| async { shared_route_owner_count(&runtime.registry, &dev_name, &route).await == 2 },
WAIT,
)
.await;
assert!(ipv4_route_exists_in_ns(
&netns.name,
&format!("{MAGIC_DNS_FAKE_IP} dev {dev_name}")
));
drop_insts(vec![first]).await;
wait_for_condition(
|| async {
shared_route_owner_count(&runtime.registry, &dev_name, &route).await == 1
&& ipv4_route_exists_in_ns(
&netns.name,
&format!("{MAGIC_DNS_FAKE_IP} dev {dev_name}"),
)
},
WAIT,
)
.await;
drop_insts(vec![second]).await;
wait_for_condition(
|| async {
shared_route_owner_count(&runtime.registry, &dev_name, &route).await == 0
&& !ipv4_route_exists_in_ns(
&netns.name,
&format!("{MAGIC_DNS_FAKE_IP} dev {dev_name}"),
)
},
WAIT,
)
.await;
}
+151
View File
@@ -1017,6 +1017,157 @@ pub async fn public_ipv6_auto_addr_reconnect_reuses_same_address() {
drop_insts(vec![provider, client]).await; drop_insts(vec![provider, client]).await;
} }
#[cfg(feature = "tun")]
#[tokio::test]
#[serial_test::serial]
pub async fn shared_tun_public_ipv6_auto_addr_end_to_end() {
let lab = PublicIpv6Lab::setup_with_topology(PublicIpv6LabTopology::DelegatedPrefix);
let provider_dev = format!("st{:08x}", rand::random::<u32>());
let client_dev = format!("st{:08x}", rand::random::<u32>());
let process_runtime = CoreProcessRuntime::new();
let shared_virtual_nic_registry = Instance::new_shared_virtual_nic_registry();
let provider_cfg = get_public_ipv6_config(
"provider_shared_public_ipv6",
PublicIpv6Lab::PROVIDER_NS,
"10.144.144.1",
&provider_dev,
uuid::Uuid::parse_str("44444444-4444-4444-4444-444444444444").unwrap(),
);
provider_cfg.set_ipv6_public_addr_provider(true);
let client_cfg = get_public_ipv6_config(
"client_shared_public_ipv6",
PublicIpv6Lab::CLIENT_NS,
"10.144.144.2",
&client_dev,
uuid::Uuid::parse_str("55555555-5555-5555-5555-555555555555").unwrap(),
);
client_cfg.set_ipv6_public_addr_auto(true);
let client_peer_cfg = get_public_ipv6_config(
"client_shared_public_ipv6_peer",
PublicIpv6Lab::CLIENT_NS,
"10.144.145.3",
&client_dev,
uuid::Uuid::parse_str("66666666-6666-6666-6666-666666666666").unwrap(),
);
client_peer_cfg.set_listeners(vec![]);
let mut provider = Instance::new_with_process_runtime_and_shared_virtual_nic_registry(
provider_cfg,
process_runtime.clone(),
shared_virtual_nic_registry.clone(),
);
let mut client = Instance::new_with_process_runtime_and_shared_virtual_nic_registry(
client_cfg,
process_runtime.clone(),
shared_virtual_nic_registry.clone(),
);
let mut client_peer = Instance::new_with_process_runtime_and_shared_virtual_nic_registry(
client_peer_cfg,
process_runtime,
shared_virtual_nic_registry,
);
let mut client_events = client.get_global_ctx().subscribe();
let mut client_peer_events = client_peer.get_global_ctx().subscribe();
provider.run().await.unwrap();
client.run().await.unwrap();
client_peer.run().await.unwrap();
let shared_ifname = wait_for_tun_ready(&mut client_events).await;
assert_eq!(
shared_ifname,
wait_for_tun_ready(&mut client_peer_events).await
);
assert_eq!(shared_ifname, client_dev);
provider.add_connector_url("tcp://10.1.1.2:11010".parse().unwrap());
wait_for_condition(
|| async {
provider.get_core_instance().route_snapshots().await.len() == 1
&& client.get_core_instance().route_snapshots().await.len() == 1
},
Duration::from_secs(8),
)
.await;
wait_for_condition(
|| async {
provider
.get_core_instance()
.node_snapshot()
.await
.ipv6_public_addr_prefix
== Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
},
Duration::from_secs(10),
)
.await;
let leased = wait_for_public_ipv6_addr(&client).await;
wait_for_public_ipv6_route(&provider, leased).await;
wait_for_condition(
|| async {
addr_exists_in_ns(PublicIpv6Lab::CLIENT_NS, &client_dev, &leased.to_string())
&& route_exists_in_ns(
PublicIpv6Lab::CLIENT_NS,
&format!("default dev {client_dev}"),
)
&& route_exists_in_ns(
PublicIpv6Lab::PROVIDER_NS,
&format!("{} dev {provider_dev}", leased.address()),
)
},
Duration::from_secs(10),
)
.await;
wait_for_condition(
|| async { ping6_test(PublicIpv6Lab::CLIENT_NS, PublicIpv6Lab::SERVER_IP, None).await },
Duration::from_secs(10),
)
.await;
wait_for_condition(
|| async {
ping6_test(
PublicIpv6Lab::SERVER_NS,
leased.address().to_string().as_str(),
None,
)
.await
},
Duration::from_secs(10),
)
.await;
drop_insts(vec![provider, client, client_peer]).await;
drop(lab);
}
#[cfg(feature = "tun")]
async fn wait_for_tun_ready(
receiver: &mut tokio::sync::broadcast::Receiver<crate::common::global_ctx::GlobalCtxEvent>,
) -> String {
tokio::time::timeout(Duration::from_secs(5), async {
loop {
match receiver.recv().await.unwrap() {
crate::common::global_ctx::GlobalCtxEvent::TunDeviceReady(ifname) => return ifname,
crate::common::global_ctx::GlobalCtxEvent::TunDeviceError(error) => {
panic!("tun device error: {error}")
}
_ => {}
}
}
})
.await
.expect("timed out waiting for tun ready")
}
#[rstest::rstest] #[rstest::rstest]
#[tokio::test] #[tokio::test]
#[serial_test::serial] #[serial_test::serial]
@@ -10,6 +10,7 @@ import android.os.ParcelFileDescriptor
import android.os.Bundle import android.os.Bundle
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import android.system.OsConstants.AF_INET6
import java.net.InetAddress import java.net.InetAddress
import java.util.Arrays import java.util.Arrays
@@ -20,10 +21,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"
@@ -39,7 +42,8 @@ class TauriVpnService : VpnService() {
println("vpn on start command ${intent?.getExtras()} $intent") println("vpn on start command ${intent?.getExtras()} $intent")
startVpnForegroundService() startVpnForegroundService()
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)
@@ -90,6 +94,7 @@ class TauriVpnService : VpnService() {
private fun clearStatus() { private fun clearStatus() {
ipv4Addr = null ipv4Addr = null
ipv4Addrs = emptyArray()
routes = emptyArray() routes = emptyArray()
dns = null 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 { 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.allowFamily(AF_INET6)
builder.setMtu(mtu) builder.setMtu(mtu)
dns?.let { builder.addDnsServer(it) } dns?.let { builder.addDnsServer(it) }
@@ -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()
@@ -106,6 +108,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)
@@ -137,7 +140,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;
} }
+2
View File
@@ -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>,
} }