Compare commits

...
Author SHA1 Message Date
KKRainbow 0f2f019f16 ci(core): keep matrix jobs running after transient failures
Disable fail-fast for the Core build matrix so one flaky external
download does not cancel every other platform build. This preserves
successful matrix results and makes failed jobs independently
retryable.
2026-08-13 06:40:33 +08:00
KKRainbow 86bf3df0fd fix(shared-tun): restore routed IPv6 replies
Forward route-owned packets unchanged when a member has no address
suitable for source translation. This preserves public IPv6 provider
replies while keeping cross-member source translation when an address
is available.

Keep the shared DNS route backend available in tun-only builds and
use the current error conversion in the FreeBSD IPv4 setup path.
2026-08-13 02:51:40 +08:00
KKRainbow daf30bff6e test(shared-tun): satisfy strict clippy check
Return the shared route owner count expression directly so the new root
integration helper passes the workspace -D warnings lint gate.
2026-08-13 01:59:44 +08:00
KKRainbow b5bc4a853d fix(shared-tun): close mobile routing gaps
Translate IPv6 traffic across shared mobile members, including
transport checksums and reverse-flow handling.

Normalize saved Android TUN configs and detach runtimes before the
VPN fd closes. Restore the mainline lockfile after resolving the merge.
2026-08-13 01:43:40 +08:00
KKRainbow 144e306562 Merge upstream main into shared virtual NIC work
Port shared TUN ownership, routing, mobile source dispatch, and
Magic DNS route claims onto the native runtime-host architecture.

Preserve per-member lifecycle and add focused desktop, mobile, netlink,
GUI, FFI, and root integration validation for the merged code.
2026-08-13 00:56:52 +08:00
sijie.sun ba86e47cc6 fix: adapt mobile ffi tun fd API 2026-06-16 20:31:22 +08:00
sijie.sun d742fa34aa refactor: simplify shared virtual nic helpers 2026-06-16 12:46:28 +08:00
sijie.sun 800c840bb5 shared-tun: preserve member ownership on mobile
Introduce shared NIC source ownership and dispatcher handling so a
single dev_name can be shared by multiple tun-enabled instances while
keeping per-member IP and route claims distinct.

Pass Android VpnService fd registration with per-instance source and
route claims. Keep the VPN address list limited to real member
addresses and allow AF_INET6 without installing hidden fd00::1.

Invalidate dispatcher flow and NAT state when source ownership changes
or a member unregisters. Avoid rewriting non-first IPv4 fragment
payloads, and adjust fragmented TCP/UDP checksums without recomputing
over partial fragment bodies.

Preserve source-owner routing for equal-prefix route conflicts, keep
ICMP echo NAT entries distinct by echo id, and retry stale flow-owner
send failures from the original packet. Only record NAT state after a
translated packet is accepted by its member.

Apply Linux IPv4 route preferred-source hints for shared routes and keep
route repair paths source-aware. Keep Darwin ifcfg access scoped to
cleanup-only paths where netns is not available.
2026-06-16 02:16:25 +08:00
sijie.sun ca17e856a6 fix: tighten shared virtual nic lifecycle
Key shared virtual NIC registry entries by both netns and device
name. Instances in different namespaces can no longer reuse the same
underlying device by accident.

Store each member's configured MTU separately and publish it as
the initial MTU claim after device creation. Later members no longer
inherit only the first member's MTU.

Shut down the dispatcher when the last current member leaves, and
invalidate the shared NIC if member cleanup fails. The registry entry
remains available for a later clean recreate.

Keep Magic DNS fake-IP routes owned by shared TUN members through
the shared backend. Stopping one member no longer removes the route
while another shared member still owns it, and Linux retained routes
are re-applied after address deletion.

Extend the shared TUN Magic DNS integration test to drop one shared
member first, verify DNS still works through the remaining member,
and then verify final cleanup after the last member exits.
2026-06-15 01:34:29 +08:00
sijie.sun d87624b353 fix: address shared tun ci failures 2026-06-14 23:14:45 +08:00
sijie.sun a7ab60e0e4 fix: support android shared tun fd groups
Android previously treated setTunFd as a single-instance update, and the
VpnService plugin could only expose one IPv4 address. That made shared
TUN members disable each other or leave only one address configured.

Group enabled Android TUN instances by shared dev_name, send the fd to
every compatible member, and only disable incompatible TUN users. Build
the Android VPN request from the whole running shared group and pass
every IPv4 address to VpnService.

The shared mobile dispatcher now owns current fd device state on a
process-level runtime. New setTunFd calls replace that state even when
the raw fd number is reused, and mobile TUN read/write/create failures
rebuild with backoff while preserving member registrations.

Protect shared member cleanup with per-registration ownership tokens, so
old async cleanup cannot unregister a recreated member or remove its
source claims. Mobile source addresses are registered in the dispatcher
without applying OS ifcfg changes, so Android-originated packets return
through the owning instance.

When one shared member stops while another remains, notify the frontend
to recalculate the VpnService config instead of leaving stale addresses
and routes. Serialize Android VpnService config recalculation so stale
async events cannot overwrite newer shared-group state.

If one shared member is not ready, rebuild from the healthy members and
retry the missing member later. If no healthy member remains, stop the
Android VPN service instead of keeping stale routes active.
2026-06-14 16:24:08 +08:00
sijie.sun 3d0d2bed9e fix: preserve macos shared tun ipv4 aliases 2026-06-14 11:38:12 +08:00
sijie.sun 7ef023fcdd test: cover shared tun dynamic proxy routes
Cover shared tun duplicate proxy CIDR failover and runtime proxy
CIDR add/remove through netns integration tests.

Start IP proxy from config patch events so a node that did not have
proxy CIDRs at startup can serve a later proxy network patch.

Ignore NotFound for shared NIC remove-side ifcfg cleanup so member
teardown does not poison shared owner state when the OS item is
already gone.

Add dispatcher coverage for TUN read failure invalidation and member
close notification.
2026-06-14 02:37:19 +08:00
sijie.sun 69d476ad13 test: cover shared tun subnet proxy paths
Add a shared tun subnet proxy topology with shared members on both
the source and destination sides. The test exercises ICMP, TCP
subnet proxy, and UDP subnet proxy against a real netns target.

Add focused KCP and QUIC TCP proxy variants that verify the selected
TcpProxyConnect transport label. The helper waits for real proxy CIDR
reachability before running transport-specific checks to avoid racing
route installation.
2026-06-14 01:26:19 +08:00
sijie.sun cf3fcb75b5 test: cover shared tun magic dns in netns
Add an end-to-end shared tun Magic DNS test with two members
sharing one device in the same namespace. The test checks real
netns ping paths and queries the Magic DNS fake IP through dig.

Teach DnsRunner to pass the instance netns into
MagicDnsServerInstance so fake DNS route add/remove operations run
where the tun device exists. Keep the existing constructors as
wrappers so current callers stay source-compatible.
2026-06-14 01:26:19 +08:00
sijie.sun 17195a5cc4 fix: dispatch shared tun packets by source owner
Track each shared tun member's claimed IPv4 and IPv6 addresses in
the dispatcher.

When a packet arrives from the shared device, keep the existing flow
affinity lookup first, then route by source address owner before
falling back to an arbitrary member. This keeps public IPv6 auto
traffic on the member that owns the leased address while preserving
the simple local-origin fallback.

Migrate high-value shared tun coverage from the backup branch: real
member communication, no_tun isolation, proxy CIDR forwarding, and
public IPv6 auto through a shared device.
2026-06-14 01:26:19 +08:00
sijie.sun 22bcfebe3e test: cover shared nic instance selection
Add focused tests for Instance nic context creation without creating a
real tun device.

Verify empty dev_name keeps the dedicated backend, while matching
shared dev_name values reuse the same shared virtual nic and get fresh
member ids on each recreation.
2026-06-14 01:26:19 +08:00
sijie.sun 47a2fb19a6 feat: wire shared nic into instance setup
Route normal, DHCP, and mobile nic creation through one helper that
selects the shared backend when dev_name is configured.

Keep dedicated nic creation as the default for configs without a
dev_name, and keep no_tun handling in the existing callers.

Store a process-wide shared virtual nic registry behind each instance
so networks with the same dev_name attach to the same shared device.
2026-06-14 01:26:19 +08:00
sijie.sun 62e43855cb feat: support shared nic mobile fd
Create the shared virtual nic dispatcher from the mobile tun fd.

Later shared members attach through their ring tunnel and do not
consume another fd. Dedicated mobile nic creation stays unchanged.

Fix mobile no-magic-dns cfg so the mobile tun path checks without
enabling magic-dns.
2026-06-14 01:26:19 +08:00
sijie.sun 5efbf4853a feat: apply shared nic member ifcfg
Route shared nic IP and route configuration through member claims.
Repeated addresses or routes now touch the OS device only when the
first owner appears, and are removed only after the last owner leaves.

Forward NicBackend shared operations to the member claim layer.
Dynamic proxy and public IPv6 route updates now go through NicBackend,
so they use the same ownership merge behavior.

Clean member claims when shared member registration is dropped. Keep
raw ifcfg access for non-Linux platform cleanup only, and document
that this raw path does not carry the netns guard.
2026-06-14 01:26:19 +08:00
sijie.sun 3e0bde68b8 perf: bound shared nic flow eviction
Replace full flow table clears with a small FIFO order queue so overflow evicts only older flow ownership hints.

Keep lookup read-only on the packet hot path and prune stale queue entries when a shared member unregisters.
2026-06-14 01:26:19 +08:00
sijie.sun bda06ebb61 perf: speed up shared nic flow lookup
Parse shared virtual nic flow keys directly from IP payload bytes instead of building pnet packet wrappers on the dispatcher hot path.

Use a local HashMap for flow owner lookup and clear the bounded table on overflow instead of removing a BTreeMap entry by key order.
2026-06-14 01:26:19 +08:00
sijie.sun be0859aca6 refactor: keep shared dispatcher state local
Move shared member and flow ownership state into the dispatcher task.

Use control messages for member register and unregister events.

Keep the member table lock off the packet forwarding path.
2026-06-14 01:26:19 +08:00
sijie.sun b2f1b37336 refactor: add shared virtual nic dispatcher
Add the shared NIC dispatcher that owns the underlying VirtualNic tunnel.

Connect member ring tunnels through a private member table.

Track reverse flow ownership from packets written by a member.

Replies read from the shared device go back to the same member.

Packets without an owner fall back to any active member.

Mark the shared NIC invalid when the underlying tunnel closes or fails.

Notify members so their NicCtx can rebuild.
2026-06-14 01:26:19 +08:00
sijie.sun 583fb45939 refactor: reuse ring tunnel for shared members
Use create_ring_tunnel_pair for member-level shared NIC tunnels.

That replaces the custom packet stream and sink implementation.

Keep one side of each ring tunnel in SharedVirtualNic's member map.

Later dispatcher code can connect those endpoints to the shared device.
2026-06-14 01:26:19 +08:00
sijie.sun a343a907a9 refactor: add shared nic ctx constructor
Add a shared NicCtx constructor that obtains a SharedVirtualNicMember from the registry and wraps it in NicBackend::Shared.

Keep NicCtx::new on the dedicated backend path so existing runtime behavior does not change before the shared tunnel implementation is ready.
2026-06-14 01:26:19 +08:00
sijie.sun b1ac64f40f refactor: route nic ctx through nic backend
Replace NicCtx's direct VirtualNic field with NicBackend while keeping the constructor on the dedicated backend path.

Move existing device creation, ifcfg lookup, IP assignment, and public IPv6 updater calls through backend helpers so shared mode can be wired in a later commit without changing the dedicated flow.
2026-06-14 01:26:19 +08:00
sijie.sun 122fb9b54e refactor: add shared nic backend skeleton
Introduce SharedVirtualNicMember as the per-instance handle for a shared virtual NIC and add a registry helper to create members.

Add NicBackend so NicCtx can later choose between dedicated VirtualNic and shared member-backed tunnel creation without changing the existing dedicated path.
2026-06-14 01:26:19 +08:00
sijie.sun 09780c1698 refactor: add shared virtual nic registry
Add a small registry keyed by dev_name so shared-mode setup can reuse one SharedVirtualNic per device name.

Track invalid shared NICs through a shared flag and replace them on the next get_or_create call.
2026-06-14 01:26:19 +08:00
sijie.sun e533b48244 refactor: decouple virtual nic from global ctx
Move TUN-specific configuration into VirtualNicConfig so VirtualNic and SharedVirtualNic no longer depend on ArcGlobalCtx.

Keep global context side effects in NicCtx, including the Windows generated device-name writeback.
2026-06-14 01:26:19 +08:00
sijie.sun c0bd050742 add shared virtual nic 2026-06-14 01:26:19 +08:00
35 changed files with 8026 additions and 354 deletions
+1 -1
View File
@@ -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
View File
@@ -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"
+1 -1
View File
@@ -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
}
}
}
+342 -32
View File
@@ -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!({
+18 -3
View File
@@ -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,
})
}
+10
View File
@@ -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),
+288 -43
View File
@@ -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()
}
+1
View File
@@ -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",
+90 -13
View File
@@ -1,10 +1,42 @@
use std::net::Ipv4Addr;
use std::{
collections::{BTreeMap, BTreeSet},
net::Ipv4Addr,
sync::Arc,
};
use super::{Error, IfConfiguerTrait, cidr_to_subnet_mask, run_shell_cmd};
use 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> {
+21
View File
@@ -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 {}
+174 -17
View File
@@ -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 {
+72
View File
@@ -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![
+1
View File
@@ -22,6 +22,7 @@ use winreg::{
};
use super::{Error, IfConfiguerTrait};
#[derive(Default)]
pub struct WindowsIfConfiger {}
fn format_win_error(error: u32) -> String {
+8 -1
View File
@@ -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,
+111 -10
View File
@@ -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;
+30
View File
@@ -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")]
+3
View File
@@ -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;
+39 -7
View File
@@ -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
+40 -5
View File
@@ -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
+3
View File
@@ -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")]
+561
View File
@@ -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;
}
+151
View File
@@ -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;
}
+2
View File
@@ -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>,
}