mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 11:22:39 +00:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f2f019f16 | ||
|
|
86bf3df0fd | ||
|
|
daf30bff6e | ||
|
|
b5bc4a853d | ||
|
|
144e306562 | ||
|
|
ba86e47cc6 | ||
|
|
d742fa34aa | ||
|
|
800c840bb5 | ||
|
|
ca17e856a6 | ||
|
|
d87624b353 | ||
|
|
a7ab60e0e4 | ||
|
|
3d0d2bed9e | ||
|
|
7ef023fcdd | ||
|
|
69d476ad13 | ||
|
|
cf3fcb75b5 | ||
|
|
17195a5cc4 | ||
|
|
22bcfebe3e | ||
|
|
47a2fb19a6 | ||
|
|
62e43855cb | ||
|
|
5efbf4853a | ||
|
|
3e0bde68b8 | ||
|
|
bda06ebb61 | ||
|
|
be0859aca6 | ||
|
|
b2f1b37336 | ||
|
|
583fb45939 | ||
|
|
a343a907a9 | ||
|
|
b1ac64f40f | ||
|
|
122fb9b54e | ||
|
|
09780c1698 | ||
|
|
e533b48244 | ||
|
|
c0bd050742 |
@@ -58,7 +58,7 @@ jobs:
|
||||
easytier-web/frontend/dist/*
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
|
||||
Generated
+34
@@ -2357,6 +2357,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"pnet_datalink",
|
||||
"pnet_packet",
|
||||
"prost 0.14.3",
|
||||
"quanta",
|
||||
"quinn",
|
||||
@@ -6619,6 +6620,39 @@ dependencies = [
|
||||
"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.117",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pnet_sys"
|
||||
version = "0.35.0"
|
||||
|
||||
@@ -13,6 +13,7 @@ ffi-dataplane = [
|
||||
"easytier/ffi-dataplane",
|
||||
"easytier-core/proxy-smoltcp-stack",
|
||||
]
|
||||
macos-ne = ["easytier/macos-ne"]
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../../easytier", features = ["tracing-log"] }
|
||||
@@ -31,4 +32,3 @@ uuid = "1.17.0"
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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 crate::{
|
||||
@@ -9,6 +16,28 @@ use crate::{
|
||||
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
|
||||
/// Set the tun fd
|
||||
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) {
|
||||
Ok(Some(instance_id)) => instance_id,
|
||||
Ok(None) => {
|
||||
set_error_msg("instance not found");
|
||||
set_error_msg(&format!("instance not found: {inst_name}"));
|
||||
return -1;
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -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) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to set tun fd: {}", e));
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
mod elevate;
|
||||
|
||||
use anyhow::Context;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
use easytier::instance::factory::attach_mobile_tun_fd;
|
||||
#[cfg(target_os = "android")]
|
||||
use easytier::instance::factory::subscribe_native_instance_event;
|
||||
use easytier::proto::api::manage::{
|
||||
@@ -71,6 +73,9 @@ static RPC_SERVER: once_cell::sync::Lazy<Mutex<Option<RpcServer>>> =
|
||||
static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> =
|
||||
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 {
|
||||
() => {{
|
||||
let guard = CLIENT_MANAGER
|
||||
@@ -81,6 +86,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]
|
||||
fn easytier_version() -> Result<String, String> {
|
||||
Ok(easytier::VERSION.to_string())
|
||||
@@ -105,6 +124,8 @@ fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), Strin
|
||||
|
||||
#[tauri::command]
|
||||
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())?;
|
||||
Ok(toml.dump())
|
||||
}
|
||||
@@ -122,6 +143,8 @@ async fn run_network_instance(
|
||||
cfg: NetworkConfig,
|
||||
save: bool,
|
||||
) -> Result<(), String> {
|
||||
let mut cfg = cfg;
|
||||
normalize_network_config_for_runtime(&mut cfg);
|
||||
let client_manager = get_client_manager!()?;
|
||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
@@ -160,20 +183,191 @@ async fn set_logging_level(level: String) -> Result<(), String> {
|
||||
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]
|
||||
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 {
|
||||
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()
|
||||
.next()
|
||||
|
||||
let target_ids = match instance_ids {
|
||||
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
|
||||
.attach_tun_fd(uuid, fd)
|
||||
.map_err(|e| e.to_string())?;
|
||||
for instance_id in &target_ids {
|
||||
if instance_manager.instance(*instance_id).is_none() {
|
||||
return Err(format!("instance {instance_id} not found"));
|
||||
}
|
||||
if !source_map.contains_key(instance_id) {
|
||||
return Err(format!("missing tun sources for instance {instance_id}"));
|
||||
}
|
||||
}
|
||||
if let Some(unexpected_id) = source_map
|
||||
.keys()
|
||||
.find(|instance_id| !target_id_set.contains(instance_id))
|
||||
{
|
||||
return Err(format!(
|
||||
"received tun sources for unexpected instance {unexpected_id}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
let mut attach_error = None;
|
||||
for instance_id in target_ids.iter().copied() {
|
||||
let result = attach_mobile_tun_fd(
|
||||
instance_manager.as_ref(),
|
||||
instance_id,
|
||||
fd,
|
||||
source_map
|
||||
.remove(&instance_id)
|
||||
.expect("tun sources were validated before attachment"),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
attach_error = Some(format!("{instance_id}: {error}"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(attach_error) = attach_error {
|
||||
let mut rollback_errors = Vec::new();
|
||||
for instance_id in target_ids {
|
||||
if let Err(error) = attach_mobile_tun_fd(
|
||||
instance_manager.as_ref(),
|
||||
instance_id,
|
||||
0,
|
||||
easytier::instance::virtual_nic::MobileTunSources::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
rollback_errors.push(format!("{instance_id}: {error}"));
|
||||
}
|
||||
}
|
||||
let rollback = if rollback_errors.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("; rollback failed for {}", rollback_errors.join("; "))
|
||||
};
|
||||
return Err(format!(
|
||||
"failed to set tun fd for the instance group: {attach_error}{rollback}"
|
||||
));
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
let mut errors = Vec::new();
|
||||
for instance_id in target_ids {
|
||||
if let Err(error) = instance_manager.attach_tun_fd(instance_id, fd) {
|
||||
errors.push(format!("{instance_id}: {error}"));
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"failed to set tun fd for the instance group: {}",
|
||||
errors.join("; ")
|
||||
))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -789,9 +983,10 @@ mod manager {
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
inst_id: Uuid,
|
||||
cfg: NetworkConfig,
|
||||
mut cfg: NetworkConfig,
|
||||
source: PersistedConfigSource,
|
||||
) -> anyhow::Result<()> {
|
||||
normalize_network_config_for_runtime(&mut cfg);
|
||||
let source = self
|
||||
.network_configs
|
||||
.get(&inst_id)
|
||||
@@ -926,32 +1121,99 @@ mod manager {
|
||||
.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")]
|
||||
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,
|
||||
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||
web_only: bool,
|
||||
shared_dev_name: Option<&str>,
|
||||
) -> Vec<uuid::Uuid> {
|
||||
self.storage
|
||||
.network_configs
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.filter(|v| !v.config.no_tun())
|
||||
.filter(|v| v.source.is_web_like())
|
||||
.filter(|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())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(super) async fn disable_instances_with_tun(
|
||||
pub(super) async fn disable_incompatible_instances_with_tun(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
web_only: bool,
|
||||
shared_dev_name: Option<&str>,
|
||||
) -> Result<(), easytier_core::management::remote_client::RemoteClientError<anyhow::Error>>
|
||||
{
|
||||
let inst_ids: Vec<uuid::Uuid> = if web_only {
|
||||
self.get_enabled_instances_with_web_like_tun_ids().collect()
|
||||
} else {
|
||||
self.get_enabled_instances_with_tun_ids().collect()
|
||||
};
|
||||
for inst_id in inst_ids {
|
||||
for inst_id in self.enabled_incompatible_tun_ids(web_only, shared_dev_name) {
|
||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||
.await?;
|
||||
}
|
||||
@@ -959,11 +1221,20 @@ mod manager {
|
||||
}
|
||||
|
||||
pub(super) fn notify_vpn_stop_if_no_tun(&self, app: &AppHandle) -> Result<(), String> {
|
||||
let has_tun = self.get_enabled_instances_with_tun_ids().any(|_| true);
|
||||
if !has_tun {
|
||||
app.emit("vpn_service_stop", "")
|
||||
#[cfg(target_os = "android")]
|
||||
if let Some(instance_id) = self.get_enabled_instances_with_tun_ids().next() {
|
||||
app.emit("vpn_service_config_changed", instance_id.to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
app.emit("vpn_service_stop", "")
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -979,19 +1250,31 @@ mod manager {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if !cfg.get_flags().no_tun {
|
||||
let shared_dev_name = Self::runtime_shared_tun_dev_name(cfg);
|
||||
match source {
|
||||
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
||||
self.disable_instances_with_tun(app, false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.disable_incompatible_instances_with_tun(
|
||||
app,
|
||||
false,
|
||||
shared_dev_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
PersistedConfigSource::Web => {
|
||||
self.disable_instances_with_tun(app, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||
self.disable_incompatible_instances_with_tun(
|
||||
app,
|
||||
true,
|
||||
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(
|
||||
"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(),
|
||||
);
|
||||
}
|
||||
@@ -1110,10 +1393,12 @@ mod manager {
|
||||
) -> anyhow::Result<()> {
|
||||
self.storage.network_configs.clear();
|
||||
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(
|
||||
instance_id.parse()?,
|
||||
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
||||
GUIConfig::new(instance_id.to_string(), config, stored.source),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1180,8 +1465,33 @@ mod manager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PersistedConfigSource, StoredGuiConfig};
|
||||
use crate::{ANDROID_SHARED_TUN_DEV_NAME, normalize_android_network_config};
|
||||
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]
|
||||
fn stored_gui_config_defaults_missing_source_to_legacy() {
|
||||
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
||||
|
||||
@@ -70,8 +70,23 @@ export async function setLoggingLevel(level: string) {
|
||||
return await invoke('set_logging_level', { level })
|
||||
}
|
||||
|
||||
export async function setTunFd(fd: number) {
|
||||
return await invoke('set_tun_fd', { fd })
|
||||
export interface TunFdInstanceSources {
|
||||
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() {
|
||||
@@ -110,7 +125,7 @@ export async function sendConfigs(enabledNetworks: string[]) {
|
||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||
source,
|
||||
})),
|
||||
enabledNetworks
|
||||
enabledNetworks,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const EVENTS = Object.freeze({
|
||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||
POST_RUN_NETWORK_INSTANCE: 'post_run_network_instance',
|
||||
VPN_SERVICE_STOP: 'vpn_service_stop',
|
||||
VPN_SERVICE_CONFIG_CHANGED: 'vpn_service_config_changed',
|
||||
DHCP_IP_CHANGED: 'dhcp_ip_changed',
|
||||
PROXY_CIDRS_UPDATED: 'proxy_cidrs_updated',
|
||||
EVENT_LAGGED: 'event_lagged',
|
||||
@@ -76,6 +77,14 @@ async function onVpnServiceStop(event: Event<unknown>) {
|
||||
await syncMobileVpnService();
|
||||
}
|
||||
|
||||
async function onVpnServiceConfigChanged(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.VPN_SERVICE_CONFIG_CHANGED}' for instance: ${instanceId}`)
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDhcpIpChanged(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${instanceId}`);
|
||||
@@ -104,6 +113,7 @@ export async function listenGlobalEvents() {
|
||||
await listen(EVENTS.PRE_RUN_NETWORK_INSTANCE, onPreRunNetworkInstance),
|
||||
await listen(EVENTS.POST_RUN_NETWORK_INSTANCE, onPostRunNetworkInstance),
|
||||
await listen(EVENTS.VPN_SERVICE_STOP, onVpnServiceStop),
|
||||
await listen(EVENTS.VPN_SERVICE_CONFIG_CHANGED, onVpnServiceConfigChanged),
|
||||
await listen(EVENTS.DHCP_IP_CHANGED, onDhcpIpChanged),
|
||||
await listen(EVENTS.PROXY_CIDRS_UPDATED, onProxyCidrsUpdated),
|
||||
await listen(EVENTS.EVENT_LAGGED, onEventLagged),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import type { TunFdInstanceSources } from './backend'
|
||||
import { addPluginListener } from '@tauri-apps/api/core'
|
||||
import { Utils } from 'easytier-frontend-lib'
|
||||
import { get_vpn_status, prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
@@ -8,20 +9,28 @@ type Route = NetworkTypes.Route
|
||||
interface vpnStatus {
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Addrs: string[]
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
dns: string | null | undefined
|
||||
instanceIds: string[]
|
||||
instanceSources: TunFdInstanceSources[]
|
||||
}
|
||||
|
||||
let dhcpPollingTimer: NodeJS.Timeout | null = null
|
||||
let vpnConfigSyncTask: Promise<void> | null = null
|
||||
let pendingVpnConfigInstanceId: string | null = null
|
||||
const DHCP_POLLING_INTERVAL = 2000 // 2秒后重试
|
||||
|
||||
const curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Addrs: [],
|
||||
ipv4Cidr: undefined,
|
||||
routes: [],
|
||||
dns: undefined,
|
||||
instanceIds: [],
|
||||
instanceSources: [],
|
||||
}
|
||||
|
||||
async function requestVpnPermission() {
|
||||
@@ -42,9 +51,12 @@ async function requestVpnPermission() {
|
||||
|
||||
function resetVpnConfigStatus() {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Addrs = []
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
curVpnStatus.instanceIds = []
|
||||
curVpnStatus.instanceSources = []
|
||||
}
|
||||
|
||||
function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) {
|
||||
@@ -54,7 +66,14 @@ function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_statu
|
||||
return
|
||||
}
|
||||
|
||||
const ipv4WithCidr = status?.ipv4Addr
|
||||
const nativeIpv4Addrs = status?.ipv4Addrs?.length
|
||||
? [...status.ipv4Addrs]
|
||||
: status?.ipv4Addr
|
||||
? [status.ipv4Addr]
|
||||
: []
|
||||
curVpnStatus.ipv4Addrs = nativeIpv4Addrs
|
||||
|
||||
const ipv4WithCidr = curVpnStatus.ipv4Addrs[0]
|
||||
if (ipv4WithCidr?.length) {
|
||||
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
@@ -81,11 +100,23 @@ 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) {
|
||||
const wasRunning = curVpnStatus.running
|
||||
if (!force && !wasRunning) {
|
||||
return
|
||||
}
|
||||
const instanceIds = [...curVpnStatus.instanceIds]
|
||||
const instanceSources = [...curVpnStatus.instanceSources]
|
||||
await detachTunFd(instanceIds, instanceSources)
|
||||
console.log('stop vpn')
|
||||
const stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
@@ -96,14 +127,25 @@ async function doStopVpn(force = false) {
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
|
||||
async function doStartVpn(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) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('start vpn service', ipv4Addr, cidr, routes, dns)
|
||||
const primaryIpv4 = ipv4Addrs[0]
|
||||
const [ipv4Addr, cidr] = ipv4Addrs[0].split('/')
|
||||
curVpnStatus.instanceIds = [...instanceIds]
|
||||
curVpnStatus.instanceSources = [...instanceSources]
|
||||
console.log('start vpn service', ipv4Addrs, routes, dns, instanceIds)
|
||||
const request = {
|
||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
||||
ipv4Addr: primaryIpv4,
|
||||
ipv4Addrs,
|
||||
routes,
|
||||
dns,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
@@ -127,23 +169,46 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?
|
||||
await waitVpnStatus(true, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.ipv4Cidr = cidr
|
||||
curVpnStatus.ipv4Addrs = [...ipv4Addrs]
|
||||
curVpnStatus.ipv4Cidr = Number(cidr)
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.dns = dns
|
||||
curVpnStatus.instanceIds = [...instanceIds]
|
||||
curVpnStatus.instanceSources = [...instanceSources]
|
||||
}
|
||||
|
||||
function scheduleVpnConfigRetry(instanceId: string) {
|
||||
if (dhcpPollingTimer) {
|
||||
clearTimeout(dhcpPollingTimer)
|
||||
}
|
||||
dhcpPollingTimer = setTimeout(() => {
|
||||
onNetworkInstanceChange(instanceId)
|
||||
}, DHCP_POLLING_INTERVAL)
|
||||
}
|
||||
|
||||
function hasQueuedVpnConfigChange() {
|
||||
return pendingVpnConfigInstanceId !== null
|
||||
}
|
||||
|
||||
async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
await setTunFd(payload.fd).catch((e) => {
|
||||
try {
|
||||
await setTunFd(payload.fd, curVpnStatus.instanceIds, curVpnStatus.instanceSources)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('set tun fd failed', e)
|
||||
})
|
||||
await doStopVpn(true).catch(stopError => console.error('stop vpn after tun attach failure', stopError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(payload: any) {
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
const instanceIds = [...curVpnStatus.instanceIds]
|
||||
const instanceSources = [...curVpnStatus.instanceSources]
|
||||
await detachTunFd(instanceIds, instanceSources)
|
||||
curVpnStatus.running = false
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
@@ -186,7 +251,93 @@ function getRoutesForVpn(routes: Route[] | undefined, node_config: NetworkTypes.
|
||||
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 }
|
||||
}
|
||||
|
||||
export async function onNetworkInstanceChange(instanceId: string) {
|
||||
pendingVpnConfigInstanceId = instanceId
|
||||
if (!vpnConfigSyncTask) {
|
||||
vpnConfigSyncTask = drainVpnConfigChanges().finally(() => {
|
||||
vpnConfigSyncTask = null
|
||||
})
|
||||
}
|
||||
|
||||
await vpnConfigSyncTask
|
||||
}
|
||||
|
||||
async function drainVpnConfigChanges() {
|
||||
while (pendingVpnConfigInstanceId !== null) {
|
||||
const instanceId = pendingVpnConfigInstanceId
|
||||
pendingVpnConfigInstanceId = null
|
||||
await applyNetworkInstanceChange(instanceId)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyNetworkInstanceChange(instanceId: string) {
|
||||
console.error('vpn service network instance change id', instanceId)
|
||||
|
||||
if (dhcpPollingTimer) {
|
||||
@@ -197,61 +348,124 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
if (!instanceId) {
|
||||
console.warn('vpn service skipped because instance id is empty')
|
||||
if (curVpnStatus.running) {
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
return
|
||||
}
|
||||
await doStopVpn()
|
||||
}
|
||||
return
|
||||
}
|
||||
const config = await getConfig(instanceId)
|
||||
console.log('vpn service loaded config', instanceId, JSON.stringify({
|
||||
no_tun: config.no_tun,
|
||||
dhcp: config.dhcp,
|
||||
enable_magic_dns: config.enable_magic_dns,
|
||||
}))
|
||||
if (config.no_tun) {
|
||||
console.log('vpn service skipped because no_tun is enabled', instanceId)
|
||||
return
|
||||
}
|
||||
const curNetworkInfo = (await collectNetworkInfo(instanceId))?.info?.map?.[instanceId]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
console.warn('vpn service skipped because network info is unavailable', instanceId, curNetworkInfo?.error_msg)
|
||||
const group = await findRunningTunInstanceGroup(instanceId)
|
||||
if (!group.length) {
|
||||
console.warn('vpn service skipped because no running tun instance is available', instanceId)
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
return
|
||||
}
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4
|
||||
const virtual_ip = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined
|
||||
const ipv4Addrs: string[] = []
|
||||
const instanceSources: TunFdInstanceSources[] = []
|
||||
const routes = new Set<string>()
|
||||
let dns: string | undefined
|
||||
const retryInstanceIds: string[] = []
|
||||
for (const { instanceId, config } of group) {
|
||||
console.log('vpn service loaded config', instanceId, JSON.stringify({
|
||||
no_tun: config.no_tun,
|
||||
dhcp: config.dhcp,
|
||||
enable_magic_dns: config.enable_magic_dns,
|
||||
dev_name: config.dev_name,
|
||||
}))
|
||||
|
||||
if (config.dhcp && (!virtual_ip || !virtual_ip.length)) {
|
||||
console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms')
|
||||
dhcpPollingTimer = setTimeout(() => {
|
||||
onNetworkInstanceChange(instanceId)
|
||||
}, DHCP_POLLING_INTERVAL)
|
||||
return
|
||||
const curNetworkInfo = getCollectedNetworkInfo(await collectNetworkInfo(instanceId), instanceId)
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
console.warn('vpn service skipped because network info is unavailable, will retry', instanceId, curNetworkInfo?.error_msg)
|
||||
retryInstanceIds.push(instanceId)
|
||||
continue
|
||||
}
|
||||
|
||||
const virtualIpv4 = curNetworkInfo.my_node_info?.virtual_ipv4
|
||||
const virtual_ip = virtualIpv4?.address?.addr ? Utils.ipv4ToString(virtualIpv4.address) : undefined
|
||||
if (config.dhcp && (!virtual_ip || !virtual_ip.length)) {
|
||||
console.log('DHCP enabled but no IP yet, will retry in', DHCP_POLLING_INTERVAL, 'ms')
|
||||
retryInstanceIds.push(instanceId)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
retryInstanceIds.push(instanceId)
|
||||
continue
|
||||
}
|
||||
|
||||
let network_length = virtualIpv4?.network_length
|
||||
if (!network_length) {
|
||||
network_length = 24
|
||||
}
|
||||
|
||||
const sourceIpv4 = `${virtual_ip}/${network_length}`
|
||||
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,
|
||||
ipv4Addrs: [sourceIpv4],
|
||||
ipv6Addrs: [],
|
||||
ipv4Routes,
|
||||
ipv6Routes,
|
||||
})
|
||||
if (config.enable_magic_dns) {
|
||||
dns = '100.100.100.101'
|
||||
}
|
||||
}
|
||||
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
if (retryInstanceIds.length) {
|
||||
scheduleVpnConfigRetry(retryInstanceIds[0])
|
||||
console.warn('vpn service waiting for complete shared tun instance sources', retryInstanceIds)
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
return
|
||||
}
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
let network_length = virtualIpv4?.network_length
|
||||
if (!network_length) {
|
||||
network_length = 24
|
||||
if (!ipv4Addrs.length) {
|
||||
console.warn('vpn service skipped because no healthy tun instance info is available', instanceId)
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
return
|
||||
}
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
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 sortedIpv4Addrs = [...ipv4Addrs].sort()
|
||||
const sortedRoutes = Array.from(routes).sort()
|
||||
const sortedInstanceIds = group.map(({ instanceId }) => instanceId).sort()
|
||||
const sortedInstanceSources = sortInstanceSources(instanceSources)
|
||||
const ipChanged = JSON.stringify(sortedIpv4Addrs) !== JSON.stringify(curVpnStatus.ipv4Addrs)
|
||||
const routesChanged = JSON.stringify(sortedRoutes) !== JSON.stringify(curVpnStatus.routes)
|
||||
const dnsChanged = dns !== curVpnStatus.dns
|
||||
const instanceIdsChanged = JSON.stringify(sortedInstanceIds) !== JSON.stringify(curVpnStatus.instanceIds)
|
||||
const instanceSourcesChanged = JSON.stringify(sortedInstanceSources) !== JSON.stringify(sortInstanceSources(curVpnStatus.instanceSources))
|
||||
const configChanged = ipChanged || routesChanged || dnsChanged || instanceIdsChanged || instanceSourcesChanged
|
||||
const shouldStartVpn = !curVpnStatus.running
|
||||
|
||||
if (shouldStartVpn || configChanged) {
|
||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
console.info('vpn service skipped stale config apply because a newer change is queued')
|
||||
return
|
||||
}
|
||||
|
||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), sortedIpv4Addrs)
|
||||
if (curVpnStatus.running) {
|
||||
try {
|
||||
await doStopVpn()
|
||||
@@ -259,10 +473,14 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (hasQueuedVpnConfigChange()) {
|
||||
console.info('vpn service skipped stale config start because a newer change is queued')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(virtual_ip, network_length, routes, dns)
|
||||
await doStartVpn(sortedIpv4Addrs, sortedRoutes, dns, sortedInstanceIds, sortedInstanceSources)
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error && e.message === 'need_prepare') {
|
||||
@@ -301,6 +519,33 @@ async function findRunningTunInstanceId() {
|
||||
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[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() {
|
||||
await registerVpnServiceListener()
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ rand = "0.8.5"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
pnet_datalink = { version = "0.35.0", optional = true }
|
||||
pnet_packet = "0.35.0"
|
||||
smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp.git", rev = "0a926767a68bc88d5512afefa7529c5ecdade4ea", optional = true, default-features = false, features = [
|
||||
"std",
|
||||
"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 async_trait::async_trait;
|
||||
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]
|
||||
impl IfConfiguerTrait for MacIfConfiger {
|
||||
async fn add_ipv4_route(
|
||||
@@ -14,12 +46,28 @@ impl IfConfiguerTrait for MacIfConfiger {
|
||||
cidr_prefix: u8,
|
||||
cost: Option<i32>,
|
||||
) -> 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(
|
||||
format!(
|
||||
"route -n add {} -netmask {} -interface {} -hopcount {}",
|
||||
"route -n add {} -netmask {} -interface {}{} -hopcount {}",
|
||||
address,
|
||||
cidr_to_subnet_mask(cidr_prefix),
|
||||
name,
|
||||
source_hint,
|
||||
cost.unwrap_or(7)
|
||||
)
|
||||
.as_str(),
|
||||
@@ -51,14 +99,21 @@ impl IfConfiguerTrait for MacIfConfiger {
|
||||
address: Ipv4Addr,
|
||||
cidr_prefix: u8,
|
||||
) -> Result<(), Error> {
|
||||
run_shell_cmd(
|
||||
format!(
|
||||
"ifconfig {} {:?}/{:?} {:?} up",
|
||||
name, address, cidr_prefix, address,
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
let addr = Ipv4Inet::new(address, cidr_prefix).map_err(|err| {
|
||||
anyhow::anyhow!("invalid IPv4 address {address}/{cidr_prefix}: {err:?}")
|
||||
})?;
|
||||
let mut configured_ipv4 = self.configured_ipv4.lock().await;
|
||||
let has_configured_ipv4 = configured_ipv4
|
||||
.get(name)
|
||||
.is_some_and(|addresses| !addresses.is_empty());
|
||||
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> {
|
||||
@@ -67,11 +122,33 @@ impl IfConfiguerTrait for MacIfConfiger {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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> {
|
||||
|
||||
@@ -40,6 +40,16 @@ pub trait IfConfiguerTrait: Send + Sync {
|
||||
) -> Result<(), Error> {
|
||||
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(
|
||||
&self,
|
||||
_name: &str,
|
||||
@@ -48,6 +58,16 @@ pub trait IfConfiguerTrait: Send + Sync {
|
||||
) -> Result<(), Error> {
|
||||
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(
|
||||
&self,
|
||||
_name: &str,
|
||||
@@ -157,6 +177,7 @@ async fn run_shell_cmd(cmd: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DummyIfConfiger {}
|
||||
#[async_trait]
|
||||
impl IfConfiguerTrait for DummyIfConfiger {}
|
||||
|
||||
@@ -133,6 +133,7 @@ fn dump_netlink_messages<T: NetlinkDecode>(
|
||||
receive_netlink_dump(builder)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NetlinkIfConfiger {}
|
||||
|
||||
impl NetlinkIfConfiger {
|
||||
@@ -335,6 +336,52 @@ impl NetlinkIfConfiger {
|
||||
})
|
||||
.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))
|
||||
&& 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]
|
||||
@@ -346,15 +393,25 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
cidr_prefix: u8,
|
||||
cost: Option<i32>,
|
||||
) -> Result<(), Error> {
|
||||
let message = RouteMessageBuilder::new(libc::AF_INET as u8)
|
||||
.destination(IpAddr::V4(address), cidr_prefix)
|
||||
.oif(Self::get_interface_index(name)?)
|
||||
.priority(cost.unwrap_or(65535) as u32)
|
||||
.table(libc::RT_TABLE_MAIN.into())
|
||||
.static_protocol()
|
||||
.universe_scope()
|
||||
.route_type(RouteType::Unicast)
|
||||
.build();
|
||||
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 message = NetlinkIfConfiger::ipv4_route_message(
|
||||
NetlinkIfConfiger::get_interface_index(name)?,
|
||||
address,
|
||||
cidr_prefix,
|
||||
cost,
|
||||
source_hint,
|
||||
);
|
||||
let request = message_request(
|
||||
RTM_NEWROUTE,
|
||||
NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL | NLM_F_REQUEST,
|
||||
@@ -373,14 +430,36 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
|
||||
|
||||
for msg in routes {
|
||||
let destination = msg
|
||||
.destination()
|
||||
.copied()
|
||||
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
|
||||
if destination == IpAddr::V4(address)
|
||||
&& msg.dst_len() == cidr_prefix
|
||||
&& msg.oif() == Some(ifidx)
|
||||
{
|
||||
if NetlinkIfConfiger::ipv4_route_target_matches(&msg, address, cidr_prefix, ifidx) {
|
||||
let request = message_request(RTM_DELROUTE, NLM_F_ACK | NLM_F_REQUEST, &msg)?;
|
||||
send_netlink_req_and_wait_ack(request)?;
|
||||
return 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> {
|
||||
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)?;
|
||||
send_netlink_req_and_wait_ack(request)?;
|
||||
return Ok(());
|
||||
@@ -601,6 +680,84 @@ 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),
|
||||
));
|
||||
}
|
||||
|
||||
struct PrepareEnv {}
|
||||
impl PrepareEnv {
|
||||
fn new() -> Self {
|
||||
|
||||
@@ -37,6 +37,7 @@ const RTA_DST: u16 = 1;
|
||||
const RTA_SRC: u16 = 2;
|
||||
const RTA_OIF: u16 = 4;
|
||||
const RTA_PRIORITY: u16 = 6;
|
||||
const RTA_PREFSRC: u16 = 7;
|
||||
const RTA_TABLE: u16 = 15;
|
||||
|
||||
const NDA_DST: u16 = 1;
|
||||
@@ -341,7 +342,9 @@ pub(crate) struct RouteMessage {
|
||||
attributes: Vec<Attribute>,
|
||||
destination: Option<IpAddr>,
|
||||
source: Option<IpAddr>,
|
||||
preferred_source: Option<IpAddr>,
|
||||
oif: Option<u32>,
|
||||
priority: Option<u32>,
|
||||
}
|
||||
|
||||
impl RouteMessage {
|
||||
@@ -369,9 +372,25 @@ impl RouteMessage {
|
||||
self.source.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn preferred_source(&self) -> Option<&IpAddr> {
|
||||
self.preferred_source.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn oif(&self) -> Option<u32> {
|
||||
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 {
|
||||
@@ -397,10 +416,18 @@ impl NetlinkDecode for RouteMessage {
|
||||
.iter()
|
||||
.find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_SRC)
|
||||
.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
|
||||
.iter()
|
||||
.find(|attribute| attribute.kind & NLA_TYPE_MASK == RTA_OIF)
|
||||
.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 {
|
||||
family,
|
||||
@@ -415,7 +442,9 @@ impl NetlinkDecode for RouteMessage {
|
||||
attributes,
|
||||
destination,
|
||||
source,
|
||||
preferred_source,
|
||||
oif,
|
||||
priority,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -458,7 +487,9 @@ impl RouteMessageBuilder {
|
||||
attributes: Vec::new(),
|
||||
destination: None,
|
||||
source: None,
|
||||
preferred_source: None,
|
||||
oif: None,
|
||||
priority: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -481,6 +512,7 @@ impl RouteMessageBuilder {
|
||||
}
|
||||
|
||||
pub(crate) fn priority(mut self, priority: u32) -> Self {
|
||||
self.message.priority = Some(priority);
|
||||
self.message.attributes.push(Attribute::new(
|
||||
RTA_PRIORITY,
|
||||
priority.to_ne_bytes().to_vec(),
|
||||
@@ -488,6 +520,14 @@ impl RouteMessageBuilder {
|
||||
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 {
|
||||
if let Ok(table) = u8::try_from(table) {
|
||||
self.message.table = table;
|
||||
@@ -632,6 +672,38 @@ mod tests {
|
||||
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]
|
||||
fn route_parser_reads_ipv6_source_prefix() {
|
||||
let mut bytes = vec![
|
||||
|
||||
@@ -22,6 +22,7 @@ use winreg::{
|
||||
};
|
||||
|
||||
use super::{Error, IfConfiguerTrait};
|
||||
#[derive(Default)]
|
||||
pub struct WindowsIfConfiger {}
|
||||
|
||||
fn format_win_error(error: u32) -> String {
|
||||
|
||||
@@ -32,6 +32,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::host::{NativeInstanceHost, native_instance_host};
|
||||
#[cfg(feature = "tun")]
|
||||
use super::shared_virtual_nic::ArcSharedVirtualNicRegistry;
|
||||
#[cfg(feature = "kcp")]
|
||||
use crate::gateway::kcp_proxy::KcpProxyService;
|
||||
#[cfg(feature = "quic")]
|
||||
@@ -45,6 +47,7 @@ pub(crate) type NativeCoreInstance = CoreInstance<NativeInstanceHost>;
|
||||
pub(crate) fn compose_native_core_instance(
|
||||
toml_config: TomlConfig,
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
#[cfg(feature = "tun")] shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
compact_runtime: bool,
|
||||
) -> anyhow::Result<Arc<NativeCoreInstance>> {
|
||||
let host_config = if compact_runtime {
|
||||
@@ -58,7 +61,11 @@ pub(crate) fn compose_native_core_instance(
|
||||
&normalized,
|
||||
&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(
|
||||
global_ctx.clone(),
|
||||
process_runtime,
|
||||
|
||||
@@ -5,7 +5,15 @@ use std::{net::Ipv4Addr, sync::Arc, time::Duration};
|
||||
|
||||
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};
|
||||
|
||||
@@ -17,6 +25,52 @@ pub struct DnsRunner {
|
||||
tun_dev: Option<String>,
|
||||
tun_inet: Ipv4Inet,
|
||||
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 {
|
||||
@@ -35,9 +89,15 @@ impl DnsRunner {
|
||||
tun_dev,
|
||||
tun_inet,
|
||||
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) {
|
||||
if let Some(server) = self.server.take() {
|
||||
server.clean_env().await;
|
||||
@@ -45,17 +105,53 @@ impl DnsRunner {
|
||||
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<()> {
|
||||
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
|
||||
match MagicDnsServerInstance::new(
|
||||
self.packet_plane.clone(),
|
||||
self.global_ctx.clone(),
|
||||
self.tun_dev.clone(),
|
||||
self.tun_inet,
|
||||
self.fake_ip,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let server_result = if self.should_manage_fake_ip_route_externally() {
|
||||
MagicDnsServerInstance::new_with_external_fake_ip_route(
|
||||
self.packet_plane.clone(),
|
||||
self.global_ctx.clone(),
|
||||
self.tun_dev.clone(),
|
||||
self.tun_inet,
|
||||
self.fake_ip,
|
||||
)
|
||||
.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) => {
|
||||
self.server = Some(server);
|
||||
tracing::info!("DnsRunner::run_once: server started");
|
||||
@@ -74,11 +170,16 @@ impl DnsRunner {
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, canel_token: CancellationToken) {
|
||||
let fake_ip_route_claim = self.fake_ip_route_claim();
|
||||
|
||||
loop {
|
||||
tracing::info!("DnsRunner::run: start");
|
||||
tokio::select! {
|
||||
_ = canel_token.cancelled() => {
|
||||
self.clean_env().await;
|
||||
if let Some(claim) = &fake_ip_route_claim {
|
||||
claim.remove().await;
|
||||
}
|
||||
tracing::info!("DnsRunner::run: cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error as EtError,
|
||||
global_ctx::ArcGlobalCtx,
|
||||
ifcfg::{IfConfiger, IfConfiguerTrait},
|
||||
netns::NetNS,
|
||||
},
|
||||
instance::dns_server::{
|
||||
config::{Record, RecordBuilder, RecordType},
|
||||
@@ -51,7 +53,9 @@ use std::{collections::BTreeMap, io, net::Ipv4Addr, str::FromStr, sync::Arc, tim
|
||||
pub(super) struct MagicDnsServerInstanceData {
|
||||
dns_server: Server,
|
||||
tun_dev: Option<String>,
|
||||
net_ns: NetNS,
|
||||
fake_ip: Ipv4Addr,
|
||||
manage_fake_ip_route: bool,
|
||||
route_store: MagicDnsRecordStore,
|
||||
record_apply: tokio::sync::Mutex<()>,
|
||||
|
||||
@@ -356,12 +360,66 @@ fn get_system_config(
|
||||
}
|
||||
|
||||
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(
|
||||
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, 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> {
|
||||
let tcp_listener = runtime_rpc_listener(MAGIC_DNS_INSTANCE_SOCKET_ADDR.parse()?);
|
||||
let mut rpc_server = StandAloneServer::new(tcp_listener);
|
||||
@@ -374,7 +432,8 @@ impl MagicDnsServerInstance {
|
||||
let mut dns_server = Server::new(dns_config);
|
||||
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 cost = if cfg!(target_os = "windows") {
|
||||
@@ -382,16 +441,15 @@ impl MagicDnsServerInstance {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ifcfg = IfConfiger {};
|
||||
ifcfg
|
||||
.add_ipv4_route(tun_dev_name, fake_ip, 32, cost)
|
||||
.await?;
|
||||
Self::add_fake_ip_route(tun_dev_name, fake_ip, &global_ctx.net_ns, cost).await?;
|
||||
}
|
||||
|
||||
let data = Arc::new(MagicDnsServerInstanceData {
|
||||
dns_server,
|
||||
tun_dev: tun_dev.clone(),
|
||||
net_ns: global_ctx.net_ns.clone(),
|
||||
fake_ip,
|
||||
manage_fake_ip_route,
|
||||
route_store: MagicDnsRecordStore::default(),
|
||||
record_apply: tokio::sync::Mutex::new(()),
|
||||
system_config: get_system_config(tun_dev.as_deref())?,
|
||||
@@ -436,14 +494,13 @@ impl MagicDnsServerInstance {
|
||||
if let Err(e) = ret {
|
||||
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
|
||||
{
|
||||
let ifcfg = IfConfiger {};
|
||||
let _ = ifcfg
|
||||
.remove_ipv4_route(tun_dev_name, self.data.fake_ip, 32)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if self.data.manage_fake_ip_route
|
||||
&& !self.tun_inet.contains(&self.data.fake_ip)
|
||||
&& let Some(tun_dev_name) = &self.data.tun_dev
|
||||
{
|
||||
Self::remove_fake_ip_route(tun_dev_name, self.data.fake_ip, &self.data.net_ns).await;
|
||||
}
|
||||
|
||||
self.packet_filter.close().await;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[cfg(any(feature = "management-rpc", test))]
|
||||
use easytier_core::instance::manager::InstanceManager;
|
||||
#[cfg(feature = "management-rpc")]
|
||||
@@ -12,6 +15,10 @@ use easytier_core::{
|
||||
|
||||
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::{
|
||||
composition::compose_native_core_instance, host::NativeInstanceHost,
|
||||
runtime_host::NativeInstanceRuntimeHost,
|
||||
@@ -51,6 +58,22 @@ pub fn subscribe_native_instance_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")]
|
||||
pub fn native_instance_manager_with_runtime(
|
||||
runtime_handle: tokio::runtime::Handle,
|
||||
@@ -97,6 +120,8 @@ fn native_instance_manager_with_optional_runtime(
|
||||
/// Native construction Adapter for the canonical core InstanceManager.
|
||||
pub struct NativeInstanceFactory {
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
#[cfg(feature = "tun")]
|
||||
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
runtime_handle: Option<tokio::runtime::Handle>,
|
||||
compact_runtime: bool,
|
||||
#[cfg(feature = "logging")]
|
||||
@@ -107,6 +132,8 @@ impl NativeInstanceFactory {
|
||||
pub fn new(process_runtime: Arc<CoreProcessRuntime>) -> Self {
|
||||
Self {
|
||||
process_runtime,
|
||||
#[cfg(feature = "tun")]
|
||||
shared_virtual_nic_registry: Arc::new(Mutex::new(SharedVirtualNicRegistry::new())),
|
||||
runtime_handle: None,
|
||||
compact_runtime: false,
|
||||
#[cfg(feature = "logging")]
|
||||
@@ -126,6 +153,7 @@ impl NativeInstanceFactory {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "management-rpc")]
|
||||
fn with_compact_runtime(mut self) -> Self {
|
||||
self.compact_runtime = true;
|
||||
self
|
||||
@@ -149,6 +177,8 @@ impl InstanceFactory for NativeInstanceFactory {
|
||||
let instance = compose_native_core_instance(
|
||||
config,
|
||||
self.process_runtime.clone(),
|
||||
#[cfg(feature = "tun")]
|
||||
self.shared_virtual_nic_registry.clone(),
|
||||
self.compact_runtime,
|
||||
)?;
|
||||
#[cfg(feature = "logging")]
|
||||
|
||||
@@ -18,6 +18,9 @@ pub(crate) mod listeners;
|
||||
#[cfg(feature = "public-ipv6-provider")]
|
||||
pub(crate) mod public_ipv6_provider;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
pub mod shared_virtual_nic;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
pub mod virtual_nic;
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
use easytier_core::config::runtime::CoreInstanceRuntimeConfig;
|
||||
use easytier_core::{
|
||||
config::runtime::CoreInstanceRuntimeConfig, gateway::dhcp::DhcpIpv4Host,
|
||||
host::packet::HostPacketReceiver, instance::CorePacketPlane,
|
||||
gateway::dhcp::DhcpIpv4Host, host::packet::HostPacketReceiver, instance::CorePacketPlane,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
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 implementation;
|
||||
@@ -39,9 +44,17 @@ pub(crate) struct 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 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);
|
||||
Arc::new(Self {
|
||||
global_ctx,
|
||||
@@ -121,6 +134,15 @@ impl NativeInstanceRuntimeHost {
|
||||
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<()> {
|
||||
self.tun.install_packet_receiver(receiver)
|
||||
}
|
||||
@@ -134,6 +156,16 @@ mod tests {
|
||||
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")]
|
||||
fn runtime_config(config: &TomlConfig) -> CoreInstanceRuntimeConfig {
|
||||
let normalized = easytier_core::instance::CoreInstanceConfig::from_toml(config).unwrap();
|
||||
@@ -146,7 +178,7 @@ mod tests {
|
||||
#[test]
|
||||
fn runtime_host_owns_event_subscription_context() {
|
||||
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();
|
||||
|
||||
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_ipv6(Some("fd00::1/64".parse().unwrap()));
|
||||
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_ipv4(), Some("10.20.0.1/24".parse().unwrap()));
|
||||
@@ -205,7 +237,7 @@ mod tests {
|
||||
let config = TomlConfig::default();
|
||||
config.set_dhcp(true);
|
||||
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();
|
||||
global_ctx.set_ipv4(Some(lease));
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ use easytier_core::instance::CorePacketPlane;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
|
||||
|
||||
use crate::common::global_ctx::ArcGlobalCtx;
|
||||
#[cfg(feature = "magic-dns")]
|
||||
use crate::instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
|
||||
use crate::{common::global_ctx::ArcGlobalCtx, instance::virtual_nic::NicBackend};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct MagicDnsRuntime {
|
||||
@@ -26,6 +26,7 @@ impl MagicDnsRuntime {
|
||||
packet_plane: std::sync::Arc<CorePacketPlane>,
|
||||
tun_dev: Option<String>,
|
||||
tun_ip: Ipv4Inet,
|
||||
shared_route_backend: Option<NicBackend>,
|
||||
) -> Self {
|
||||
let active = global_ctx.get_flags().accept_dns.then(|| {
|
||||
let mut runner = DnsRunner::new(
|
||||
@@ -34,7 +35,8 @@ impl MagicDnsRuntime {
|
||||
tun_dev,
|
||||
tun_ip,
|
||||
MAGIC_DNS_FAKE_IP.parse().unwrap(),
|
||||
);
|
||||
)
|
||||
.with_shared_route_backend(shared_route_backend);
|
||||
let cancel = CancellationToken::new();
|
||||
let task_cancel = cancel.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
@@ -54,6 +56,7 @@ impl MagicDnsRuntime {
|
||||
_packet_plane: std::sync::Arc<CorePacketPlane>,
|
||||
_tun_dev: Option<String>,
|
||||
_tun_ip: Ipv4Inet,
|
||||
_shared_route_backend: Option<NicBackend>,
|
||||
) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
@@ -3,11 +3,42 @@ use std::{
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use easytier_core::host::packet::HostPacketReceiver;
|
||||
use easytier_core::{host::packet::HostPacketReceiver, instance::CorePacketPlane};
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
|
||||
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 {
|
||||
_nic_ctx: Option<Box<dyn Any + Send>>,
|
||||
|
||||
@@ -14,28 +14,37 @@ use tokio::{
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::{MagicDnsRuntime, tun_common::TunNicState};
|
||||
use super::{
|
||||
MagicDnsRuntime,
|
||||
tun_common::{TunNicState, create_nic_ctx},
|
||||
};
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
|
||||
},
|
||||
instance::virtual_nic::NicCtx,
|
||||
instance::shared_virtual_nic::ArcSharedVirtualNicRegistry,
|
||||
};
|
||||
|
||||
pub(super) struct NativeTunRuntime {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
cancel: CancellationToken,
|
||||
nic: TunNicState,
|
||||
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
static_ip_task: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
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 {
|
||||
global_ctx,
|
||||
cancel,
|
||||
nic: TunNicState::empty(),
|
||||
shared_virtual_nic_registry,
|
||||
static_ip_task: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
@@ -67,6 +76,7 @@ impl NativeTunRuntime {
|
||||
let cancel = self.cancel.clone();
|
||||
let global_ctx = self.global_ctx.clone();
|
||||
let receiver = self.nic.receiver();
|
||||
let shared_virtual_nic_registry = self.shared_virtual_nic_registry.clone();
|
||||
let (output, first_round) = oneshot::channel();
|
||||
let task = tokio::spawn(async move {
|
||||
let mut output = Some(output);
|
||||
@@ -76,12 +86,29 @@ impl NativeTunRuntime {
|
||||
return;
|
||||
}
|
||||
let closed = Arc::new(Notify::new());
|
||||
let mut nic = NicCtx::new(
|
||||
let mut nic = match create_nic_ctx(
|
||||
global_ctx.clone(),
|
||||
packet_plane.clone(),
|
||||
receiver.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! {
|
||||
biased;
|
||||
_ = cancel.cancelled() => {
|
||||
@@ -104,11 +131,13 @@ impl NativeTunRuntime {
|
||||
}
|
||||
|
||||
let magic_dns = if let Some(ip) = ipv4 {
|
||||
let shared_route_backend = nic.shared_route_backend_for_dns();
|
||||
MagicDnsRuntime::start(
|
||||
global_ctx.clone(),
|
||||
packet_plane.clone(),
|
||||
nic.ifname().await,
|
||||
ip,
|
||||
shared_route_backend,
|
||||
)
|
||||
} else {
|
||||
MagicDnsRuntime::default()
|
||||
@@ -161,6 +190,7 @@ impl NativeTunRuntime {
|
||||
nic: self.nic.clone(),
|
||||
closed: Arc::new(Notify::new()),
|
||||
packet_plane,
|
||||
shared_virtual_nic_registry: self.shared_virtual_nic_registry.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -172,6 +202,7 @@ struct NativeDhcpIpv4Host {
|
||||
nic: TunNicState,
|
||||
closed: Arc<Notify>,
|
||||
packet_plane: Arc<CorePacketPlane>,
|
||||
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
}
|
||||
|
||||
impl NativeDhcpIpv4Host {
|
||||
@@ -199,21 +230,25 @@ impl NativeDhcpIpv4Host {
|
||||
return Ok(Some(ip));
|
||||
}
|
||||
|
||||
let mut nic = NicCtx::new(
|
||||
let mut nic = create_nic_ctx(
|
||||
self.global_ctx.clone(),
|
||||
self.packet_plane.clone(),
|
||||
self.nic.receiver(),
|
||||
self.closed.clone(),
|
||||
);
|
||||
self.shared_virtual_nic_registry.clone(),
|
||||
)
|
||||
.await?;
|
||||
tokio::select! {
|
||||
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; DHCP update cancelled"),
|
||||
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(
|
||||
self.global_ctx.clone(),
|
||||
self.packet_plane.clone(),
|
||||
nic.ifname().await,
|
||||
ip,
|
||||
shared_route_backend,
|
||||
);
|
||||
self.nic.install(nic, magic_dns).await;
|
||||
self.global_ctx.set_ipv4(Some(ip));
|
||||
|
||||
@@ -8,26 +8,40 @@ use easytier_core::{
|
||||
instance::CorePacketPlane,
|
||||
};
|
||||
use futures::FutureExt as _;
|
||||
use tokio::sync::{Mutex, Notify, mpsc};
|
||||
use tokio::sync::{Mutex, Notify, mpsc, oneshot};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::{MagicDnsRuntime, tun_common::TunNicState};
|
||||
use super::{
|
||||
MagicDnsRuntime,
|
||||
tun_common::{TunNicState, create_nic_ctx},
|
||||
};
|
||||
use crate::{
|
||||
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 {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
cancel: CancellationToken,
|
||||
nic: TunNicState,
|
||||
tun_fd: mpsc::Sender<i32>,
|
||||
tun_fd_receiver: Mutex<Option<mpsc::Receiver<i32>>>,
|
||||
tun_fd: mpsc::Sender<MobileTunAttachment>,
|
||||
tun_fd_receiver: Mutex<Option<mpsc::Receiver<MobileTunAttachment>>>,
|
||||
task: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
}
|
||||
|
||||
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);
|
||||
Self {
|
||||
global_ctx,
|
||||
@@ -36,6 +50,7 @@ impl NativeTunRuntime {
|
||||
tun_fd,
|
||||
tun_fd_receiver: Mutex::new(Some(tun_fd_receiver)),
|
||||
task: Mutex::new(None),
|
||||
shared_virtual_nic_registry,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,23 +66,31 @@ impl NativeTunRuntime {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
packet_plane: Arc<CorePacketPlane>,
|
||||
fd: i32,
|
||||
sources: MobileTunSources,
|
||||
shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
) -> anyhow::Result<()> {
|
||||
nic_state.drain().await;
|
||||
if fd <= 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let closed = Arc::new(Notify::new());
|
||||
let mut nic = NicCtx::new(
|
||||
let mut nic = create_nic_ctx(
|
||||
global_ctx.clone(),
|
||||
packet_plane.clone(),
|
||||
nic_state.receiver(),
|
||||
closed,
|
||||
);
|
||||
nic.run_for_mobile(fd).await.context("add ip failed")?;
|
||||
let magic_dns = global_ctx
|
||||
.get_ipv4()
|
||||
.map(|ip| MagicDnsRuntime::start(global_ctx, packet_plane, None, ip))
|
||||
.unwrap_or_default();
|
||||
shared_virtual_nic_registry,
|
||||
)
|
||||
.await?;
|
||||
nic.run_for_mobile(fd, sources)
|
||||
.await
|
||||
.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;
|
||||
Ok(())
|
||||
}
|
||||
@@ -80,28 +103,43 @@ impl NativeTunRuntime {
|
||||
let nic_state = self.nic.clone();
|
||||
let global_ctx = self.global_ctx.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 {
|
||||
loop {
|
||||
let fd = tokio::select! {
|
||||
let attachment = tokio::select! {
|
||||
_ = 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(
|
||||
nic_state.clone(),
|
||||
global_ctx.clone(),
|
||||
packet_plane.clone(),
|
||||
fd,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let result = if attachment.fd <= 0 {
|
||||
nic_state.drain().await;
|
||||
Ok(())
|
||||
} else {
|
||||
Self::install_mobile_tun(
|
||||
nic_state.clone(),
|
||||
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");
|
||||
}
|
||||
if let Some(completion) = attachment.completion {
|
||||
let _ = completion.send(result);
|
||||
}
|
||||
}
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn shutdown(&self) {
|
||||
self.cancel.cancel();
|
||||
if let Some(task) = self.task.lock().await.take() {
|
||||
let _ = task.await;
|
||||
}
|
||||
@@ -109,11 +147,46 @@ impl NativeTunRuntime {
|
||||
}
|
||||
|
||||
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
|
||||
.try_send(fd)
|
||||
.try_send(MobileTunAttachment {
|
||||
fd,
|
||||
sources: MobileTunSources::default(),
|
||||
completion: None,
|
||||
})
|
||||
.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(
|
||||
&self,
|
||||
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,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
use crate::instance::shared_virtual_nic::{ArcSharedVirtualNicRegistry, SharedVirtualNicRegistry};
|
||||
use crate::{
|
||||
common::global_ctx::{ArcGlobalCtx, GlobalCtx},
|
||||
instance::{
|
||||
@@ -15,6 +17,8 @@ use crate::{
|
||||
},
|
||||
socket::udp::RuntimeUdpSocket,
|
||||
};
|
||||
#[cfg(feature = "tun")]
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub(crate) struct TestInstance {
|
||||
core: Arc<NativeCoreInstance>,
|
||||
@@ -26,7 +30,27 @@ impl TestInstance {
|
||||
config: TomlConfig,
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
) -> 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(
|
||||
@@ -35,14 +59,21 @@ impl TestInstance {
|
||||
provider: Box<dyn StunSocketMapper<RuntimeUdpSocket>>,
|
||||
) -> Self {
|
||||
let provider: Arc<dyn StunSocketMapper<RuntimeUdpSocket>> = Arc::from(provider);
|
||||
Self::compose(config, process_runtime, move |adapters| {
|
||||
adapters.replace_stun_provider(provider);
|
||||
})
|
||||
Self::compose(
|
||||
config,
|
||||
process_runtime,
|
||||
#[cfg(feature = "tun")]
|
||||
Arc::new(Mutex::new(SharedVirtualNicRegistry::new())),
|
||||
move |adapters| {
|
||||
adapters.replace_stun_provider(provider);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn compose(
|
||||
config: TomlConfig,
|
||||
process_runtime: Arc<CoreProcessRuntime>,
|
||||
#[cfg(feature = "tun")] shared_virtual_nic_registry: ArcSharedVirtualNicRegistry,
|
||||
customize: impl FnOnce(
|
||||
&mut easytier_core::instance::CoreHostAdapters<
|
||||
crate::instance::host::NativeInstanceHost,
|
||||
@@ -50,7 +81,11 @@ impl TestInstance {
|
||||
),
|
||||
) -> Self {
|
||||
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(
|
||||
global_ctx.clone(),
|
||||
process_runtime,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod three_node;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "tun"))]
|
||||
mod shared_virtual_nic;
|
||||
|
||||
mod ipv6_test;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
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 PROXY_TARGET: &str = "10.1.2.4";
|
||||
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()
|
||||
.snapshot()
|
||||
.ipv4_routes
|
||||
.get(route)
|
||||
.map_or(0, |owners| owners.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 duplicate_proxy_cidr_survives_one_shared_owner_departure() {
|
||||
let lab = ProxyLab::new();
|
||||
let source_dev = test_dev_name();
|
||||
let owner_dev = test_dev_name();
|
||||
let runtime = SharedTestRuntime::new();
|
||||
|
||||
let mut source = runtime.instance(test_config(
|
||||
"shared_proxy_source",
|
||||
"shared_proxy_network",
|
||||
"shared_proxy_secret",
|
||||
Some(&lab.source_ns),
|
||||
Some(&source_dev),
|
||||
"10.144.245.1/24",
|
||||
));
|
||||
let primary_config = test_config(
|
||||
"shared_proxy_primary",
|
||||
"shared_proxy_network",
|
||||
"shared_proxy_secret",
|
||||
Some(&lab.owner_ns),
|
||||
Some(&owner_dev),
|
||||
"10.144.245.2/24",
|
||||
);
|
||||
primary_config
|
||||
.add_proxy_cidr(PROXY_CIDR.parse().unwrap(), None)
|
||||
.unwrap();
|
||||
let backup_config = test_config(
|
||||
"shared_proxy_backup",
|
||||
"shared_proxy_network",
|
||||
"shared_proxy_secret",
|
||||
Some(&lab.owner_ns),
|
||||
Some(&owner_dev),
|
||||
"10.144.245.3/24",
|
||||
);
|
||||
backup_config
|
||||
.add_proxy_cidr(PROXY_CIDR.parse().unwrap(), None)
|
||||
.unwrap();
|
||||
let mut primary = runtime.instance(primary_config);
|
||||
let mut backup = runtime.instance(backup_config);
|
||||
|
||||
source.run().await.unwrap();
|
||||
primary.run().await.unwrap();
|
||||
backup.run().await.unwrap();
|
||||
wait_tun_ready(&source, &source_dev).await;
|
||||
wait_tun_ready(&primary, &owner_dev).await;
|
||||
wait_tun_ready(&backup, &owner_dev).await;
|
||||
|
||||
primary.add_connector_url(source.ring_listener_url());
|
||||
backup.add_connector_url(source.ring_listener_url());
|
||||
wait_proxy_route(&source, primary.peer_id(), PROXY_CIDR).await;
|
||||
wait_proxy_route(&source, backup.peer_id(), PROXY_CIDR).await;
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
ipv4_route_exists_in_ns(&lab.source_ns, &format!("{PROXY_CIDR} dev {source_dev}"))
|
||||
&& ping_test(&lab.source_ns, PROXY_TARGET, None).await
|
||||
},
|
||||
WAIT,
|
||||
)
|
||||
.await;
|
||||
|
||||
let primary_id = primary.peer_id();
|
||||
drop_insts(vec![primary]).await;
|
||||
wait_proxy_route_absent(&source, primary_id, PROXY_CIDR).await;
|
||||
wait_proxy_route(&source, backup.peer_id(), PROXY_CIDR).await;
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
ipv4_route_exists_in_ns(&lab.source_ns, &format!("{PROXY_CIDR} dev {source_dev}"))
|
||||
&& ping_test(&lab.source_ns, PROXY_TARGET, None).await
|
||||
},
|
||||
WAIT,
|
||||
)
|
||||
.await;
|
||||
|
||||
drop_insts(vec![source, backup]).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.243.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;
|
||||
}
|
||||
@@ -1001,6 +1001,157 @@ pub async fn public_ipv6_auto_addr_reconnect_reuses_same_address() {
|
||||
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.144.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]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.Bundle
|
||||
import android.system.OsConstants.AF_INET6
|
||||
import java.net.InetAddress
|
||||
import java.util.Arrays
|
||||
|
||||
@@ -15,10 +16,12 @@ class TauriVpnService : VpnService() {
|
||||
@JvmField var triggerCallback: (String, JSObject) -> Unit = { _, _ -> }
|
||||
@JvmField var self: TauriVpnService? = null
|
||||
@JvmField var ipv4Addr: String? = null
|
||||
@JvmField var ipv4Addrs: Array<String> = emptyArray()
|
||||
@JvmField var routes: Array<String> = emptyArray()
|
||||
@JvmField var dns: String? = null
|
||||
|
||||
const val IPV4_ADDR = "IPV4_ADDR"
|
||||
const val IPV4_ADDRS = "IPV4_ADDRS"
|
||||
const val ROUTES = "ROUTES"
|
||||
const val DNS = "DNS"
|
||||
const val DISALLOWED_APPLICATIONS = "DISALLOWED_APPLICATIONS"
|
||||
@@ -30,7 +33,8 @@ class TauriVpnService : VpnService() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
println("vpn on start command ${intent?.getExtras()} $intent")
|
||||
var args = intent?.getExtras()
|
||||
ipv4Addr = args?.getString(IPV4_ADDR)
|
||||
ipv4Addrs = getIpv4Addrs(args)
|
||||
ipv4Addr = ipv4Addrs.firstOrNull()
|
||||
routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
||||
dns = args?.getString(DNS)
|
||||
|
||||
@@ -74,29 +78,45 @@ class TauriVpnService : VpnService() {
|
||||
|
||||
private fun clearStatus() {
|
||||
ipv4Addr = null
|
||||
ipv4Addrs = emptyArray()
|
||||
routes = emptyArray()
|
||||
dns = null
|
||||
}
|
||||
|
||||
private fun getIpv4Addrs(args: Bundle?): Array<String> {
|
||||
val ipv4Addrs = args
|
||||
?.getStringArray(IPV4_ADDRS)
|
||||
?.filter { it.isNotBlank() }
|
||||
?.toTypedArray()
|
||||
?: emptyArray()
|
||||
if (ipv4Addrs.isNotEmpty()) {
|
||||
return ipv4Addrs
|
||||
}
|
||||
|
||||
return arrayOf(args?.getString(IPV4_ADDR) ?: "10.126.126.1/24")
|
||||
}
|
||||
|
||||
private fun createVpnInterface(args: Bundle?): ParcelFileDescriptor {
|
||||
var builder = Builder()
|
||||
.setSession("TauriVpnService")
|
||||
.setBlocking(false)
|
||||
|
||||
var mtu = args?.getInt(MTU) ?: 1500
|
||||
var ipv4Addr = args?.getString(IPV4_ADDR) ?: "10.126.126.1/24"
|
||||
var ipv4Addrs = getIpv4Addrs(args)
|
||||
var dns: String? = args?.getString(DNS)
|
||||
var routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
||||
var disallowedApplications = args?.getStringArray(DISALLOWED_APPLICATIONS) ?: emptyArray()
|
||||
|
||||
println("vpn create vpn interface. mtu: $mtu, ipv4Addr: $ipv4Addr, dns:" +
|
||||
println("vpn create vpn interface. mtu: $mtu, ipv4Addrs: ${java.util.Arrays.toString(ipv4Addrs)}, dns:" +
|
||||
"$dns, routes: ${java.util.Arrays.toString(routes)}," +
|
||||
"disallowedApplications: ${java.util.Arrays.toString(disallowedApplications)}")
|
||||
|
||||
val ipParts = ipv4Addr.split("/")
|
||||
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
||||
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
||||
builder.addAddress("fd00::1", 128)
|
||||
for (ipv4Addr in ipv4Addrs) {
|
||||
val ipParts = ipv4Addr.split("/")
|
||||
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
||||
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
||||
}
|
||||
builder.allowFamily(AF_INET6)
|
||||
|
||||
builder.setMtu(mtu)
|
||||
dns?.let { builder.addDnsServer(it) }
|
||||
|
||||
@@ -12,6 +12,7 @@ import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import android.webkit.WebView
|
||||
import org.json.JSONArray
|
||||
|
||||
@InvokeArg
|
||||
class PingArgs {
|
||||
@@ -21,6 +22,7 @@ class PingArgs {
|
||||
@InvokeArg
|
||||
class StartVpnArgs {
|
||||
var ipv4Addr: String? = null
|
||||
var ipv4Addrs: Array<String> = emptyArray()
|
||||
var routes: Array<String> = emptyArray()
|
||||
var dns: String? = null
|
||||
var disallowedApplications: Array<String> = emptyArray()
|
||||
@@ -85,6 +87,7 @@ class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
||||
} else {
|
||||
val intent = Intent(activity, TauriVpnService::class.java)
|
||||
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
|
||||
intent.putExtra(TauriVpnService.IPV4_ADDRS, args.ipv4Addrs)
|
||||
intent.putExtra(TauriVpnService.ROUTES, args.routes)
|
||||
intent.putExtra(TauriVpnService.DNS, args.dns)
|
||||
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
|
||||
@@ -112,7 +115,8 @@ class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
||||
val ret = JSObject()
|
||||
ret.put("running", TauriVpnService.self != null)
|
||||
ret.put("ipv4Addr", TauriVpnService.ipv4Addr)
|
||||
ret.put("routes", TauriVpnService.routes)
|
||||
ret.put("ipv4Addrs", JSONArray(TauriVpnService.ipv4Addrs))
|
||||
ret.put("routes", JSONArray(TauriVpnService.routes))
|
||||
ret.put("dns", TauriVpnService.dns)
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface InvokeResponse {
|
||||
|
||||
export interface StartVpnRequest {
|
||||
ipv4Addr?: string;
|
||||
ipv4Addrs?: string[];
|
||||
routes?: string[];
|
||||
dns?: string;
|
||||
disallowedApplications?: string[];
|
||||
@@ -24,6 +25,7 @@ export interface StartVpnRequest {
|
||||
export interface VpnStatusResponse {
|
||||
running: boolean;
|
||||
ipv4Addr?: string;
|
||||
ipv4Addrs?: string[];
|
||||
routes?: string[];
|
||||
dns?: string;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct VoidRequest {}
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StartVpnRequest {
|
||||
pub ipv4_addr: Option<String>,
|
||||
pub ipv4_addrs: Option<Vec<String>>,
|
||||
pub routes: Option<Vec<String>>,
|
||||
pub dns: Option<String>,
|
||||
pub disallowed_applications: Option<Vec<String>>,
|
||||
@@ -39,6 +40,7 @@ pub struct Status {
|
||||
pub struct VpnStatus {
|
||||
pub running: bool,
|
||||
pub ipv4_addr: Option<String>,
|
||||
pub ipv4_addrs: Option<Vec<String>>,
|
||||
pub routes: Option<Vec<String>>,
|
||||
pub dns: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user