diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6110c25 --- /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: sudo env "PATH=$PATH" "HOME=$HOME" cargo test + - name: Clippy + run: cargo clippy --all-targets -- -D warnings + - name: Doc + run: cargo doc --no-deps 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..50daf77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# 0.2.0 + +## Breaking Changes + +* 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. + +## Dependency Upgrades + +* `tokio-core` 0.1 → `tokio` 1.x (features: net, rt, rt-multi-thread, io-util, macros, time) +* `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) + +## 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..5cf76bc 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,19 @@ 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" +tokio = ["dep:tokio", "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" +version-sync = "0.9" +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..b4e7ff7 100644 --- a/examples/pingpong.rs +++ b/examples/pingpong.rs @@ -5,10 +5,6 @@ //! //! 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; use std::process::Command; use std::sync::Arc; @@ -19,10 +15,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,6 +53,8 @@ fn main() { assert!(amount == PING.len()); } }); + + #[allow(unused_variables)] let reader = thread::spawn(move || { // MTU + TUN header let mut buffer = vec![0; 1504]; @@ -65,8 +65,6 @@ fn main() { println!("Packet: {:?}", &buffer[4..size]); } }); - writer.join() - .unwrap(); - reader.join() - .unwrap(); + writer.join().unwrap(); + reader.join().unwrap(); } diff --git a/examples/tokio.rs b/examples/tokio.rs index 6dbb007..b8fe1ee 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::sync::Arc; use std::time::Duration; -use futures::{Future, Stream}; -use tokio_core::reactor::{Core, Interval}; +use tokio::time; +use tun_tap::aio::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,39 @@ 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) // 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 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"); - PING.to_owned() + 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; + } + } }) - .forward(sink); - let reader = stream.for_each(|packet| { - println!("Received: {:?}", packet); - Ok(()) - }); - 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..ab3dc6d 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,53 @@ //! 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::net::UdpSocket; +use tun_tap::aio::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 tun = Arc::new(Async::new(tun).unwrap()); + + // TUN → UDP (packets from kernel → remote peer) + let tun_to_udp = { + let socket = socket.clone(); + let tun = tun.clone(); + tokio::spawn(async move { + let mut buf = vec![0u8; 1504]; + loop { + let n = tun.recv(&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 = { + 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.send(&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/aio.rs b/src/aio.rs new file mode 100644 index 0000000..38feb82 --- /dev/null +++ b/src/aio.rs @@ -0,0 +1,87 @@ +//! Integration of TUN/TAP into tokio. +//! +//! 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 tokio::io::unix::AsyncFd; + +use super::Iface; + +/// An asynchronous wrapper around a TUN/TAP [`Iface`]. +/// +/// 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. +/// +/// 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, +} + +impl Async { + /// Consumes an `Iface` and wraps it in a new `Async`. + /// + /// Sets the underlying fd to non-blocking mode automatically. + /// + /// # Errors + /// + /// This fails with an error in case of low-level OS errors. + pub fn new(iface: Iface) -> io::Result { + iface.set_non_blocking()?; + Ok(Async { + inner: AsyncFd::new(iface)?, + }) + } +} + +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 = self.inner.readable().await?; + match guard.try_io(|inner| inner.get_ref().recv(buf)) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } + + /// 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 = self.inner.writable().await?; + match guard.try_io(|inner| inner.get_ref().send(data)) { + Ok(result) => return result, + Err(_would_block) => continue, + } + } + } +} diff --git a/src/async.rs b/src/async.rs deleted file mode 100644 index c54fc2b..0000000 --- a/src/async.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! 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; - -use std::io::{Error, ErrorKind, Read, Result, Write}; -use std::os::unix::io::AsRawFd; - -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 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. -/// -/// This turns the synchronous `Iface` into an asynchronous `Sink + Stream` of packets. -pub struct Async { - mio: PollEvented, - recv_bufsize: usize, -} - -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. - /// - /// # Errors - /// - /// This fails with an error in case of low-level OS errors (they shouldn't usually happen). - /// - /// # 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() { - /// 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(); - /// # } - /// ``` - pub fn new(iface: Iface, handle: &Handle) -> Result { - iface.set_non_blocking()?; - Ok(Async { - mio: PollEvented::new(MioWrapper { iface }, handle)?, - recv_bufsize: 1542, - }) - } - /// 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; - } -} - -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 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), - } - } - fn poll_complete(&mut self) -> FPoll<(), Self::SinkError> { - Ok(FAsync::Ready(())) - } -} diff --git a/src/lib.rs b/src/lib.rs index 7917579..3c17847 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)] @@ -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. @@ -34,9 +36,9 @@ use std::os::raw::{c_char, c_int}; use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd}; #[cfg(feature = "tokio")] -pub mod async; +pub mod aio; -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..01a2b1e 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"); } @@ -63,3 +59,154 @@ 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(); + 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. + // 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]; + 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; + } + } +}