Skip to content

Commit aa38ea6

Browse files
committed
Properly close TUN device from C lib
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
1 parent 2477759 commit aa38ea6

3 files changed

Lines changed: 117 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- When the mycelium library is used, the TUN device is now properly
13+
cleaned up after the node is stopped
14+
1015
## [0.7.8] - 2026-05-19
1116

1217
### Added

mycelium/src/ffi/exports.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,12 @@ pub unsafe extern "C" fn mycelium_start(
237237
}
238238
}
239239

240-
/// Halt the node and release its internal resources. The handle remains
241-
/// valid for `mycelium_is_running` queries (which will return false) until
242-
/// `mycelium_node_free` is called. No-op on an already-stopped node.
240+
/// Halt the node and release its internal resources. This call blocks until
241+
/// the node's background runtime has fully shut down (typically well under a
242+
/// second; bounded at a few seconds) and any network interface it created has
243+
/// been removed. The handle remains valid for `mycelium_is_running` queries
244+
/// (which will return false) until `mycelium_node_free` is called. No-op on an
245+
/// already-stopped node.
243246
///
244247
/// # Safety
245248
///
@@ -255,15 +258,17 @@ pub unsafe extern "C" fn mycelium_stop(node: *mut mycelium_node_t) -> i32 {
255258
Ok(g) => g,
256259
Err(_) => return error::set_and_return("node lock poisoned", MYCELIUM_ERR_INTERNAL),
257260
};
258-
match &mut *guard {
259-
NodeState::Running(h) => {
260-
info!("stopping mycelium node");
261-
h.stop();
262-
*guard = NodeState::Stopped;
263-
}
264-
NodeState::Stopped => {
265-
warn!("mycelium_stop called on a stopped node");
266-
}
261+
if matches!(&*guard, NodeState::Running(_)) {
262+
info!("stopping mycelium node");
263+
// Swap the handle out under the lock, then release the lock before
264+
// dropping the handle: `NodeHandle`'s `Drop` blocks until the
265+
// background runtime is fully torn down, and we must not hold the
266+
// `state` mutex while that happens.
267+
let old = std::mem::replace(&mut *guard, NodeState::Stopped);
268+
drop(guard);
269+
drop(old);
270+
} else {
271+
warn!("mycelium_stop called on a stopped node");
267272
}
268273
MYCELIUM_OK
269274
}

mycelium/src/node_handle.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::net::SocketAddr;
99
use std::sync::Arc;
1010

1111
use tokio::sync::Mutex;
12-
use tracing::{error, info};
12+
use tracing::{debug, error, info};
1313

1414
use crate::metrics::NoMetrics;
1515
use crate::{Config, Node};
@@ -72,13 +72,32 @@ pub fn create_tun_fd(tun_name: &str) -> Result<i32, std::io::Error> {
7272

7373
// ── NodeHandle ──────────────────────────────────────────────────────────────
7474

75+
/// Upper bound on how long node teardown waits for the background Tokio
76+
/// runtime to shut down. Asynchronous tasks (including the TUN reader/writer)
77+
/// are cancelled immediately when the runtime is dropped; this timeout only
78+
/// bounds the wait on any outstanding `spawn_blocking` work so a stuck
79+
/// blocking task cannot hang teardown forever.
80+
const SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
81+
7582
/// Handle to a running mycelium node. Holds a reference to the node for direct
7683
/// method calls and the Tokio runtime handle to drive them from non-async
7784
/// threads (e.g. Binder threads, C FFI callbacks).
7885
pub struct NodeHandle {
7986
node: Arc<Mutex<Node<NoMetrics>>>,
8087
rt_handle: tokio::runtime::Handle,
8188
shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
89+
/// Background OS thread that owns the Tokio runtime. Joined on drop so
90+
/// teardown is synchronous: once the join completes the runtime is gone
91+
/// and any TUN interface the node created has been removed.
92+
thread: Option<std::thread::JoinHandle<()>>,
93+
/// On Android the `tun` crate does not close the TUN file descriptor on
94+
/// drop (it assumes the fd is owned by Android's `VpnService`). Mycelium
95+
/// opens this fd itself via [`create_tun_fd`], so it owns it and must
96+
/// close it during teardown — otherwise the kernel keeps the
97+
/// non-persistent TUN interface alive. Closed in [`Drop`], after the
98+
/// background runtime has been joined.
99+
#[cfg(target_os = "android")]
100+
tun_fd: Option<i32>,
82101
}
83102

84103
impl NodeHandle {
@@ -87,10 +106,15 @@ impl NodeHandle {
87106
/// Blocks the calling thread until the node is ready (or fails to start).
88107
/// The caller provides a fully constructed [`Config`].
89108
pub fn start(config: Config<NoMetrics>) -> Result<Self, NodeError> {
109+
// On Android mycelium opens the TUN fd itself (see `create_tun_fd`),
110+
// so the handle owns it and is responsible for closing it on drop.
111+
#[cfg(target_os = "android")]
112+
let tun_fd = config.tun_fd;
113+
90114
let (result_tx, result_rx) = std::sync::mpsc::sync_channel(1);
91115
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
92116

93-
std::thread::spawn(move || {
117+
let thread = std::thread::spawn(move || {
94118
let rt = match tokio::runtime::Builder::new_multi_thread()
95119
.worker_threads(2)
96120
.enable_all()
@@ -121,14 +145,44 @@ impl NodeHandle {
121145
}
122146
}
123147
});
148+
149+
// Explicit, bounded teardown. Dropping the runtime cancels every
150+
// asynchronous task and drops the TUN device, then waits up to
151+
// SHUTDOWN_TIMEOUT for any `spawn_blocking` work to finish.
152+
debug!("shutting down node runtime");
153+
rt.shutdown_timeout(SHUTDOWN_TIMEOUT);
154+
debug!("node runtime shut down");
124155
});
125156

126-
let (node, rt_handle) = result_rx.recv().map_err(|_| NodeError::ThreadPanic)??;
157+
let (node, rt_handle) = match result_rx.recv() {
158+
Ok(Ok(pair)) => pair,
159+
other => {
160+
// The node failed to start. Wait for the background runtime
161+
// to finish tearing down, then release the TUN fd we opened
162+
// (on Android nothing else will close it).
163+
let _ = thread.join();
164+
#[cfg(target_os = "android")]
165+
if let Some(fd) = tun_fd {
166+
if fd >= 0 {
167+
// SAFETY: `fd` came from `create_tun_fd`; the runtime
168+
// that used it has been joined, so it is unreferenced.
169+
unsafe { libc::close(fd) };
170+
}
171+
}
172+
return Err(match other {
173+
Ok(Err(e)) => e,
174+
_ => NodeError::ThreadPanic,
175+
});
176+
}
177+
};
127178

128179
Ok(NodeHandle {
129180
node,
130181
rt_handle,
131182
shutdown_tx: Some(shutdown_tx),
183+
thread: Some(thread),
184+
#[cfg(target_os = "android")]
185+
tun_fd,
132186
})
133187
}
134188

@@ -158,6 +212,44 @@ impl NodeHandle {
158212
}
159213
}
160214

215+
impl Drop for NodeHandle {
216+
/// Shut the node down and block until its background runtime — and any
217+
/// network interface it created — has been fully torn down.
218+
///
219+
/// NOTE: this joins the background OS thread, so it must not be invoked
220+
/// from within the node's own Tokio runtime (a thread cannot join
221+
/// itself). Calling it from an external thread — as the C FFI layer
222+
/// does — is safe.
223+
fn drop(&mut self) {
224+
// Signal shutdown if `stop()` has not already done so.
225+
if let Some(tx) = self.shutdown_tx.take() {
226+
let _ = tx.send(());
227+
}
228+
// Block until the background thread has finished tearing down the
229+
// runtime. Dropping the runtime cancels every task, including the
230+
// TUN reader/writer that owns the tun device.
231+
if let Some(thread) = self.thread.take() {
232+
if let Err(e) = thread.join() {
233+
error!("node background thread panicked during shutdown: {e:?}");
234+
}
235+
}
236+
// On Android the `tun` crate does not close the TUN fd on drop (it
237+
// assumes Android's `VpnService` owns it). Mycelium opened this fd
238+
// itself via `create_tun_fd`, so it must close it here — after the
239+
// join above guarantees the runtime, and the tun device using the
240+
// fd, are gone. The interface is non-persistent, so closing the last
241+
// fd makes the kernel remove it.
242+
#[cfg(target_os = "android")]
243+
if let Some(fd) = self.tun_fd.take() {
244+
if fd >= 0 {
245+
// SAFETY: `fd` came from `create_tun_fd`; the runtime that
246+
// used it has been joined, so nothing else references it.
247+
unsafe { libc::close(fd) };
248+
}
249+
}
250+
}
251+
}
252+
161253
// ── Helpers ─────────────────────────────────────────────────────────────────
162254

163255
/// Parse a proxy address string (`"ip:port"`) into a [`SocketAddr`].

0 commit comments

Comments
 (0)