From c602874194cfb527093403c5e629e086a0c46704 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Wed, 20 May 2026 17:50:30 +0800 Subject: [PATCH 01/12] feat: modernize tuntap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Async API completely rewritten: `Stream + Sink` (tokio-core 0.1 / futures 0.1) replaced with `AsyncRead + AsyncWrite` (tokio 1.x / `AsyncFd`). The `Async` struct no longer implements `Stream` or `Sink`; use `AsyncReadExt` / `AsyncWriteExt` methods instead. * Feature renamed: `tokio` → `async`. Enable with `--features async` (on by default). * `set_recv_bufsize` removed from `Async`; external buffer sizing is now caller-managed via `AsyncRead`. * `tokio-core` 0.1 → `tokio` 1.x (features: net, rt, rt-multi-thread, io-util, macros, time) * `futures` 0.1 → removed (tokio provides async traits) * `mio` 0.6 → removed (tokio's `AsyncFd` replaces raw mio) * `etherparse` 0.9 → 0.17 (dev-dependency) * `serial_test` 0.4 → 3.x (dev-dependency) * `version-sync` → removed * Edition 2024 compliance: `extern "C"` → `unsafe extern "C"`, removed `extern crate` declarations * `build.rs`: removed `extern crate cc;` * `[badges]` section removed from `Cargo.toml` (deprecated by crates.io) * Travis CI replaced with GitHub Actions (`.github/workflows/ci.yml`) * `tests/version.rs` removed (depended on dropped `version-sync`) * Fixed `cmd` function in examples to accept arbitrary command instead of hardcoded `"ip"` (#17) * Fixed typo in doc comment (#16) --- .travis.yml | 28 ------ CHANGELOG.md | 33 +++++++ Cargo.toml | 30 +++--- README.md | 2 +- build.rs | 2 - examples/dump_iface.rs | 7 +- examples/pingpong.rs | 21 ++--- examples/tokio.rs | 57 ++++++------ examples/vpn.rs | 90 +++++++++--------- src/async.rs | 187 +++++++++++++++++++------------------- src/lib.rs | 8 +- tests/integration_test.rs | 10 +- tests/version.rs | 12 --- 13 files changed, 228 insertions(+), 259 deletions(-) delete mode 100644 .travis.yml delete mode 100644 tests/version.rs diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d532fa2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: rust -cache: cargo -rust: - - stable - - beta - - nightly -os: - - linux - #- osx - -before_script: - - sh tests/setup.sh - - | - (test "$TRAVIS_RUST_VERSION" != nightly || travis_wait rustup component add clippy-preview || true) - -script: - - | - export PATH="$PATH":~/.cargo/bin && - export RUST_BACKTRACE=1 && - export CARGO_INCREMENTAL=1 && - cargo build && - cargo test && - cargo doc --no-deps && - (test "$TRAVIS_RUST_VERSION" != nightly || cargo clippy -- --deny clippy) - -matrix: - allow_failures: - - rust: nightly diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9ab3d..4a2208d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +# 0.2.0 + +## Breaking Changes + +* Async API completely rewritten: `Stream + Sink` (tokio-core 0.1 / futures 0.1) replaced with + `AsyncRead + AsyncWrite` (tokio 1.x / `AsyncFd`). The `Async` struct no longer implements + `Stream` or `Sink`; use `AsyncReadExt` / `AsyncWriteExt` methods instead. +* Feature renamed: `tokio` → `async`. Enable with `--features async` (on by default). +* `set_recv_bufsize` removed from `Async`; external buffer sizing is now caller-managed + via `AsyncRead`. + +## Dependency Upgrades + +* `tokio-core` 0.1 → `tokio` 1.x (features: net, rt, rt-multi-thread, io-util, macros, time) +* `futures` 0.1 → removed (tokio provides async traits) +* `mio` 0.6 → removed (tokio's `AsyncFd` replaces raw mio) +* `etherparse` 0.9 → 0.17 (dev-dependency) +* `serial_test` 0.4 → 3.x (dev-dependency) +* `version-sync` → removed + +## Code Modernization + +* Edition 2024 compliance: `extern "C"` → `unsafe extern "C"`, removed `extern crate` declarations +* `build.rs`: removed `extern crate cc;` +* `[badges]` section removed from `Cargo.toml` (deprecated by crates.io) +* Travis CI replaced with GitHub Actions (`.github/workflows/ci.yml`) +* `tests/version.rs` removed (depended on dropped `version-sync`) + +## Upstream Fixes (since 0.1.4) + +* Fixed `cmd` function in examples to accept arbitrary command instead of hardcoded `"ip"` (#17) +* Fixed typo in doc comment (#16) + # 0.1.4 * Ability to set nonblocking without tokio (#12). diff --git a/Cargo.toml b/Cargo.toml index ec9beb9..fb82e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tun-tap" -# Also don't forget to update the version in the html_root_url attribute in src/lib.rs -version = "0.1.4" +edition = "2024" +version = "0.2.0" authors = ["Michal 'vorner' Vaner "] description = "TUN/TAP interface wrapper" documentation = "https://docs.rs/tun-tap" @@ -11,24 +11,18 @@ keywords = ["tun", "tap", "network"] categories = ["network-programming"] license = "Apache-2.0/MIT" -[badges] -travis-ci = { repository = "vorner/tuntap" } -maintenance = { status = "passively-maintained" } - [features] -default = ["tokio"] -tokio = ["futures", "libc", "mio", "tokio-core"] - -[build-dependencies] -cc = "~1" +default = ["async"] +async = ["dep:tokio", "dep:libc", "libc"] +libc = ["dep:libc"] [dependencies] -futures = { version = "~0.1", optional = true } -libc = { version = "~0.2", optional = true } -mio = { version = "~0.6", optional = true } -tokio-core = { version = "~0.1", optional = true } +libc = { version = "0.2", optional = true } +tokio = { version = "1", features = ["net", "rt", "rt-multi-thread", "io-util", "macros", "time"], optional = true } + +[build-dependencies] +cc = "1" [dev-dependencies] -version-sync = "~0.9" -etherparse = "~0.9" -serial_test = "~0.4" +etherparse = "0.17" +serial_test = "3" diff --git a/README.md b/README.md index c692762..4018f78 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TunTap -[![Travis Build Status](https://api.travis-ci.org/vorner/tuntap.png?branch=master)](https://travis-ci.org/vorner/tuntap) +[![CI](https://github.com/vorner/tuntap/actions/workflows/ci.yml/badge.svg)](https://github.com/vorner/tuntap/actions/workflows/ci.yml) TUN/TAP wrapper for Rust. diff --git a/build.rs b/build.rs index ce7faed..aa38ad0 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,3 @@ -extern crate cc; - use cc::Build; fn main() { diff --git a/examples/dump_iface.rs b/examples/dump_iface.rs index 3b7ea02..df295ee 100644 --- a/examples/dump_iface.rs +++ b/examples/dump_iface.rs @@ -4,7 +4,6 @@ //! raw data of the packets that arrive. //! //! You really do want better error handling than all these unwraps. -extern crate tun_tap; use std::process::Command; @@ -28,8 +27,10 @@ fn main() { // Configure the „local“ (kernel) endpoint. cmd("ip", &["addr", "add", "dev", iface.name(), "10.107.1.2/24"]); cmd("ip", &["link", "set", "up", "dev", iface.name()]); - println!("Created interface {}. Send some packets into it and see they're printed here", - iface.name()); + println!( + "Created interface {}. Send some packets into it and see they're printed here", + iface.name() + ); println!("You can for example ping 10.107.1.3 (it won't answer)"); // That 1500 is a guess for the IFace's MTU (we probably could configure it explicitly). 4 more // for TUN's „header“. diff --git a/examples/pingpong.rs b/examples/pingpong.rs index fd3d874..a81a01d 100644 --- a/examples/pingpong.rs +++ b/examples/pingpong.rs @@ -6,9 +6,7 @@ //! The `dump_iface` example is simpler (contains only the reading end), so you want to start with //! that. //! -//! You really do want better error handling than all these unwraps. - -extern crate tun_tap; +//! that. use std::process::Command; use std::sync::Arc; @@ -19,10 +17,12 @@ use tun_tap::{Iface, Mode}; /// The packet data. Note that it is prefixed by 4 bytes ‒ two bytes are flags, another two are /// protocol. 8, 0 is IPv4, 134, 221 is IPv6. . -const PING: &[u8] = &[0, 0, 8, 0, 69, 0, 0, 84, 44, 166, 64, 0, 64, 1, 247, 40, 10, 107, 1, 2, 10, - 107, 1, 3, 8, 0, 62, 248, 19, 160, 0, 2, 232, 228, 34, 90, 0, 0, 0, 0, 216, 83, 3, 0, 0, 0, 0, - 0, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55]; +const PING: &[u8] = &[ + 0, 0, 8, 0, 69, 0, 0, 84, 44, 166, 64, 0, 64, 1, 247, 40, 10, 107, 1, 2, 10, 107, 1, 3, 8, 0, + 62, 248, 19, 160, 0, 2, 232, 228, 34, 90, 0, 0, 0, 0, 216, 83, 3, 0, 0, 0, 0, 0, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, +]; /// Run a shell command. Panic if it fails in any way. fn cmd(cmd: &str, args: &[&str]) { @@ -55,7 +55,7 @@ fn main() { assert!(amount == PING.len()); } }); - let reader = thread::spawn(move || { + thread::spawn(move || { // MTU + TUN header let mut buffer = vec![0; 1504]; loop { @@ -65,8 +65,5 @@ fn main() { println!("Packet: {:?}", &buffer[4..size]); } }); - writer.join() - .unwrap(); - reader.join() - .unwrap(); + writer.join().unwrap(); } diff --git a/examples/tokio.rs b/examples/tokio.rs index 6dbb007..0d8299d 100644 --- a/examples/tokio.rs +++ b/examples/tokio.rs @@ -3,29 +3,27 @@ //! This creates an interface, configures the kernel endpoint and sends pings to that endpoint. It //! prints whatever packets come back (which should include the pong responses). //! -//! This does essentialy the same as the `pingpoing` example. However, instead of threads, this +//! This does essentially the same as the `pingpong` example. However, instead of threads, this //! uses tokio asynchronous event loop to multiplex between the two directions. //! //! You really do want better error handling than all these unwraps. -extern crate futures; -extern crate tokio_core; -extern crate tun_tap; - use std::process::Command; use std::time::Duration; -use futures::{Future, Stream}; -use tokio_core::reactor::{Core, Interval}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::time; +use tun_tap::r#async::Async; use tun_tap::{Iface, Mode}; -use tun_tap::async::Async; -/// The packet data. Note that it is prefixed by 4 bytes ‒ two bytes are flags, another two are +/// The packet data. Note that it is prefixed by 4 bytes — two bytes are flags, another two are /// protocol. 8, 0 is IPv4, 134, 221 is IPv6. . -const PING: &[u8] = &[0, 0, 8, 0, 69, 0, 0, 84, 44, 166, 64, 0, 64, 1, 247, 40, 10, 107, 1, 2, 10, - 107, 1, 3, 8, 0, 62, 248, 19, 160, 0, 2, 232, 228, 34, 90, 0, 0, 0, 0, 216, 83, 3, 0, 0, 0, 0, - 0, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55]; +const PING: &[u8] = &[ + 0, 0, 8, 0, 69, 0, 0, 84, 44, 166, 64, 0, 64, 1, 247, 40, 10, 107, 1, 2, 10, 107, 1, 3, 8, 0, + 62, 248, 19, 160, 0, 2, 232, 228, 34, 90, 0, 0, 0, 0, 216, 83, 3, 0, 0, 0, 0, 0, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, +]; /// Run a shell command. Panic if it fails in any way. fn cmd(cmd: &str, args: &[&str]) { @@ -38,26 +36,29 @@ fn cmd(cmd: &str, args: &[&str]) { assert!(ecode.success(), "Failed to execte {}", cmd); } -fn main() { +#[tokio::main] +async fn main() { let iface = Iface::new("testtun%d", Mode::Tun).unwrap(); eprintln!("Iface: {:?}", iface); - // Configure the „local“ (kernel) endpoint. Kernel is (the host) 10.107.1.3, we (the app) + // Configure the "local" (kernel) endpoint. Kernel is (the host) 10.107.1.3, we (the app) // pretend to be 10.107.1.2. cmd("ip", &["addr", "add", "dev", iface.name(), "10.107.1.3/24"]); cmd("ip", &["link", "set", "up", "dev", iface.name()]); - let mut core = Core::new().unwrap(); - let iface = Async::new(iface, &core.handle()).unwrap(); - let (sink, stream) = iface.split(); - let writer = Interval::new(Duration::from_secs(1), &core.handle()) - .unwrap() - .map(|_| { + let (mut reader, mut writer) = tokio::io::split(Async::new(iface).unwrap()); + let write_task = tokio::spawn(async move { + let mut interval = time::interval(Duration::from_secs(1)); + loop { + interval.tick().await; println!("Sending ping"); - PING.to_owned() - }) - .forward(sink); - let reader = stream.for_each(|packet| { - println!("Received: {:?}", packet); - Ok(()) + writer.write_all(PING).await.unwrap(); + } + }); + let read_task = tokio::spawn(async move { + let mut buf = vec![0u8; 1504]; + loop { + let n = reader.read(&mut buf).await.unwrap(); + println!("Received: {:?}", &buf[..n]); + } }); - core.run(reader.join(writer)).unwrap(); + tokio::try_join!(write_task, read_task).unwrap(); } diff --git a/examples/vpn.rs b/examples/vpn.rs index 04c7753..f7d4cdf 100644 --- a/examples/vpn.rs +++ b/examples/vpn.rs @@ -1,6 +1,6 @@ //! An VPN example //! -//! This creates one endpoint of a VPN. It takes two parameters ‒ local address and the address +//! This creates one endpoint of a VPN. It takes two parameters — local address and the address //! address of the other endpoint, and sends all packets there, encapsulated in UDP. Packets //! received from the other side are put to the kernel from the other side. //! @@ -12,58 +12,52 @@ //! Do not use as a VPN in any real-life situation. There's no authentication, encryption, nearly //! no error handling, etc. -extern crate futures; -extern crate tokio_core; -extern crate tun_tap; - use std::env; -use std::io::Result; +use std::io; use std::net::SocketAddr; +use std::sync::Arc; -use futures::{Future, Stream}; -use tokio_core::net::{UdpCodec, UdpSocket}; -use tokio_core::reactor::Core; - +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::UdpSocket; +use tun_tap::r#async::Async; use tun_tap::{Iface, Mode}; -use tun_tap::async::Async; -struct VecCodec(SocketAddr); +#[tokio::main] +async fn main() -> io::Result<()> { + let loc_address: SocketAddr = env::args().nth(1).unwrap().parse().unwrap(); + let rem_address: SocketAddr = env::args().nth(2).unwrap().parse().unwrap(); -impl UdpCodec for VecCodec { - type In = Vec; - type Out = Vec; - fn decode(&mut self, _src: &SocketAddr, buf: &[u8]) -> Result { - Ok(buf.to_owned()) - } - fn encode(&mut self, msg: Self::Out, buf: &mut Vec) -> SocketAddr { - buf.extend(&msg); - self.0 - } -} + let socket = Arc::new(UdpSocket::bind(loc_address).await?); + let tun = Iface::new("vpn%d", Mode::Tun).unwrap(); + let (mut tun_reader, mut tun_writer) = tokio::io::split(Async::new(tun).unwrap()); + + // TUN → UDP (packets from kernel → remote peer) + let tun_to_udp = { + let socket = Arc::clone(&socket); + tokio::spawn(async move { + let mut buf = vec![0u8; 1504]; + loop { + let n = tun_reader.read(&mut buf).await?; + socket.send_to(&buf[..n], rem_address).await?; + } + #[allow(unreachable_code)] + Ok::<_, io::Error>(()) + }) + }; + + // UDP → TUN (packets from remote peer → kernel) + let udp_to_tun = { + tokio::spawn(async move { + let mut buf = vec![0u8; 1504]; + loop { + let (n, _src) = socket.recv_from(&mut buf).await?; + tun_writer.write_all(&buf[..n]).await?; + } + #[allow(unreachable_code)] + Ok::<_, io::Error>(()) + }) + }; -fn main() { - let mut core = Core::new().unwrap(); - let loc_address = env::args() - .nth(1) - .unwrap() - .parse() - .unwrap(); - let rem_address = env::args() - .nth(2) - .unwrap() - .parse() - .unwrap(); - let socket = UdpSocket::bind(&loc_address, &core.handle()) - .unwrap(); - let (sender, receiver) = socket.framed(VecCodec(rem_address)) - .split(); - let tun = Iface::new("vpn%d", Mode::Tun) - .unwrap(); - let (sink, stream) = Async::new(tun, &core.handle()) - .unwrap() - .split(); - let reader = stream.forward(sender); - let writer = receiver.forward(sink); - core.run(reader.join(writer)) - .unwrap(); + let _ = tokio::try_join!(tun_to_udp, udp_to_tun); + Ok(()) } diff --git a/src/async.rs b/src/async.rs index c54fc2b..f152c1c 100644 --- a/src/async.rs +++ b/src/async.rs @@ -1,139 +1,134 @@ //! Integration of TUN/TAP into tokio. //! //! See the [`Async`](struct.Async.html) structure. -extern crate futures; -extern crate libc; -extern crate mio; -extern crate tokio_core; +//! +//! # Examples +//! +//! ```rust,no_run +//! # use tun_tap::*; +//! # use tun_tap::r#async::Async; +//! # use tokio::io::AsyncReadExt; +//! # #[tokio::main] +//! # async fn main() { +//! let iface = Iface::new("mytun%d", Mode::Tun).unwrap(); +//! let mut async_iface = Async::new(iface).unwrap(); +//! let mut buf = vec![0u8; 1504]; +//! let n = async_iface.read(&mut buf).await.unwrap(); +//! # } +//! ``` -use std::io::{Error, ErrorKind, Read, Result, Write}; -use std::os::unix::io::AsRawFd; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; -use self::futures::{Async as FAsync, AsyncSink, Sink, StartSend, Stream, Poll as FPoll}; -use self::mio::{Evented, Poll as MPoll, PollOpt, Ready, Token}; -use self::mio::unix::EventedFd; -use self::tokio_core::reactor::{Handle, PollEvented}; +use tokio::io::unix::AsyncFd; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use super::Iface; -struct MioWrapper { - iface: Iface, -} - -impl Evented for MioWrapper { - fn register(&self, poll: &MPoll, token: Token, events: Ready, opts: PollOpt) -> Result<()> { - EventedFd(&self.iface.as_raw_fd()).register(poll, token, events, opts) - } - fn reregister(&self, poll: &MPoll, token: Token, events: Ready, opts: PollOpt) -> Result<()> { - EventedFd(&self.iface.as_raw_fd()).reregister(poll, token, events, opts) - } - fn deregister(&self, poll: &MPoll) -> Result<()> { - EventedFd(&self.iface.as_raw_fd()).deregister(poll) - } -} - -impl Read for MioWrapper { - fn read(&mut self, buf: &mut [u8]) -> Result { - self.iface.recv(buf) - } -} - -impl Write for MioWrapper { - fn write(&mut self, buf: &[u8]) -> Result { - self.iface.send(buf) - } - fn flush(&mut self) -> Result<()> { - Ok(()) - } -} - -/// A wrapper around [`Iface`](../struct.Iface.html) for use in connection with tokio. +/// A wrapper around [`Iface`](../struct.Iface.html) for use with tokio. /// -/// This turns the synchronous `Iface` into an asynchronous `Sink + Stream` of packets. +/// This turns the synchronous `Iface` into an asynchronous reader/writer. +/// Implements [`AsyncRead`] and [`AsyncWrite`], so it can be used with +/// standard tokio utilities like `AsyncReadExt::read` or `copy_bidirectional`. +/// +/// Equivalent to the old `Stream + Sink` API — just use `read` and `write` +/// directly via [`AsyncReadExt`](tokio::io::AsyncReadExt) / +/// [`AsyncWriteExt`](tokio::io::AsyncWriteExt). pub struct Async { - mio: PollEvented, - recv_bufsize: usize, + inner: AsyncFd, } impl Async { /// Consumes an `Iface` and wraps it in a new `Async`. /// - /// # Parameters - /// - /// * `iface`: The created interface to wrap. It gets consumed. - /// * `handle`: The handle to tokio's `Core` to run on. + /// Sets the underlying fd to non-blocking mode automatically. /// /// # Errors /// - /// This fails with an error in case of low-level OS errors (they shouldn't usually happen). + /// This fails with an error in case of low-level OS errors. /// /// # Examples /// /// ```rust,no_run - /// # extern crate futures; - /// # extern crate tokio_core; - /// # extern crate tun_tap; - /// # use futures::Stream; /// # use tun_tap::*; - /// # use tun_tap::async::*; - /// # use tokio_core::reactor::Core; - /// # fn main() { + /// # use tun_tap::r#async::Async; + /// # use tokio::io::AsyncReadExt; + /// # #[tokio::main] + /// # async fn main() { /// let iface = Iface::new("mytun%d", Mode::Tun).unwrap(); /// let name = iface.name().to_owned(); /// // Bring the interface up by `ip addr add IP dev $name; ip link set up dev $name` - /// let core = Core::new().unwrap(); - /// let async = Async::new(iface, &core.handle()).unwrap(); - /// let (sink, stream) = async.split(); + /// let mut async_iface = Async::new(iface).unwrap(); + /// let mut buf = vec![0u8; 1504]; + /// let n = async_iface.read(&mut buf).await.unwrap(); /// # } /// ``` - pub fn new(iface: Iface, handle: &Handle) -> Result { + pub fn new(iface: Iface) -> io::Result { iface.set_non_blocking()?; Ok(Async { - mio: PollEvented::new(MioWrapper { iface }, handle)?, - recv_bufsize: 1542, + inner: AsyncFd::new(iface)?, }) } - /// Sets the receive buffer size. - /// - /// When receiving a packet, a buffer of this size is allocated and the packet read into it. - /// This configures the size of the buffer. - /// - /// This needs to be called when the interface's MTU is changed from the default 1500. The - /// default should be enough otherwise. - pub fn set_recv_bufsize(&mut self, bufsize: usize) { - self.recv_bufsize = bufsize; + + /// Returns a shared reference to the underlying [`Iface`]. + pub fn get_ref(&self) -> &Iface { + self.inner.get_ref() + } + + /// Returns a mutable reference to the underlying [`Iface`]. + pub fn get_mut(&mut self) -> &mut Iface { + self.inner.get_mut() + } + + /// Consumes this `Async` and returns the underlying [`Iface`]. + pub fn into_inner(self) -> Iface { + self.inner.into_inner() } } -impl Stream for Async { - type Item = Vec; - type Error = Error; - fn poll(&mut self) -> FPoll, Self::Error> { - // TODO Reuse buffer? - let mut buffer = vec![0; self.recv_bufsize]; - match self.mio.read(&mut buffer) { - Ok(size) => { - buffer.resize(size, 0); - Ok(FAsync::Ready(Some(buffer))) - }, - Err(ref e) if e.kind() == ErrorKind::WouldBlock => Ok(FAsync::NotReady), - Err(e) => Err(e), +impl AsyncRead for Async { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + loop { + let mut guard = std::task::ready!(self.inner.poll_read_ready(cx))?; + let unfilled = buf.initialize_unfilled(); + match guard.try_io(|inner| inner.get_ref().recv(unfilled)) { + Ok(Ok(n)) => { + buf.advance(n); + return Poll::Ready(Ok(())); + } + Ok(Err(e)) if e.kind() == io::ErrorKind::WouldBlock => continue, + Ok(Err(e)) => return Poll::Ready(Err(e)), + Err(_would_block) => continue, + } } } } -impl Sink for Async { - type SinkItem = Vec; - type SinkError = Error; - fn start_send(&mut self, item: Self::SinkItem) -> StartSend { - match self.mio.write(&item) { - // TODO What to do about short write? Can it happen? - Ok(_size) => Ok(AsyncSink::Ready), - Err(ref e) if e.kind() == ErrorKind::WouldBlock => Ok(AsyncSink::NotReady(item)), - Err(e) => Err(e), +impl AsyncWrite for Async { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + loop { + let mut guard = std::task::ready!(self.inner.poll_write_ready(cx))?; + match guard.try_io(|inner| inner.get_ref().send(buf)) { + Ok(result) => return Poll::Ready(result), + Err(_would_block) => continue, + } } } - fn poll_complete(&mut self) -> FPoll<(), Self::SinkError> { - Ok(FAsync::Ready(())) + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) } } diff --git a/src/lib.rs b/src/lib.rs index 7917579..1434f30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![doc( - html_root_url = "https://docs.rs/tun-tap/0.1.4/tun-tap/", + html_root_url = "https://docs.rs/tun-tap/0.2.0/tun-tap/", test(attr(deny(warnings), allow(unused_variables))) )] #![deny(missing_docs)] @@ -33,10 +33,10 @@ use std::io::{Error, Read, Result, Write}; use std::os::raw::{c_char, c_int}; use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; -#[cfg(feature = "tokio")] -pub mod async; +#[cfg(feature = "async")] +pub mod r#async; -extern "C" { +unsafe extern "C" { fn tuntap_setup(fd: c_int, name: *mut u8, mode: c_int, packet_info: c_int) -> c_int; } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 36c4712..9bc62fa 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,8 +1,4 @@ -extern crate etherparse; -extern crate serial_test; -extern crate tun_tap; - -use etherparse::{IpHeader, PacketBuilder, PacketHeaders, TransportHeader}; +use etherparse::{NetHeaders, PacketBuilder, PacketHeaders, TransportHeader}; use serial_test::serial; use std::net::{IpAddr, Ipv4Addr, UdpSocket}; use tun_tap::{Iface, Mode}; @@ -22,7 +18,7 @@ fn it_sents_packets() { assert_eq!(num, 38); let packet = &buf[..num]; if let PacketHeaders { - ip: Some(IpHeader::Version4(ip_header)), + net: Some(NetHeaders::Ipv4(ip_header, _ext)), transport: Some(TransportHeader::Udp(udp_header)), payload, .. @@ -32,7 +28,7 @@ fn it_sents_packets() { assert_eq!(ip_header.destination, [10, 10, 10, 2]); assert_eq!(udp_header.source_port, 2424); assert_eq!(udp_header.destination_port, 4242); - assert_eq!(payload, data); + assert_eq!(payload.slice(), data); } else { panic!("incorrect packet"); } diff --git a/tests/version.rs b/tests/version.rs deleted file mode 100644 index 411eea5..0000000 --- a/tests/version.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[macro_use] -extern crate version_sync; - -#[test] -fn test_readme_deps() { - assert_markdown_deps_updated!("README.md"); -} - -#[test] -fn test_html_root_url() { - assert_html_root_url_updated!("src/lib.rs"); -} From 7dcd89077d9843c76ec8b8dbfe4758405d7d6c0a Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Mon, 25 May 2026 09:23:27 +0800 Subject: [PATCH 02/12] ci: replace `Travis CI` with GitHub Action --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..649cac4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Setup test interface + run: sudo sh tests/setup.sh + - name: Build + run: cargo build --all-targets + - name: Test + run: cargo test + - name: Clippy + run: cargo clippy --all-targets -- -D warnings + - name: Doc + run: cargo doc --no-deps From fb7314ee2a88a2f3b70a2ba108304214abacb227 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 20 Jun 2026 17:48:50 +0800 Subject: [PATCH 03/12] fix: add a filter to get `PING` from writer Remove loop in `writer_task` and add a filter in `read_task`, so we can get exact `PING`. --- examples/tokio.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/tokio.rs b/examples/tokio.rs index 0d8299d..a75e0ad 100644 --- a/examples/tokio.rs +++ b/examples/tokio.rs @@ -40,24 +40,29 @@ fn cmd(cmd: &str, args: &[&str]) { async fn main() { let iface = Iface::new("testtun%d", Mode::Tun).unwrap(); eprintln!("Iface: {:?}", iface); - // Configure the "local" (kernel) endpoint. Kernel is (the host) 10.107.1.3, we (the app) + // Configure the „local“ (kernel) endpoint. Kernel is (the host) 10.107.1.3, we (the app) // pretend to be 10.107.1.2. cmd("ip", &["addr", "add", "dev", iface.name(), "10.107.1.3/24"]); cmd("ip", &["link", "set", "up", "dev", iface.name()]); let (mut reader, mut writer) = tokio::io::split(Async::new(iface).unwrap()); let write_task = tokio::spawn(async move { - let mut interval = time::interval(Duration::from_secs(1)); - loop { - interval.tick().await; - println!("Sending ping"); - writer.write_all(PING).await.unwrap(); - } + time::sleep(Duration::from_secs(1)).await; + println!("Sending ping"); + writer.write_all(PING).await.unwrap(); }); + let read_task = tokio::spawn(async move { let mut buf = vec![0u8; 1504]; loop { let n = reader.read(&mut buf).await.unwrap(); - println!("Received: {:?}", &buf[..n]); + // only catch `ping` from the writer, ignore other packets + // buf[2..4]: IPv4 protocol, + // buf[13]: the ICMP type (1 for echo request, 0 for echo reply) + // buf[24]: the ICMP code (0 for both echo request and reply) + if buf[2..4] == [8, 0] && buf[13] == 1 && buf[24] == 0 { + println!("Received: {:?}", &buf[..n]); + break; + } } }); tokio::try_join!(write_task, read_task).unwrap(); From dc72c73c2175742ea7df59efd97dd21ff052a137 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 20 Jun 2026 18:04:14 +0800 Subject: [PATCH 04/12] fix: rename feature `async` to `tokio` --- Cargo.toml | 4 ++-- src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb82e46..c9e6903 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,8 @@ categories = ["network-programming"] license = "Apache-2.0/MIT" [features] -default = ["async"] -async = ["dep:tokio", "dep:libc", "libc"] +default = ["tokio"] +tokio = ["dep:tokio", "dep:libc", "libc"] libc = ["dep:libc"] [dependencies] diff --git a/src/lib.rs b/src/lib.rs index 1434f30..b8a230f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ use std::io::{Error, Read, Result, Write}; use std::os::raw::{c_char, c_int}; use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; -#[cfg(feature = "async")] +#[cfg(feature = "tokio")] pub mod r#async; unsafe extern "C" { From 9d1d3a461bde6ed0a5924837cd21ad31d275ed53 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 20 Jun 2026 18:16:17 +0800 Subject: [PATCH 05/12] refactor: rename `async` to `aio` We change the name `async` to avoid using `r#` hack --- examples/tokio.rs | 2 +- examples/vpn.rs | 2 +- src/{async.rs => aio.rs} | 4 ++-- src/lib.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/{async.rs => aio.rs} (98%) diff --git a/examples/tokio.rs b/examples/tokio.rs index a75e0ad..fba10c5 100644 --- a/examples/tokio.rs +++ b/examples/tokio.rs @@ -13,7 +13,7 @@ use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::time; -use tun_tap::r#async::Async; +use tun_tap::aio::Async; use tun_tap::{Iface, Mode}; /// The packet data. Note that it is prefixed by 4 bytes — two bytes are flags, another two are diff --git a/examples/vpn.rs b/examples/vpn.rs index f7d4cdf..cbef188 100644 --- a/examples/vpn.rs +++ b/examples/vpn.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::UdpSocket; -use tun_tap::r#async::Async; +use tun_tap::aio::Async; use tun_tap::{Iface, Mode}; #[tokio::main] diff --git a/src/async.rs b/src/aio.rs similarity index 98% rename from src/async.rs rename to src/aio.rs index f152c1c..9e2daa7 100644 --- a/src/async.rs +++ b/src/aio.rs @@ -6,7 +6,7 @@ //! //! ```rust,no_run //! # use tun_tap::*; -//! # use tun_tap::r#async::Async; +//! # use tun_tap::aio::Async; //! # use tokio::io::AsyncReadExt; //! # #[tokio::main] //! # async fn main() { @@ -52,7 +52,7 @@ impl Async { /// /// ```rust,no_run /// # use tun_tap::*; - /// # use tun_tap::r#async::Async; + /// # use tun_tap::aio::Async; /// # use tokio::io::AsyncReadExt; /// # #[tokio::main] /// # async fn main() { diff --git a/src/lib.rs b/src/lib.rs index b8a230f..b3a33a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ use std::os::raw::{c_char, c_int}; use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; #[cfg(feature = "tokio")] -pub mod r#async; +pub mod aio; unsafe extern "C" { fn tuntap_setup(fd: c_int, name: *mut u8, mode: c_int, packet_info: c_int) -> c_int; From b725d5162c6b961d3c10ea6985d05c89f49bf967 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 20 Jun 2026 18:18:07 +0800 Subject: [PATCH 06/12] fix: add `#[allow(unused_variables)]` to fix warning --- examples/pingpong.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/pingpong.rs b/examples/pingpong.rs index a81a01d..b4e7ff7 100644 --- a/examples/pingpong.rs +++ b/examples/pingpong.rs @@ -5,8 +5,6 @@ //! //! The `dump_iface` example is simpler (contains only the reading end), so you want to start with //! that. -//! -//! that. use std::process::Command; use std::sync::Arc; @@ -55,7 +53,9 @@ fn main() { assert!(amount == PING.len()); } }); - thread::spawn(move || { + + #[allow(unused_variables)] + let reader = thread::spawn(move || { // MTU + TUN header let mut buffer = vec![0; 1504]; loop { @@ -66,4 +66,5 @@ fn main() { } }); writer.join().unwrap(); + reader.join().unwrap(); } From 729dbbd2fd860aa362968a4d60a0bb7c4e0e222e Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 20 Jun 2026 18:19:10 +0800 Subject: [PATCH 07/12] fix: add `tests/version.rs` again --- Cargo.toml | 1 + tests/version.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 tests/version.rs diff --git a/Cargo.toml b/Cargo.toml index c9e6903..4828aac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,5 +24,6 @@ tokio = { version = "1", features = ["net", "rt", "rt-multi-thread", "io-util", cc = "1" [dev-dependencies] +version-sync = "0.9" etherparse = "0.17" serial_test = "3" diff --git a/tests/version.rs b/tests/version.rs new file mode 100644 index 0000000..411eea5 --- /dev/null +++ b/tests/version.rs @@ -0,0 +1,12 @@ +#[macro_use] +extern crate version_sync; + +#[test] +fn test_readme_deps() { + assert_markdown_deps_updated!("README.md"); +} + +#[test] +fn test_html_root_url() { + assert_html_root_url_updated!("src/lib.rs"); +} From 70d00bd60d4ffcac160cdf57dc0433ab9d277f08 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 20 Jun 2026 18:21:31 +0800 Subject: [PATCH 08/12] docs: update `CHANGELOG.md` --- CHANGELOG.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2208d..ca8e7c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ * Async API completely rewritten: `Stream + Sink` (tokio-core 0.1 / futures 0.1) replaced with `AsyncRead + AsyncWrite` (tokio 1.x / `AsyncFd`). The `Async` struct no longer implements `Stream` or `Sink`; use `AsyncReadExt` / `AsyncWriteExt` methods instead. -* Feature renamed: `tokio` → `async`. Enable with `--features async` (on by default). * `set_recv_bufsize` removed from `Async`; external buffer sizing is now caller-managed via `AsyncRead`. +* Rename module `r#async` to `aio` to avoid conflict with Rust 2024 edition's `async` keyword. ## Dependency Upgrades @@ -16,15 +16,6 @@ * `mio` 0.6 → removed (tokio's `AsyncFd` replaces raw mio) * `etherparse` 0.9 → 0.17 (dev-dependency) * `serial_test` 0.4 → 3.x (dev-dependency) -* `version-sync` → removed - -## Code Modernization - -* Edition 2024 compliance: `extern "C"` → `unsafe extern "C"`, removed `extern crate` declarations -* `build.rs`: removed `extern crate cc;` -* `[badges]` section removed from `Cargo.toml` (deprecated by crates.io) -* Travis CI replaced with GitHub Actions (`.github/workflows/ci.yml`) -* `tests/version.rs` removed (depended on dropped `version-sync`) ## Upstream Fixes (since 0.1.4) From a8f5af2cd268be1b0900b681df5e45e9422519b8 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 20 Jun 2026 18:24:30 +0800 Subject: [PATCH 09/12] ci: fix `ci.yml` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 649cac4..6110c25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - name: Build run: cargo build --all-targets - name: Test - run: cargo test + run: sudo env "PATH=$PATH" "HOME=$HOME" cargo test - name: Clippy run: cargo clippy --all-targets -- -D warnings - name: Doc From 44a2aea9aa99bfc870e3095e2d51c690941b2152 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 27 Jun 2026 16:25:17 +0800 Subject: [PATCH 10/12] refactor: message-oriented async I/O interface Replace the stream-based (AsyncRead + AsyncWrite) async interface with datagram-style recv/send methods to match the TUN device's message semantics. --- Cargo.toml | 2 +- examples/tokio.rs | 47 +++++++------ examples/vpn.rs | 12 ++-- src/aio.rs | 135 ++++++++++++------------------------- tests/integration_test.rs | 137 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 118 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4828aac..5cf76bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ license = "Apache-2.0/MIT" [features] default = ["tokio"] -tokio = ["dep:tokio", "dep:libc", "libc"] +tokio = ["dep:tokio", "libc"] libc = ["dep:libc"] [dependencies] diff --git a/examples/tokio.rs b/examples/tokio.rs index fba10c5..b8fe1ee 100644 --- a/examples/tokio.rs +++ b/examples/tokio.rs @@ -9,9 +9,9 @@ //! You really do want better error handling than all these unwraps. use std::process::Command; +use std::sync::Arc; use std::time::Duration; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::time; use tun_tap::aio::Async; use tun_tap::{Iface, Mode}; @@ -44,26 +44,31 @@ async fn main() { // pretend to be 10.107.1.2. cmd("ip", &["addr", "add", "dev", iface.name(), "10.107.1.3/24"]); cmd("ip", &["link", "set", "up", "dev", iface.name()]); - let (mut reader, mut writer) = tokio::io::split(Async::new(iface).unwrap()); - let write_task = tokio::spawn(async move { - time::sleep(Duration::from_secs(1)).await; - println!("Sending ping"); - writer.write_all(PING).await.unwrap(); - }); - - let read_task = tokio::spawn(async move { - let mut buf = vec![0u8; 1504]; - loop { - let n = reader.read(&mut buf).await.unwrap(); - // only catch `ping` from the writer, ignore other packets - // buf[2..4]: IPv4 protocol, - // buf[13]: the ICMP type (1 for echo request, 0 for echo reply) - // buf[24]: the ICMP code (0 for both echo request and reply) - if buf[2..4] == [8, 0] && buf[13] == 1 && buf[24] == 0 { - println!("Received: {:?}", &buf[..n]); - break; + let iface = Arc::new(Async::new(iface).unwrap()); + let write_task = { + let iface = iface.clone(); + tokio::spawn(async move { + time::sleep(Duration::from_secs(1)).await; + println!("Sending ping"); + iface.send(PING).await.unwrap(); + }) + }; + let read_task = { + let iface = iface.clone(); + tokio::spawn(async move { + let mut buf = vec![0u8; 1504]; + loop { + let n = iface.recv(&mut buf).await.unwrap(); + // only catch `ping` from the writer, ignore other packets + // buf[2..4]: IPv4 protocol, + // buf[13]: the ICMP type (1 for echo request, 0 for echo reply) + // buf[24]: the ICMP code (0 for both echo request and reply) + if buf[2..4] == [8, 0] && buf[13] == 1 && buf[24] == 0 { + println!("Received: {:?}", &buf[..n]); + break; + } } - } - }); + }) + }; tokio::try_join!(write_task, read_task).unwrap(); } diff --git a/examples/vpn.rs b/examples/vpn.rs index cbef188..ec5cb99 100644 --- a/examples/vpn.rs +++ b/examples/vpn.rs @@ -17,7 +17,6 @@ use std::io; use std::net::SocketAddr; use std::sync::Arc; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::UdpSocket; use tun_tap::aio::Async; use tun_tap::{Iface, Mode}; @@ -29,15 +28,17 @@ async fn main() -> io::Result<()> { let socket = Arc::new(UdpSocket::bind(loc_address).await?); let tun = Iface::new("vpn%d", Mode::Tun).unwrap(); - let (mut tun_reader, mut tun_writer) = tokio::io::split(Async::new(tun).unwrap()); + let tun = Arc::new(Async::new(tun).unwrap()); + // let (mut tun_reader, mut tun_writer) = tokio::io::split(Async::new(tun).unwrap()); // TUN → UDP (packets from kernel → remote peer) let tun_to_udp = { - let socket = Arc::clone(&socket); + let socket = socket.clone(); + let tun = tun.clone(); tokio::spawn(async move { let mut buf = vec![0u8; 1504]; loop { - let n = tun_reader.read(&mut buf).await?; + let n = tun.recv(&mut buf).await?; socket.send_to(&buf[..n], rem_address).await?; } #[allow(unreachable_code)] @@ -47,11 +48,12 @@ async fn main() -> io::Result<()> { // UDP → TUN (packets from remote peer → kernel) let udp_to_tun = { + let tun = tun.clone(); tokio::spawn(async move { let mut buf = vec![0u8; 1504]; loop { let (n, _src) = socket.recv_from(&mut buf).await?; - tun_writer.write_all(&buf[..n]).await?; + tun.send(&buf[..n]).await?; } #[allow(unreachable_code)] Ok::<_, io::Error>(()) diff --git a/src/aio.rs b/src/aio.rs index 9e2daa7..38feb82 100644 --- a/src/aio.rs +++ b/src/aio.rs @@ -1,40 +1,27 @@ //! Integration of TUN/TAP into tokio. //! -//! See the [`Async`](struct.Async.html) structure. -//! -//! # Examples -//! -//! ```rust,no_run -//! # use tun_tap::*; -//! # use tun_tap::aio::Async; -//! # use tokio::io::AsyncReadExt; -//! # #[tokio::main] -//! # async fn main() { -//! let iface = Iface::new("mytun%d", Mode::Tun).unwrap(); -//! let mut async_iface = Async::new(iface).unwrap(); -//! let mut buf = vec![0u8; 1504]; -//! let n = async_iface.read(&mut buf).await.unwrap(); -//! # } -//! ``` +//! Wraps an [`Iface`] in tokio's [`AsyncFd`] to provide +//! async versions of [`Iface::recv`] and [`Iface::send`]. The underlying +//! fd is automatically set to non-blocking mode so that the kernel +//! returns `EWOULDBLOCK` instead of blocking the reactor thread. use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; use tokio::io::unix::AsyncFd; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use super::Iface; -/// A wrapper around [`Iface`](../struct.Iface.html) for use with tokio. +/// An asynchronous wrapper around a TUN/TAP [`Iface`]. /// -/// This turns the synchronous `Iface` into an asynchronous reader/writer. -/// Implements [`AsyncRead`] and [`AsyncWrite`], so it can be used with -/// standard tokio utilities like `AsyncReadExt::read` or `copy_bidirectional`. +/// Integrates the raw file descriptor with tokio's I/O reactor so that +/// [`recv`](Async::recv) / [`send`](Async::send) can be `.await`ed +/// cooperatively without blocking the runtime. /// -/// Equivalent to the old `Stream + Sink` API — just use `read` and `write` -/// directly via [`AsyncReadExt`](tokio::io::AsyncReadExt) / -/// [`AsyncWriteExt`](tokio::io::AsyncWriteExt). +/// The TUN device is **message-oriented** (datagram semantics) — +/// every read/write corresponds to exactly one network packet. +/// Do **not** use `write_all` or byte-stream abstractions; a single +/// `write` that is shorter than the caller intended injects a truncated +/// (garbage) packet into the kernel. pub struct Async { inner: AsyncFd, } @@ -47,88 +34,54 @@ impl Async { /// # Errors /// /// This fails with an error in case of low-level OS errors. - /// - /// # Examples - /// - /// ```rust,no_run - /// # use tun_tap::*; - /// # use tun_tap::aio::Async; - /// # use tokio::io::AsyncReadExt; - /// # #[tokio::main] - /// # async fn main() { - /// let iface = Iface::new("mytun%d", Mode::Tun).unwrap(); - /// let name = iface.name().to_owned(); - /// // Bring the interface up by `ip addr add IP dev $name; ip link set up dev $name` - /// let mut async_iface = Async::new(iface).unwrap(); - /// let mut buf = vec![0u8; 1504]; - /// let n = async_iface.read(&mut buf).await.unwrap(); - /// # } - /// ``` pub fn new(iface: Iface) -> io::Result { iface.set_non_blocking()?; Ok(Async { inner: AsyncFd::new(iface)?, }) } - - /// Returns a shared reference to the underlying [`Iface`]. - pub fn get_ref(&self) -> &Iface { - self.inner.get_ref() - } - - /// Returns a mutable reference to the underlying [`Iface`]. - pub fn get_mut(&mut self) -> &mut Iface { - self.inner.get_mut() - } - - /// Consumes this `Async` and returns the underlying [`Iface`]. - pub fn into_inner(self) -> Iface { - self.inner.into_inner() - } } -impl AsyncRead for Async { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { +impl Async { + /// Receives a single packet from the interface asynchronously. + /// + /// Awaits readability on the underlying fd, then calls + /// [`Iface::recv`] to copy one packet into `buf`. + /// + /// The buffer must be large enough to hold one full packet + /// (MTU + 4-byte packet-info header if enabled). If it is + /// too small the packet is truncated by the kernel. + /// + /// # Returns + /// + /// The number of bytes written into `buf`. + pub async fn recv(&self, buf: &mut [u8]) -> io::Result { loop { - let mut guard = std::task::ready!(self.inner.poll_read_ready(cx))?; - let unfilled = buf.initialize_unfilled(); - match guard.try_io(|inner| inner.get_ref().recv(unfilled)) { - Ok(Ok(n)) => { - buf.advance(n); - return Poll::Ready(Ok(())); - } - Ok(Err(e)) if e.kind() == io::ErrorKind::WouldBlock => continue, - Ok(Err(e)) => return Poll::Ready(Err(e)), + let mut guard = self.inner.readable().await?; + match guard.try_io(|inner| inner.get_ref().recv(buf)) { + Ok(result) => return result, Err(_would_block) => continue, } } } -} -impl AsyncWrite for Async { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { + /// Sends a single packet into the interface asynchronously. + /// + /// Awaits writability on the underlying fd, then calls + /// [`Iface::send`] to inject the packet into the kernel + /// network stack. + /// + /// # Returns + /// + /// The number of bytes sent (should equal `data.len()` under + /// normal conditions). + pub async fn send(&self, data: &[u8]) -> io::Result { loop { - let mut guard = std::task::ready!(self.inner.poll_write_ready(cx))?; - match guard.try_io(|inner| inner.get_ref().send(buf)) { - Ok(result) => return Poll::Ready(result), + let mut guard = self.inner.writable().await?; + match guard.try_io(|inner| inner.get_ref().send(data)) { + Ok(result) => return result, Err(_would_block) => continue, } } } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 9bc62fa..d7ca4de 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -59,3 +59,140 @@ fn it_receives_packets() { assert_eq!(source.port(), 4242); assert_eq!(data, &buf[..num]); } + +#[cfg(feature = "tokio")] +mod aio_tests { + use super::*; + use std::sync::Arc; + use tun_tap::aio::Async; + + #[tokio::test] + #[serial] + async fn it_sents_packets_async() { + let iface = + Iface::without_packet_info("tun10", Mode::Tun).expect("failed to create a TUN device"); + let aio = Async::new(iface).expect("failed to create Async wrapper"); + + let data = [99; 8]; + let socket = UdpSocket::bind("10.10.10.1:2426").expect("failed to bind"); + socket + .send_to(&data, "10.10.10.2:4244") + .expect("socket send failed"); + + let mut buf = [0; 1500]; + let n = aio.recv(&mut buf).await.expect("async recv failed"); + + let packet = &buf[..n]; + let headers = + PacketHeaders::from_ip_slice(packet).expect("failed to parse received packet"); + if let PacketHeaders { + net: Some(NetHeaders::Ipv4(ip, _ext)), + transport: Some(TransportHeader::Udp(udp)), + payload, + .. + } = headers + { + assert_eq!(ip.source, [10, 10, 10, 1]); + assert_eq!(ip.destination, [10, 10, 10, 2]); + assert_eq!(udp.source_port, 2426); + assert_eq!(udp.destination_port, 4244); + assert_eq!(payload.slice(), data); + } else { + panic!( + "unexpected packet structure: {:?}", + std::any::type_name_of_val(&headers) + ); + } + } + + #[tokio::test] + #[serial] + async fn it_receives_packets_async() { + let iface = + Iface::without_packet_info("tun10", Mode::Tun).expect("failed to create a TUN device"); + let aio = Async::new(iface).expect("failed to create Async wrapper"); + + let data = [42; 12]; + let socket = UdpSocket::bind("10.10.10.1:2425").expect("failed to bind"); + + let builder = PacketBuilder::ipv4([10, 10, 10, 2], [10, 10, 10, 1], 20).udp(4243, 2425); + let packet = { + let mut p = Vec::with_capacity(builder.size(data.len())); + builder + .write(&mut p, &data) + .expect("failed to build packet"); + p + }; + + // Send the packet asynchronously into the TUN device. + aio.send(&packet).await.expect("async send failed"); + + let mut buf = [0; 50]; + let (n, src) = socket.recv_from(&mut buf).expect("socket recv failed"); + assert_eq!(n, data.len()); + assert_eq!(&buf[..n], &data); + assert_eq!(src.ip(), IpAddr::V4(Ipv4Addr::new(10, 10, 10, 2))); + assert_eq!(src.port(), 4243); + } + + #[tokio::test(flavor = "multi_thread")] + #[serial] + async fn it_bidirectional_async() { + let iface = + Iface::without_packet_info("tun10", Mode::Tun).expect("failed to create a TUN device"); + // Drain any pending system traffic from the TUN buffer before testing. + iface.set_non_blocking().expect("set_non_blocking"); + let mut drain = [0u8; 4096]; + while iface.recv(&mut drain).is_ok() {} + let aio = Arc::new(Async::new(iface).expect("failed to create Async wrapper")); + + let data = [0xAB; 16]; + let socket = UdpSocket::bind("10.10.10.1:2427").expect("failed to bind"); + + // Spawn a task that sends a packet into TUN via the async sender. + let write_task = { + let aio = aio.clone(); + let data = data; + tokio::spawn(async move { + let builder = + PacketBuilder::ipv4([10, 10, 10, 2], [10, 10, 10, 1], 20).udp(4245, 2427); + let mut packet = Vec::with_capacity(builder.size(data.len())); + builder.write(&mut packet, &data).unwrap(); + aio.send(&packet).await.unwrap(); + }) + }; + + // Meanwhile, the socket should receive the packet. + let mut buf = [0; 64]; + let (n, src) = socket.recv_from(&mut buf).expect("socket recv failed"); + assert_eq!(&buf[..n], &data); + assert_eq!(src.port(), 4245); + + write_task.await.unwrap(); + + // Now send a packet from the socket and read it via async recv. + let read_data = [0xCD; 8]; + socket + .send_to(&read_data, "10.10.10.2:4246") + .expect("socket send failed"); + + let mut read_buf = [0; 1500]; + let n = aio.recv(&mut read_buf).await.expect("async recv failed"); + let headers = PacketHeaders::from_ip_slice(&read_buf[..n]).expect("failed to parse"); + if let PacketHeaders { + net: Some(NetHeaders::Ipv4(ip, _ext)), + transport: Some(TransportHeader::Udp(udp)), + payload, + .. + } = headers + { + assert_eq!(ip.source, [10, 10, 10, 1]); + assert_eq!(ip.destination, [10, 10, 10, 2]); + assert_eq!(udp.source_port, 2427); + assert_eq!(udp.destination_port, 4246); + assert_eq!(payload.slice(), read_data); + } else { + panic!("unexpected packet structure"); + } + } +} From 1e130ab9a50bbc5f39ba5fc0ca82f7e8111c336f Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 27 Jun 2026 16:32:51 +0800 Subject: [PATCH 11/12] docs: update `CHANGELOG.md` --- CHANGELOG.md | 7 +++---- examples/vpn.rs | 1 - src/lib.rs | 8 +++++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca8e7c3..50daf77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,8 @@ ## Breaking Changes -* Async API completely rewritten: `Stream + Sink` (tokio-core 0.1 / futures 0.1) replaced with - `AsyncRead + AsyncWrite` (tokio 1.x / `AsyncFd`). The `Async` struct no longer implements - `Stream` or `Sink`; use `AsyncReadExt` / `AsyncWriteExt` methods instead. +* Async API completely rewritten: The `Async` struct no longer implements + `Stream` or `Sink`. It simply provides async version `recv` and `send` methods for reading/writing packets. * `set_recv_bufsize` removed from `Async`; external buffer sizing is now caller-managed via `AsyncRead`. * Rename module `r#async` to `aio` to avoid conflict with Rust 2024 edition's `async` keyword. @@ -12,7 +11,7 @@ ## Dependency Upgrades * `tokio-core` 0.1 → `tokio` 1.x (features: net, rt, rt-multi-thread, io-util, macros, time) -* `futures` 0.1 → removed (tokio provides async traits) +* `futures` 0.1 → removed * `mio` 0.6 → removed (tokio's `AsyncFd` replaces raw mio) * `etherparse` 0.9 → 0.17 (dev-dependency) * `serial_test` 0.4 → 3.x (dev-dependency) diff --git a/examples/vpn.rs b/examples/vpn.rs index ec5cb99..ab3dc6d 100644 --- a/examples/vpn.rs +++ b/examples/vpn.rs @@ -29,7 +29,6 @@ async fn main() -> io::Result<()> { let socket = Arc::new(UdpSocket::bind(loc_address).await?); let tun = Iface::new("vpn%d", Mode::Tun).unwrap(); let tun = Arc::new(Async::new(tun).unwrap()); - // let (mut tun_reader, mut tun_writer) = tokio::io::split(Async::new(tun).unwrap()); // TUN → UDP (packets from kernel → remote peer) let tun_to_udp = { diff --git a/src/lib.rs b/src/lib.rs index b3a33a4..3c17847 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,10 @@ //! For basic usage, create an [`Iface`](struct.Iface.html) object and call the //! [`send`](struct.Iface.html#method.send) and [`recv`](struct.Iface.html#method.recv) methods. //! -//! You can also use [`Async`](async/struct.Async.html) if you want to integrate with tokio event -//! loop. This is configurable by a feature (it is on by default). +//! You can also use [`aio::Async`] to integrate with tokio's async runtime, +//! providing non-blocking [`recv`](aio/struct.Async.html#method.recv) / +//! [`send`](aio/struct.Async.html#method.send) methods. This is configurable +//! by a feature (it is on by default). //! //! Creating the devices requires `CAP_NETADM` privileges (most commonly done by running as root). //! @@ -21,7 +23,7 @@ //! * It is tested only on Linux and probably doesn't work anywhere else, even though other systems //! have some TUN/TAP support. Reports that it works (or not) and pull request to add other //! system's support are welcome. -//! * The [`Async`](async/struct.Async.html) interface is very minimal and will require extention +//! * The [`aio::Async`] interface is very minimal and will require extension //! for further use cases and better performance. //! * This doesn't support advanced usage patters, like reusing already created device or creating //! persistent devices. Again, pull requests are welcome. From 7f981a9df6dd4ab3a26af03f78eb996867203058 Mon Sep 17 00:00:00 2001 From: LinkWanna Date: Sat, 27 Jun 2026 17:12:11 +0800 Subject: [PATCH 12/12] ci: fix ci --- tests/integration_test.rs | 48 +++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index d7ca4de..01a2b1e 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -152,7 +152,6 @@ mod aio_tests { // Spawn a task that sends a packet into TUN via the async sender. let write_task = { let aio = aio.clone(); - let data = data; tokio::spawn(async move { let builder = PacketBuilder::ipv4([10, 10, 10, 2], [10, 10, 10, 1], 20).udp(4245, 2427); @@ -171,28 +170,43 @@ mod aio_tests { write_task.await.unwrap(); // Now send a packet from the socket and read it via async recv. + // The TUN buffer may contain background kernel traffic (IGMP, MLDv2, + // LLMNR, etc.), so loop until we find our packet by port numbers. let read_data = [0xCD; 8]; socket .send_to(&read_data, "10.10.10.2:4246") .expect("socket send failed"); let mut read_buf = [0; 1500]; - let n = aio.recv(&mut read_buf).await.expect("async recv failed"); - let headers = PacketHeaders::from_ip_slice(&read_buf[..n]).expect("failed to parse"); - if let PacketHeaders { - net: Some(NetHeaders::Ipv4(ip, _ext)), - transport: Some(TransportHeader::Udp(udp)), - payload, - .. - } = headers - { - assert_eq!(ip.source, [10, 10, 10, 1]); - assert_eq!(ip.destination, [10, 10, 10, 2]); - assert_eq!(udp.source_port, 2427); - assert_eq!(udp.destination_port, 4246); - assert_eq!(payload.slice(), read_data); - } else { - panic!("unexpected packet structure"); + loop { + let n = aio.recv(&mut read_buf).await.expect("async recv failed"); + let headers = PacketHeaders::from_ip_slice(&read_buf[..n]).expect("failed to parse"); + let PacketHeaders { + net: Some(NetHeaders::Ipv4(_, _)), + transport: Some(TransportHeader::Udp(udp)), + .. + } = &headers + else { + continue; // skip IPv6, IGMP, etc. + }; + if udp.source_port != 2427 || udp.destination_port != 4246 { + continue; // not our packet + } + // Found it — extract and assert. + if let PacketHeaders { + net: Some(NetHeaders::Ipv4(ip, _ext)), + transport: Some(TransportHeader::Udp(udp)), + payload, + .. + } = headers + { + assert_eq!(ip.source, [10, 10, 10, 1]); + assert_eq!(ip.destination, [10, 10, 10, 2]); + assert_eq!(udp.source_port, 2427); + assert_eq!(udp.destination_port, 4246); + assert_eq!(payload.slice(), read_data); + } + break; } } }