mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-23 19:32:00 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a661829886 | ||
|
|
d3bf71c259 |
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
.map(|id| {
|
||||||
|
id.parse::<uuid::Uuid>()
|
||||||
|
.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(())
|
||||||
|
} 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"))]
|
||||||
{
|
{
|
||||||
instance_manager
|
for instance_id in &target_ids {
|
||||||
.attach_tun_fd(uuid, fd)
|
if instance_manager.instance(*instance_id).is_none() {
|
||||||
.map_err(|e| e.to_string())?;
|
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("; ")
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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_stop", "")
|
app.emit("vpn_service_config_changed", instance_id.to_string())
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.emit("vpn_service_stop", "")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
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(
|
||||||
.await
|
app,
|
||||||
.map_err(|e| e.to_string())?;
|
false,
|
||||||
|
shared_dev_name.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
PersistedConfigSource::Web => {
|
PersistedConfigSource::Web => {
|
||||||
self.disable_instances_with_tun(app, true)
|
self.disable_incompatible_instances_with_tun(
|
||||||
.await
|
app,
|
||||||
.map_err(|e| e.to_string())?;
|
true,
|
||||||
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
shared_dev_name.as_deref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if !self
|
||||||
|
.enabled_incompatible_tun_ids(false, shared_dev_name.as_deref())
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
return Err(
|
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!({
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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,13 +356,98 @@ 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) {
|
||||||
console.warn('vpn service owner changed', activeVpnInstanceId, instanceId)
|
let sameSharedDevice = curVpnStatus.instanceIds.includes(instanceId)
|
||||||
await doStopVpn()
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
let curNetworkInfo
|
const ipv4Addrs: string[] = []
|
||||||
try {
|
const instanceSources: TunFdInstanceSources[] = []
|
||||||
curNetworkInfo = (await collectNetworkInfo(instanceId))?.info?.map?.[instanceId]
|
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'
|
||||||
}
|
}
|
||||||
catch (e) {
|
|
||||||
console.warn('vpn service network info query failed', instanceId, e)
|
for (const { instanceId: memberId, config } of group) {
|
||||||
scheduleVpnReconcile(instanceId, generation, 'network_info_query_failed')
|
let curNetworkInfo
|
||||||
return
|
try {
|
||||||
|
curNetworkInfo = getCollectedNetworkInfo(await collectNetworkInfo(memberId), memberId)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.warn('vpn service network info query failed', memberId, error)
|
||||||
|
retryReason ??= 'network_info_query_failed'
|
||||||
|
retainCurrentSource(memberId, config.enable_magic_dns)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!curNetworkInfo) {
|
||||||
|
console.warn('vpn service network info unavailable', memberId, curNetworkInfo?.error_msg)
|
||||||
|
retryReason ??= 'network_info_unavailable'
|
||||||
|
retainCurrentSource(memberId, config.enable_magic_dns)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curNetworkInfo.error_msg?.length) {
|
||||||
|
console.warn('vpn service network failed', memberId, curNetworkInfo.error_msg)
|
||||||
|
retryReason ??= 'network_failed'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4
|
||||||
|
const virtualIp = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined
|
||||||
|
if (!virtualIp) {
|
||||||
|
retryReason ??= config.dhcp ? 'dhcp_ipv4_unavailable' : 'static_ipv4_unavailable'
|
||||||
|
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))
|
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||||
return
|
return
|
||||||
|
|
||||||
if (!curNetworkInfo) {
|
if (!ipv4Addrs.length) {
|
||||||
scheduleVpnReconcile(instanceId, generation, 'network_info_unavailable')
|
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()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (curNetworkInfo.error_msg?.length) {
|
if (retryReason)
|
||||||
console.warn('vpn service skipped because network instance failed', instanceId, curNetworkInfo.error_msg)
|
scheduleVpnReconcile(instanceId, generation, retryReason)
|
||||||
|
else
|
||||||
vpnReconcileAttempts = 0
|
vpnReconcileAttempts = 0
|
||||||
await doStopVpn()
|
const sortedIpv4Addrs = [...ipv4Addrs].sort()
|
||||||
return
|
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))
|
||||||
|
|
||||||
const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4
|
if (!curVpnStatus.running || configChanged) {
|
||||||
const virtual_ip = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined
|
|
||||||
|
|
||||||
if (!virtual_ip || !virtual_ip.length) {
|
|
||||||
scheduleVpnReconcile(
|
|
||||||
instanceId,
|
|
||||||
generation,
|
|
||||||
config.dhcp ? 'dhcp_ipv4_unavailable' : 'static_ipv4_unavailable',
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vpnReconcileAttempts = 0
|
|
||||||
|
|
||||||
let network_length = virtualIpv4?.network_length
|
|
||||||
if (!network_length) {
|
|
||||||
network_length = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = getRoutesForVpn(curNetworkInfo?.routes, config)
|
|
||||||
|
|
||||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined
|
|
||||||
|
|
||||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
|
||||||
const cidrChanged = network_length !== curVpnStatus.ipv4Cidr
|
|
||||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
|
||||||
const dnsChanged = dns != curVpnStatus.dns
|
|
||||||
const configChanged = ipChanged || cidrChanged || routesChanged || dnsChanged
|
|
||||||
const shouldStartVpn = !curVpnStatus.running
|
|
||||||
|
|
||||||
if (shouldStartVpn || configChanged) {
|
|
||||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
|
||||||
if (curVpnStatus.running) {
|
if (curVpnStatus.running) {
|
||||||
try {
|
try {
|
||||||
await doStopVpn()
|
await doStopVpn()
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (error) {
|
||||||
console.error(e)
|
console.error('stop vpn service failed', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isCurrentVpnReconcile(instanceId, generation))
|
||||||
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!isCurrentVpnReconcile(instanceId, generation))
|
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns, sortedInstanceIds, sortedInstanceSources)
|
||||||
return
|
if (!isCurrentVpnReconcile(instanceId, generation) && activeVpnInstanceId === sortedInstanceIds[0])
|
||||||
|
|
||||||
await doStartVpn(instanceId, virtual_ip, network_length, routes, dns)
|
|
||||||
if (!isCurrentVpnReconcile(instanceId, generation) && activeVpnInstanceId === instanceId) {
|
|
||||||
await doStopVpn()
|
await doStopVpn()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (error) {
|
||||||
if (e instanceof Error && e.message === 'need_prepare') {
|
if (error instanceof Error && error.message === 'vpn_permission_denied') {
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,11 +122,33 @@ 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())
|
||||||
|
.await?;
|
||||||
|
if let Some(addresses) = configured_ipv4.get_mut(name) {
|
||||||
|
addresses.remove(&ip);
|
||||||
|
if addresses.is_empty() {
|
||||||
|
configured_ipv4.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await
|
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> {
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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![
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,17 +105,53 @@ 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() {
|
||||||
self.packet_plane.clone(),
|
MagicDnsServerInstance::new_with_external_fake_ip_route(
|
||||||
self.global_ctx.clone(),
|
self.packet_plane.clone(),
|
||||||
self.tun_dev.clone(),
|
self.global_ctx.clone(),
|
||||||
self.tun_inet,
|
self.tun_dev.clone(),
|
||||||
self.fake_ip,
|
self.tun_inet,
|
||||||
)
|
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)
|
}
|
||||||
&& let Some(tun_dev_name) = &self.data.tun_dev
|
|
||||||
{
|
if self.data.manage_fake_ip_route
|
||||||
let ifcfg = IfConfiger {};
|
&& !self.tun_inet.contains(&self.data.fake_ip)
|
||||||
let _ = ifcfg
|
&& let Some(tun_dev_name) = &self.data.tun_dev
|
||||||
.remove_ipv4_route(tun_dev_name, self.data.fake_ip, 32)
|
{
|
||||||
.await;
|
Self::remove_fake_ip_route(tun_dev_name, self.data.fake_ip, &self.data.net_ns).await;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.packet_filter.close().await;
|
self.packet_filter.close().await;
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.clone(),
|
nic_state.drain().await;
|
||||||
global_ctx.clone(),
|
Ok(())
|
||||||
packet_plane.clone(),
|
} else {
|
||||||
fd,
|
Self::install_mobile_tun(
|
||||||
)
|
nic_state.clone(),
|
||||||
.await
|
global_ctx.clone(),
|
||||||
{
|
packet_plane.clone(),
|
||||||
|
attachment.fd,
|
||||||
|
attachment.sources,
|
||||||
|
shared_virtual_nic_registry.clone(),
|
||||||
|
)
|
||||||
|
.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
@@ -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(
|
||||||
adapters.replace_stun_provider(provider);
|
config,
|
||||||
})
|
process_runtime,
|
||||||
|
#[cfg(feature = "tun")]
|
||||||
|
Arc::new(Mutex::new(SharedVirtualNicRegistry::new())),
|
||||||
|
move |adapters| {
|
||||||
|
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
@@ -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")]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)}")
|
||||||
|
|
||||||
val ipParts = ipv4Addr.split("/")
|
for (ipv4Addr in ipv4Addrs) {
|
||||||
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
val ipParts = ipv4Addr.split("/")
|
||||||
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
||||||
builder.addAddress("fd00::1", 128)
|
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user