From f4dc096d83bae557bb68c1f6c87ced24c8fcfac2 Mon Sep 17 00:00:00 2001
From: Evan Pratten <ewpratten@gmail.com>
Date: Wed, 2 Aug 2023 12:22:00 -0400
Subject: [PATCH] Implement `easy-tun` crate

---
 Cargo.toml                              | 109 +++++++++++------------
 libs/easy-tun/Cargo.toml                |  22 +++++
 libs/easy-tun/README.md                 |   3 +
 libs/easy-tun/examples/print_traffic.rs |  17 ++++
 libs/easy-tun/src/lib.rs                |   7 ++
 libs/easy-tun/src/tun.rs                | 110 ++++++++++++++++++++++++
 6 files changed, 215 insertions(+), 53 deletions(-)
 create mode 100644 libs/easy-tun/Cargo.toml
 create mode 100644 libs/easy-tun/README.md
 create mode 100644 libs/easy-tun/examples/print_traffic.rs
 create mode 100644 libs/easy-tun/src/lib.rs
 create mode 100644 libs/easy-tun/src/tun.rs

diff --git a/Cargo.toml b/Cargo.toml
index 37458a9..4033a35 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,56 +1,59 @@
-[package]
-name = "protomask"
-version = "0.2.0"
-authors = ["Evan Pratten <ewpratten@gmail.com>"]
-edition = "2021"
-description = "A user space NAT64 implementation"
-readme = "README.md"
-homepage = "https://github.com/ewpratten/protomask"
-documentation = "https://docs.rs/protomask"
-repository = "https://github.com/ewpratten/protomask"
-license = "GPL-3.0"
-keywords = []
-categories = []
+# [package]
+# name = "protomask"
+# version = "0.2.0"
+# authors = ["Evan Pratten <ewpratten@gmail.com>"]
+# edition = "2021"
+# description = "A user space NAT64 implementation"
+# readme = "README.md"
+# homepage = "https://github.com/ewpratten/protomask"
+# documentation = "https://docs.rs/protomask"
+# repository = "https://github.com/ewpratten/protomask"
+# license = "GPL-3.0"
+# keywords = []
+# categories = []
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-[dependencies]
-protomask-tun = { path = "protomask-tun", version = "0.1.0" }
-tokio = { version = "1.29.1", features = [
-    "macros",
-    "rt-multi-thread",
-    # "process",
-    "sync"
-] }
-clap = { version = "4.3.11", features = ["derive"] }
-serde = { version = "1.0.171", features = ["derive"] }
-ipnet = { version = "2.8.0", features = ["serde"] }
-hyper = { version = "0.14.27", features = ["server", "http1", "tcp"] }
-owo-colors = { version = "3.5.0", features = ["supports-colors"] }
-toml = "0.7.6"
-log = "0.4.19"
-fern = "0.6.2"
-serde_path_to_error = "0.1.13"
-thiserror = "1.0.43"
-tun-tap = "0.1.3"
-bimap = "0.6.3"
-pnet_packet = "0.34.0"
-rtnetlink = "0.13.0"
-futures = "0.3.28"
-prometheus = "0.13.3"
-lazy_static = "1.4.0"
+# # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+# [dependencies]
+# protomask-tun = { path = "protomask-tun", version = "0.1.0" }
+# tokio = { version = "1.29.1", features = [
+#     "macros",
+#     "rt-multi-thread",
+#     # "process",
+#     "sync"
+# ] }
+# clap = { version = "4.3.11", features = ["derive"] }
+# serde = { version = "1.0.171", features = ["derive"] }
+# ipnet = { version = "2.8.0", features = ["serde"] }
+# hyper = { version = "0.14.27", features = ["server", "http1", "tcp"] }
+# owo-colors = { version = "3.5.0", features = ["supports-colors"] }
+# toml = "0.7.6"
+# log = "0.4.19"
+# fern = "0.6.2"
+# serde_path_to_error = "0.1.13"
+# thiserror = "1.0.43"
+# tun-tap = "0.1.3"
+# bimap = "0.6.3"
+# pnet_packet = "0.34.0"
+# rtnetlink = "0.13.0"
+# futures = "0.3.28"
+# prometheus = "0.13.3"
+# lazy_static = "1.4.0"
 
-[[bin]]
-name = "protomask"
-path = "src/cli/main.rs"
+# [[bin]]
+# name = "protomask"
+# path = "src/cli/main.rs"
 
-[package.metadata.deb]
-section = "network"
-assets = [
-    ["target/release/protomask", "/usr/local/bin/protomask", "755"],
-    ["./protomask.toml", "/etc/protomask.toml", "644"],
-    ["README.md", "usr/share/doc/protomask/README.md", "644"]
-]
-conf-files = ["/etc/protomask.toml"]
-depends = []
-maintainer-scripts = "./debian/"
-systemd-units = { enable = false }
\ No newline at end of file
+# [package.metadata.deb]
+# section = "network"
+# assets = [
+#     ["target/release/protomask", "/usr/local/bin/protomask", "755"],
+#     ["./protomask.toml", "/etc/protomask.toml", "644"],
+#     ["README.md", "usr/share/doc/protomask/README.md", "644"]
+# ]
+# conf-files = ["/etc/protomask.toml"]
+# depends = []
+# maintainer-scripts = "./debian/"
+# systemd-units = { enable = false }
+
+[workspace]
+members=["libs/easy-tun"]
\ No newline at end of file
diff --git a/libs/easy-tun/Cargo.toml b/libs/easy-tun/Cargo.toml
new file mode 100644
index 0000000..e638933
--- /dev/null
+++ b/libs/easy-tun/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "easy-tun"
+version = "0.1.0"
+authors = ["Evan Pratten <ewpratten@gmail.com>"]
+edition = "2021"
+description = "A pure-rust TUN interface library"
+readme = "README.md"
+homepage = "https://github.com/ewpratten/protomask/tree/master/libs/easy-tun"
+documentation = "https://docs.rs/easy-tun"
+repository = "https://github.com/ewpratten/protomask"
+license = "GPL-3.0"
+keywords = []
+categories = []
+
+[dependencies]
+log = "^0.4"
+libc = "^0.2"
+ioctl-gen = "^0.1.1"
+rtnetlink = "^0.13.0"
+
+[dev-dependencies]
+env_logger = "0.10.0"
diff --git a/libs/easy-tun/README.md b/libs/easy-tun/README.md
new file mode 100644
index 0000000..7961b7b
--- /dev/null
+++ b/libs/easy-tun/README.md
@@ -0,0 +1,3 @@
+# easy-tun
+
+`easy-tun` is a pure-Rust library that can bring up and manage a TUN interface by directly interacting with the [Universal TUN/TAP Driver](https://docs.kernel.org/networking/tuntap.html).
diff --git a/libs/easy-tun/examples/print_traffic.rs b/libs/easy-tun/examples/print_traffic.rs
new file mode 100644
index 0000000..ac44ebb
--- /dev/null
+++ b/libs/easy-tun/examples/print_traffic.rs
@@ -0,0 +1,17 @@
+use easy_tun::Tun;
+use std::io::Read;
+
+fn main() {
+    // Enable logs
+    env_logger::init();
+
+    // Bring up a TUN interface
+    let mut tun = Tun::new("tun%d").unwrap();
+
+    // Loop and read from the interface
+    let mut buffer = [0u8; 1500];
+    loop {
+        let length = tun.read(&mut buffer).unwrap();
+        println!("{:?}", &buffer[..length]);
+    }
+}
diff --git a/libs/easy-tun/src/lib.rs b/libs/easy-tun/src/lib.rs
new file mode 100644
index 0000000..2a78190
--- /dev/null
+++ b/libs/easy-tun/src/lib.rs
@@ -0,0 +1,7 @@
+#![deny(clippy::pedantic)]
+#![allow(clippy::module_name_repetitions)]
+#![allow(clippy::missing_errors_doc)]
+#![allow(clippy::missing_panics_doc)]
+
+mod tun;
+pub use tun::Tun;
\ No newline at end of file
diff --git a/libs/easy-tun/src/tun.rs b/libs/easy-tun/src/tun.rs
new file mode 100644
index 0000000..7e7d3ab
--- /dev/null
+++ b/libs/easy-tun/src/tun.rs
@@ -0,0 +1,110 @@
+use std::{
+    fs::{File, OpenOptions},
+    io::{Read, Write},
+    mem::size_of,
+    os::fd::{AsRawFd, IntoRawFd, RawFd},
+};
+
+use ioctl_gen::{ioc, iow};
+use libc::{__c_anonymous_ifr_ifru, ifreq, ioctl, IFF_NO_PI, IFF_TUN, IF_NAMESIZE};
+
+/// A TUN device
+pub struct Tun {
+    /// Internal file descriptor for the TUN device
+    fd: File,
+    /// Device name
+    name: String,
+}
+
+impl Tun {
+    /// Creates a new Tun device with the given name.
+    ///
+    /// The `name` argument must be less than the system's `IFNAMSIZ` constant,
+    /// and may contain a `%d` format specifier to allow for multiple devices with the same name.
+    #[allow(clippy::cast_possible_truncation)]
+    #[allow(clippy::cast_lossless)]
+    pub fn new(dev: &str) -> Result<Self, std::io::Error> {
+        // Get a file descriptor for `/dev/net/tun`
+        log::trace!("Opening /dev/net/tun");
+        let fd = OpenOptions::new()
+            .read(true)
+            .write(true)
+            .open("/dev/net/tun")?;
+
+        // Copy the device name into a C string with padding
+        // NOTE: No zero padding is needed because we pre-init the array to all 0s
+        let mut dev_cstr = [0i8; IF_NAMESIZE];
+        let dev_bytes: Vec<i8> = dev.chars().map(|c| c as i8).collect();
+        let dev_len = dev_bytes.len().min(IF_NAMESIZE);
+        log::trace!("Device name length after truncation: {}", dev_len);
+        dev_cstr[..dev_len].copy_from_slice(&dev_bytes[..dev_len]);
+
+        // Build an `ifreq` struct to send to the kernel
+        let mut ifr = ifreq {
+            ifr_name: dev_cstr,
+            ifr_ifru: __c_anonymous_ifr_ifru {
+                ifru_flags: (IFF_TUN | IFF_NO_PI) as i16,
+            },
+        };
+
+        // Make an ioctl call to create the TUN device
+        log::trace!("Calling ioctl to create TUN device");
+        let err = unsafe {
+            ioctl(
+                fd.as_raw_fd(),
+                iow!('T', 202, size_of::<libc::c_int>()) as u64,
+                &mut ifr,
+            )
+        };
+        log::trace!("ioctl returned: {}", err);
+
+        // Check for errors
+        if err < 0 {
+            log::error!("ioctl failed: {}", err);
+            return Err(std::io::Error::last_os_error());
+        }
+
+        // Get the name of the device
+        let name = unsafe { std::ffi::CStr::from_ptr(ifr.ifr_name.as_ptr()) }
+            .to_str()
+            .unwrap()
+            .to_string();
+
+        // Build the TUN struct
+        Ok(Self { fd, name })
+    }
+
+    /// Get the name of the TUN device
+    #[must_use]
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+}
+
+impl AsRawFd for Tun {
+    fn as_raw_fd(&self) -> RawFd {
+        self.fd.as_raw_fd()
+    }
+}
+
+impl IntoRawFd for Tun {
+    fn into_raw_fd(self) -> RawFd {
+        self.fd.into_raw_fd()
+    }
+}
+
+impl Read for Tun {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        self.fd.read(buf)
+    }
+}
+
+impl Write for Tun {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.fd.write(buf)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        self.fd.flush()
+    }
+}