refactor(core): separate portable core from native runtime (#2451)

Create easytier-core as the portable owner of configuration,
connectivity, tunnels, peer and routing state, gateways, management,
the data plane, and instance lifecycle. Keep operating-system
integration, native protocol engines, process startup, and presentation
in easytier behind explicit Host capability adapters.

Create easytier-proto to own schemas, generated RPC types, descriptors,
and feature-scoped protocol slices. Remove runtime protobuf reflection
from core while preserving unknown route-peer fields across forwarding.

Normalize instance construction through CoreInstance, CoreHostAdapters,
CoreProcessRuntime, and InstanceManager. Make the runtime config store
the only authoritative mutable configuration after startup.

Move the portable TCP/UDP data plane into core and extract a generic
OperationBroker for completion, cancellation, disposal, and capacity
accounting. Expose the session-based FFI v2 completion API and keep the
WASI guest ABI, wire schemas, and adapters with core.

Migrate CLI, GUI, web, FFI, Android JNI, OHOS, uptime, and mobile
consumers to the shared manager and core state. Add explicit user/web
config ownership and revision-aware web reconciliation.

Preserve configuration, wire, and management behavior while fixing
regressions discovered by the full platform and integration matrix:

- inherit advertised relay capabilities in foreign networks;
- refresh OSPF peer state immediately after runtime config changes;
- restore CLI GlobalCtx event output without forcing GUI logging;
- retain legacy encryption names and standalone RPC tunnel metadata;
- restore ICMP host composition and fragmented UDP handling;
- use portable 64-bit atomics on 32-bit MIPS targets; and
- retain discarded operations until late cancellation completes.

Validate the refactor across 45 GitHub checks, including Linux, macOS,
Windows, FreeBSD, web, GUI, Android, OHOS, feature profiles, and
three-node and subnet-proxy integration tests.

BREAKING CHANGE: internal Rust module paths are not preserved. Legacy
native data-plane APIs are replaced by the session-based FFI v2 API.
The dedicated Android data-plane wrapper is removed.
This commit is contained in:
KKRainbow authored and GitHub committed 2026-07-26 15:41:55 +08:00
1 parent 346f32d3d0
commit 021f523431
523 files changed
+102813 -67095

No files matched your search

@@ -0,0 +1,132 @@
#[cfg(feature = "management")]
use std::{
collections::VecDeque,
sync::{Arc, RwLock},
};
#[cfg(feature = "management")]
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
#[cfg(feature = "management")]
use tokio_util::task::AbortOnDropHandle;
use crate::common::global_ctx::ArcGlobalCtx;
#[cfg(feature = "management")]
use crate::common::global_ctx::{EventBusSubscriber, GlobalCtxEvent};
#[cfg(feature = "management")]
#[derive(serde::Serialize)]
struct ManagementEvent {
time: chrono::DateTime<chrono::Local>,
event: GlobalCtxEvent,
}
#[cfg(feature = "management")]
pub(super) struct EventJournal {
global_ctx: ArcGlobalCtx,
events: Arc<RwLock<VecDeque<String>>>,
receiver: Mutex<Option<EventBusSubscriber>>,
task: Mutex<Option<AbortOnDropHandle<()>>>,
}
#[cfg(not(feature = "management"))]
pub(super) struct EventJournal;
#[cfg(feature = "management")]
impl EventJournal {
pub(super) fn new(global_ctx: &ArcGlobalCtx) -> Self {
Self {
global_ctx: global_ctx.clone(),
events: Arc::new(RwLock::new(VecDeque::new())),
receiver: Mutex::new(Some(global_ctx.subscribe())),
task: Mutex::new(None),
}
}
pub(super) async fn start(&self, cancel: CancellationToken) {
let Some(mut receiver) = self.receiver.lock().await.take() else {
return;
};
let events = self.events.clone();
let task = tokio::spawn(async move {
loop {
let event = tokio::select! {
_ = cancel.cancelled() => return,
event = receiver.recv() => match event {
Ok(event) => event,
Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
},
};
let event = ManagementEvent {
time: chrono::Local::now(),
event,
};
let Ok(event) = serde_json::to_string(&event) else {
continue;
};
let mut events = events.write().unwrap();
events.push_front(event);
if events.len() > 20 {
events.pop_back();
}
}
});
self.task.lock().await.replace(AbortOnDropHandle::new(task));
}
pub(super) async fn stop(&self) {
if let Some(task) = self.task.lock().await.take() {
let _ = task.await;
}
}
pub(super) fn events(&self) -> Vec<String> {
self.events.read().unwrap().iter().cloned().collect()
}
pub(super) fn synchronize_config(
&self,
patch: &crate::proto::api::config::InstanceConfigPatch,
) {
if let Some(hostname) = &patch.hostname {
self.global_ctx.set_hostname(hostname.clone());
}
if let Some(ipv4) = patch.ipv4.as_ref()
&& !self.global_ctx.config.get_dhcp()
{
self.global_ctx.set_ipv4(Some((*ipv4).into()));
}
if let Some(ipv6) = patch.ipv6.as_ref() {
self.global_ctx.set_ipv6(Some((*ipv6).into()));
}
if let Some(disable_relay_data) = patch.disable_relay_data {
let mut flags = self.global_ctx.get_flags();
flags.disable_relay_data = disable_relay_data;
self.global_ctx.set_flags(flags);
}
}
pub(super) fn publish_config_patch(
&self,
patch: crate::proto::api::config::InstanceConfigPatch,
) {
self.global_ctx
.issue_event(GlobalCtxEvent::ConfigPatched(patch));
}
}
#[cfg(not(feature = "management"))]
impl EventJournal {
pub(super) fn new(_global_ctx: &ArcGlobalCtx) -> Self {
Self
}
pub(super) async fn start(&self, _cancel: CancellationToken) {}
pub(super) async fn stop(&self) {}
pub(super) fn events(&self) -> Vec<String> {
Vec::new()
}
}
@@ -0,0 +1,44 @@
use std::sync::Arc;
use easytier_core::{
gateway::dhcp::DhcpIpv4Host,
instance::{CorePacketPlane, InstanceRuntimeHost},
};
use super::NativeInstanceRuntimeHost;
#[async_trait::async_trait]
impl InstanceRuntimeHost for NativeInstanceRuntimeHost {
async fn prepare(
&self,
packet_plane: Arc<CorePacketPlane>,
) -> anyhow::Result<Option<Arc<dyn DhcpIpv4Host>>> {
self.prepare_runtime(packet_plane).await
}
async fn shutdown(&self) {
self.shutdown_runtime().await;
}
fn request_shutdown(&self) {
self.request_runtime_shutdown();
}
fn management_events(&self) -> Vec<String> {
self.management_events_snapshot()
}
#[cfg(feature = "management")]
fn synchronize_config(&self, patch: &crate::proto::api::config::InstanceConfigPatch) {
self.event_journal.synchronize_config(patch);
}
#[cfg(feature = "management")]
fn publish_config_patch(&self, patch: crate::proto::api::config::InstanceConfigPatch) {
self.event_journal.publish_config_patch(patch);
}
fn attach_tun_fd(&self, fd: i32) -> anyhow::Result<()> {
self.attach_runtime_tun_fd(fd)
}
}
@@ -0,0 +1,74 @@
use cidr::Ipv4Inet;
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::{
common::config::ConfigLoader as _,
instance::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner},
};
#[derive(Default)]
pub(super) struct MagicDnsRuntime {
#[cfg(feature = "magic-dns")]
active: Option<MagicDnsTask>,
}
#[cfg(feature = "magic-dns")]
struct MagicDnsTask {
task: AbortOnDropHandle<()>,
cancel: CancellationToken,
}
impl MagicDnsRuntime {
#[cfg(feature = "magic-dns")]
pub(super) fn start(
global_ctx: ArcGlobalCtx,
packet_plane: std::sync::Arc<CorePacketPlane>,
tun_dev: Option<String>,
tun_ip: Ipv4Inet,
) -> Self {
let active = global_ctx.config.get_flags().accept_dns.then(|| {
let mut runner = DnsRunner::new(
packet_plane,
global_ctx,
tun_dev,
tun_ip,
MAGIC_DNS_FAKE_IP.parse().unwrap(),
);
let cancel = CancellationToken::new();
let task_cancel = cancel.clone();
let task = tokio::spawn(async move {
let _ = runner.run(task_cancel).await;
});
MagicDnsTask {
task: AbortOnDropHandle::new(task),
cancel,
}
});
Self { active }
}
#[cfg(not(feature = "magic-dns"))]
pub(super) fn start(
_global_ctx: ArcGlobalCtx,
_packet_plane: std::sync::Arc<CorePacketPlane>,
_tun_dev: Option<String>,
_tun_ip: Ipv4Inet,
) -> Self {
Self::default()
}
#[cfg(feature = "magic-dns")]
pub(super) async fn stop(&mut self) {
if let Some(active) = self.active.take() {
active.cancel.cancel();
let _ = active.task.await;
}
}
#[cfg(not(feature = "magic-dns"))]
pub(super) async fn stop(&mut self) {}
}
@@ -0,0 +1,78 @@
use std::{any::Any, sync::Arc};
use tokio::{sync::Mutex, task::JoinSet};
use super::{HostPacketReceiver, MagicDnsRuntime};
use crate::instance::virtual_nic::NicCtx;
struct NicCtxContainer {
_nic_ctx: Option<Box<dyn Any + Send>>,
magic_dns: MagicDnsRuntime,
}
impl NicCtxContainer {
fn new(nic_ctx: NicCtx, magic_dns: MagicDnsRuntime) -> Self {
Self {
_nic_ctx: Some(Box::new(nic_ctx)),
magic_dns,
}
}
fn packet_drain(tasks: JoinSet<()>) -> Self {
Self {
_nic_ctx: Some(Box::new(tasks)),
magic_dns: MagicDnsRuntime::default(),
}
}
}
#[derive(Clone)]
pub(super) struct TunNicState {
nic_ctx: Arc<Mutex<Option<NicCtxContainer>>>,
receiver: Arc<Mutex<HostPacketReceiver>>,
}
impl TunNicState {
pub(super) fn new(receiver: HostPacketReceiver) -> Self {
Self {
nic_ctx: Arc::new(Mutex::new(None)),
receiver: Arc::new(Mutex::new(receiver)),
}
}
pub(super) fn receiver(&self) -> Arc<Mutex<HostPacketReceiver>> {
self.receiver.clone()
}
pub(super) async fn stop(&self) {
let mut old = self.nic_ctx.lock().await.take();
if let Some(nic) = old.as_mut() {
nic.magic_dns.stop().await;
}
drop(old);
}
pub(super) async fn drain(&self) {
self.stop().await;
let receiver = self.receiver.clone();
let mut tasks = JoinSet::new();
tasks.spawn(async move {
let mut receiver = receiver.lock().await;
while let Some(packet) = receiver.recv().await {
tracing::trace!(?packet, "discarded packet without a native interface");
}
});
self.nic_ctx
.lock()
.await
.replace(NicCtxContainer::packet_drain(tasks));
}
pub(super) async fn install(&self, nic: NicCtx, magic_dns: MagicDnsRuntime) {
self.stop().await;
self.nic_ctx
.lock()
.await
.replace(NicCtxContainer::new(nic, magic_dns));
}
}
@@ -0,0 +1,253 @@
use std::{sync::Arc, time::Duration};
use anyhow::Context as _;
use cidr::Ipv4Inet;
use easytier_core::{
gateway::dhcp::{DhcpIpv4ApplyOutcome, DhcpIpv4ApplyPermit, DhcpIpv4Host},
instance::CorePacketPlane,
};
use futures::FutureExt as _;
use tokio::{
sync::{Mutex, Notify, oneshot},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use super::{HostPacketReceiver, MagicDnsRuntime, tun_common::TunNicState};
use crate::{
common::{
config::ConfigLoader as _,
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
},
instance::virtual_nic::NicCtx,
};
pub(super) struct NativeTunRuntime {
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
nic: TunNicState,
static_ip_task: Mutex<Option<JoinHandle<()>>>,
}
impl NativeTunRuntime {
pub(super) fn new(
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
peer_packet_receiver: HostPacketReceiver,
) -> Self {
Self {
global_ctx,
cancel,
nic: TunNicState::new(peer_packet_receiver),
static_ip_task: Mutex::new(None),
}
}
fn report_static_ip_cancelled(output: &mut Option<oneshot::Sender<Result<(), Error>>>) {
if let Some(output) = output.take() {
let _ = output.send(Err(anyhow::anyhow!(
"instance is closing; static IP setup cancelled"
)
.into()));
}
}
async fn start_static_ip(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
let ipv4 = self.global_ctx.get_ipv4();
let ipv6 = self.global_ctx.get_ipv6();
if ipv4.is_none() && ipv6.is_none() {
return Ok(());
}
let nic_state = self.nic.clone();
let cancel = self.cancel.clone();
let global_ctx = self.global_ctx.clone();
let receiver = self.nic.receiver();
let (output, first_round) = oneshot::channel();
let task = tokio::spawn(async move {
let mut output = Some(output);
loop {
if cancel.is_cancelled() {
Self::report_static_ip_cancelled(&mut output);
return;
}
let closed = Arc::new(Notify::new());
let mut nic = NicCtx::new(
global_ctx.clone(),
packet_plane.clone(),
receiver.clone(),
closed.clone(),
);
let result = tokio::select! {
biased;
_ = cancel.cancelled() => {
Self::report_static_ip_cancelled(&mut output);
return;
}
result = nic.run(ipv4, ipv6) => result,
};
if let Err(error) = result {
if let Some(output) = output.take() {
let _ = output.send(Err(error));
return;
}
tracing::error!(?error, "failed to create native interface");
tokio::select! {
_ = cancel.cancelled() => return,
_ = tokio::time::sleep(Duration::from_secs(1)) => {}
}
continue;
}
let magic_dns = if let Some(ip) = ipv4 {
MagicDnsRuntime::start(
global_ctx.clone(),
packet_plane.clone(),
nic.ifname().await,
ip,
)
} else {
MagicDnsRuntime::default()
};
nic_state.install(nic, magic_dns).await;
if let Some(output) = output.take() {
let _ = output.send(Ok(()));
}
tokio::select! {
_ = cancel.cancelled() => return,
_ = closed.notified() => {}
}
}
});
self.static_ip_task.lock().await.replace(task);
first_round
.await
.context("static IP setup task stopped")??;
Ok(())
}
pub(super) async fn prepare(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
self.nic.drain().await;
if !self.global_ctx.config.get_flags().no_tun {
self.start_static_ip(packet_plane).await?;
}
Ok(())
}
pub(super) async fn shutdown(&self) {
if let Some(task) = self.static_ip_task.lock().await.take() {
let _ = task.await;
}
self.nic.stop().await;
}
pub(super) fn attach_fd(&self, _fd: i32) -> anyhow::Result<()> {
anyhow::bail!("external TUN attachment is only supported on mobile Hosts")
}
pub(super) fn dhcp_host(
&self,
operation: Arc<Mutex<()>>,
packet_plane: Arc<CorePacketPlane>,
) -> Arc<dyn DhcpIpv4Host> {
Arc::new(NativeDhcpIpv4Host {
global_ctx: self.global_ctx.clone(),
operation,
cancel: self.cancel.clone(),
nic: self.nic.clone(),
closed: Arc::new(Notify::new()),
packet_plane,
})
}
}
struct NativeDhcpIpv4Host {
global_ctx: ArcGlobalCtx,
operation: Arc<Mutex<()>>,
cancel: CancellationToken,
nic: TunNicState,
closed: Arc<Notify>,
packet_plane: Arc<CorePacketPlane>,
}
impl NativeDhcpIpv4Host {
fn ensure_open(&self) -> anyhow::Result<()> {
if self.cancel.is_cancelled() {
anyhow::bail!("instance is closing; DHCP update cancelled");
}
Ok(())
}
async fn apply(&self, next: Option<Ipv4Inet>) -> anyhow::Result<Option<Ipv4Inet>> {
self.ensure_open()?;
tokio::select! {
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; DHCP update cancelled"),
_ = self.nic.drain() => {}
}
self.ensure_open()?;
let Some(ip) = next else {
self.global_ctx.set_ipv4(None);
return Ok(None);
};
if self.global_ctx.no_tun() {
self.global_ctx.set_ipv4(Some(ip));
return Ok(Some(ip));
}
let mut nic = NicCtx::new(
self.global_ctx.clone(),
self.packet_plane.clone(),
self.nic.receiver(),
self.closed.clone(),
);
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 magic_dns = MagicDnsRuntime::start(
self.global_ctx.clone(),
self.packet_plane.clone(),
nic.ifname().await,
ip,
);
self.nic.install(nic, magic_dns).await;
self.global_ctx.set_ipv4(Some(ip));
Ok(Some(ip))
}
}
#[async_trait::async_trait]
impl DhcpIpv4Host for NativeDhcpIpv4Host {
fn take_interface_closed(&self) -> bool {
self.closed.notified().now_or_never().is_some()
}
async fn apply_dhcp_ipv4(
&self,
_previous: Option<Ipv4Inet>,
next: Option<Ipv4Inet>,
) -> DhcpIpv4ApplyOutcome {
let permit = self.operation.clone().lock_owned().await;
let outcome = match self.apply(next).await {
Ok(actual) => DhcpIpv4ApplyOutcome::applied(actual),
Err(error) => DhcpIpv4ApplyOutcome::failed(self.global_ctx.get_ipv4(), error),
};
outcome.with_permit(DhcpIpv4ApplyPermit::new(permit))
}
fn publish_dhcp_ipv4(
&self,
previous: Option<Ipv4Inet>,
requested: Option<Ipv4Inet>,
actual: Option<Ipv4Inet>,
) {
let event = if requested.is_none() {
GlobalCtxEvent::DhcpIpv4Conflicted(previous)
} else {
GlobalCtxEvent::DhcpIpv4Changed(previous, actual)
};
self.global_ctx.issue_event(event);
}
}
@@ -0,0 +1,109 @@
use std::sync::Arc;
use cidr::Ipv4Inet;
use easytier_core::{
gateway::dhcp::{DhcpIpv4ApplyOutcome, DhcpIpv4ApplyPermit, DhcpIpv4Host},
instance::CorePacketPlane,
};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use super::HostPacketReceiver;
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent};
pub(super) struct NativeTunRuntime {
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
}
impl NativeTunRuntime {
pub(super) fn new(
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
peer_packet_receiver: HostPacketReceiver,
) -> Self {
drop(peer_packet_receiver);
Self { global_ctx, cancel }
}
pub(super) async fn prepare(&self, _packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
Ok(())
}
pub(super) async fn shutdown(&self) {}
pub(super) fn attach_fd(&self, _fd: i32) -> anyhow::Result<()> {
anyhow::bail!("external TUN attachment is only supported on mobile Hosts")
}
pub(super) fn dhcp_host(
&self,
operation: Arc<Mutex<()>>,
_packet_plane: Arc<CorePacketPlane>,
) -> Arc<dyn DhcpIpv4Host> {
Arc::new(NativeDhcpIpv4Host {
global_ctx: self.global_ctx.clone(),
operation,
cancel: self.cancel.clone(),
})
}
}
struct NativeDhcpIpv4Host {
global_ctx: ArcGlobalCtx,
operation: Arc<Mutex<()>>,
cancel: CancellationToken,
}
impl NativeDhcpIpv4Host {
fn ensure_open(&self) -> anyhow::Result<()> {
if self.cancel.is_cancelled() {
anyhow::bail!("instance is closing; DHCP update cancelled");
}
Ok(())
}
async fn apply(&self, next: Option<Ipv4Inet>) -> anyhow::Result<Option<Ipv4Inet>> {
self.ensure_open()?;
let Some(ip) = next else {
self.global_ctx.set_ipv4(None);
return Ok(None);
};
self.global_ctx.set_ipv4(Some(ip));
Ok(Some(ip))
}
}
#[async_trait::async_trait]
impl DhcpIpv4Host for NativeDhcpIpv4Host {
fn take_interface_closed(&self) -> bool {
false
}
async fn apply_dhcp_ipv4(
&self,
_previous: Option<Ipv4Inet>,
next: Option<Ipv4Inet>,
) -> DhcpIpv4ApplyOutcome {
let permit = self.operation.clone().lock_owned().await;
let outcome = match self.apply(next).await {
Ok(actual) => DhcpIpv4ApplyOutcome::applied(actual),
Err(error) => DhcpIpv4ApplyOutcome::failed(self.global_ctx.get_ipv4(), error),
};
outcome.with_permit(DhcpIpv4ApplyPermit::new(permit))
}
fn publish_dhcp_ipv4(
&self,
previous: Option<Ipv4Inet>,
requested: Option<Ipv4Inet>,
actual: Option<Ipv4Inet>,
) {
let event = if requested.is_none() {
GlobalCtxEvent::DhcpIpv4Conflicted(previous)
} else {
GlobalCtxEvent::DhcpIpv4Changed(previous, actual)
};
self.global_ctx.issue_event(event);
}
}
@@ -0,0 +1,193 @@
use std::sync::Arc;
use anyhow::Context as _;
use cidr::Ipv4Inet;
use easytier_core::{
gateway::dhcp::{DhcpIpv4ApplyOutcome, DhcpIpv4ApplyPermit, DhcpIpv4Host},
instance::CorePacketPlane,
};
use futures::FutureExt as _;
use tokio::sync::{Mutex, Notify, mpsc};
use tokio_util::sync::CancellationToken;
use super::{HostPacketReceiver, MagicDnsRuntime, tun_common::TunNicState};
use crate::{
common::global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
instance::virtual_nic::NicCtx,
};
pub(super) struct NativeTunRuntime {
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
nic: TunNicState,
tun_fd: mpsc::Sender<i32>,
tun_fd_receiver: Mutex<Option<mpsc::Receiver<i32>>>,
task: Mutex<Option<tokio::task::JoinHandle<()>>>,
}
impl NativeTunRuntime {
pub(super) fn new(
global_ctx: ArcGlobalCtx,
cancel: CancellationToken,
peer_packet_receiver: HostPacketReceiver,
) -> Self {
let (tun_fd, tun_fd_receiver) = mpsc::channel(16);
Self {
global_ctx,
cancel,
nic: TunNicState::new(peer_packet_receiver),
tun_fd,
tun_fd_receiver: Mutex::new(Some(tun_fd_receiver)),
task: Mutex::new(None),
}
}
async fn install_mobile_tun(
nic_state: TunNicState,
global_ctx: ArcGlobalCtx,
packet_plane: Arc<CorePacketPlane>,
fd: i32,
) -> anyhow::Result<()> {
nic_state.drain().await;
if fd <= 0 {
return Ok(());
}
let closed = Arc::new(Notify::new());
let mut nic = NicCtx::new(
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();
nic_state.install(nic, magic_dns).await;
Ok(())
}
pub(super) async fn prepare(&self, packet_plane: Arc<CorePacketPlane>) -> anyhow::Result<()> {
self.nic.drain().await;
let Some(mut tun_fds) = self.tun_fd_receiver.lock().await.take() else {
return Ok(());
};
let nic_state = self.nic.clone();
let global_ctx = self.global_ctx.clone();
let cancel = self.cancel.clone();
self.task.lock().await.replace(tokio::spawn(async move {
loop {
let fd = tokio::select! {
_ = cancel.cancelled() => return,
fd = tun_fds.recv() => match fd { Some(fd) => fd, None => return },
};
if let Err(error) = Self::install_mobile_tun(
nic_state.clone(),
global_ctx.clone(),
packet_plane.clone(),
fd,
)
.await
{
tracing::error!(?error, "failed to attach mobile TUN fd");
}
}
}));
Ok(())
}
pub(super) async fn shutdown(&self) {
if let Some(task) = self.task.lock().await.take() {
let _ = task.await;
}
self.nic.stop().await;
}
pub(super) fn attach_fd(&self, fd: i32) -> anyhow::Result<()> {
self.tun_fd
.try_send(fd)
.map_err(|error| anyhow::anyhow!("failed to send TUN fd: {error}"))
}
pub(super) fn dhcp_host(
&self,
operation: Arc<Mutex<()>>,
_packet_plane: Arc<CorePacketPlane>,
) -> Arc<dyn DhcpIpv4Host> {
Arc::new(NativeDhcpIpv4Host {
global_ctx: self.global_ctx.clone(),
operation,
cancel: self.cancel.clone(),
nic: self.nic.clone(),
closed: Arc::new(Notify::new()),
})
}
}
struct NativeDhcpIpv4Host {
global_ctx: ArcGlobalCtx,
operation: Arc<Mutex<()>>,
cancel: CancellationToken,
nic: TunNicState,
closed: Arc<Notify>,
}
impl NativeDhcpIpv4Host {
fn ensure_open(&self) -> anyhow::Result<()> {
if self.cancel.is_cancelled() {
anyhow::bail!("instance is closing; DHCP update cancelled");
}
Ok(())
}
async fn apply(&self, next: Option<Ipv4Inet>) -> anyhow::Result<Option<Ipv4Inet>> {
self.ensure_open()?;
tokio::select! {
_ = self.cancel.cancelled() => anyhow::bail!("instance is closing; DHCP update cancelled"),
_ = self.nic.drain() => {}
}
self.ensure_open()?;
let Some(ip) = next else {
self.global_ctx.set_ipv4(None);
return Ok(None);
};
self.global_ctx.set_ipv4(Some(ip));
Ok(Some(ip))
}
}
#[async_trait::async_trait]
impl DhcpIpv4Host for NativeDhcpIpv4Host {
fn take_interface_closed(&self) -> bool {
self.closed.notified().now_or_never().is_some()
}
async fn apply_dhcp_ipv4(
&self,
_previous: Option<Ipv4Inet>,
next: Option<Ipv4Inet>,
) -> DhcpIpv4ApplyOutcome {
let permit = self.operation.clone().lock_owned().await;
let outcome = match self.apply(next).await {
Ok(actual) => DhcpIpv4ApplyOutcome::applied(actual),
Err(error) => DhcpIpv4ApplyOutcome::failed(self.global_ctx.get_ipv4(), error),
};
outcome.with_permit(DhcpIpv4ApplyPermit::new(permit))
}
fn publish_dhcp_ipv4(
&self,
previous: Option<Ipv4Inet>,
requested: Option<Ipv4Inet>,
actual: Option<Ipv4Inet>,
) {
let event = if requested.is_none() {
GlobalCtxEvent::DhcpIpv4Conflicted(previous)
} else {
GlobalCtxEvent::DhcpIpv4Changed(previous, actual)
};
self.global_ctx.issue_event(event);
}
}