Repository: cloudflare/boringtun (language: rust)

Issue:
Add scaffolding needed to support user namespaces
Modern versions of Linux provide the ability for unprivileged users to create their own namespace - and their own network namespace.

Inside the namespace, we have the ability to create TUN devices and pass the corresponding file descriptor to an external process.  If `boringtun` could utilize an inherited file descriptor (in a manner similar to `wireguard-go`) as the FD corresponding to the TUN device (bypassing any attempts at device creation), we can provide connectivity without elevated privileges (beyond opening a UDP socket).

Similarly, instead of assuming the user API is a named socket on disk, boringtun could provide the ability to inherit a connected end of a `socketpair` from the parent process.  This would allow the parent to completely setup the `boringtun` process without the latter needing any sort of privilege to write into a system directory.



Discussion / hints:

Fix patch:
diff --git a/src/device/api.rs b/src/device/api.rs
index bb932170..945edcb0 100644
--- a/src/device/api.rs
+++ b/src/device/api.rs
@@ -9,7 +9,7 @@ use hex::encode as encode_hex;
 use libc::*;
 use std::fs::{create_dir, remove_file};
 use std::io::{BufRead, BufReader, BufWriter, Write};
-use std::os::unix::io::AsRawFd;
+use std::os::unix::io::{AsRawFd, FromRawFd};
 use std::os::unix::net::{UnixListener, UnixStream};
 use std::sync::atomic::Ordering;
 
@@ -77,6 +77,40 @@ impl<T: Tun, S: Sock> Device<T, S> {
         self.register_api_signal_handlers()
     }
 
+    pub fn register_api_fd(&mut self, fd: i32) -> Result<(), Error> {
+        let io_file = unsafe { UnixStream::from_raw_fd(fd) };
+
+        self.queue.new_event(
+            io_file.as_raw_fd(),
+            Box::new(move |d, _| {
+                // This is the closure that listens on the api file descriptor
+
+                let mut reader = BufReader::new(&io_file);
+                let mut writer = BufWriter::new(&io_file);
+                let mut cmd = String::new();
+                if reader.read_line(&mut cmd).is_ok() {
+                    cmd.pop(); // pop the new line character
+                    let status = match cmd.as_ref() {
+                        // Only two commands are legal according to the protocol, get=1 and set=1.
+                        "get=1" => api_get(&mut writer, d),
+                        "set=1" => api_set(&mut reader, d),
+                        _ => EIO,
+                    };
+                    // The protocol requires to return an error code as the response, or zero on success
+                    writeln!(writer, "errno={}\n", status).ok();
+                } else {
+                    // The remote side is likely closed; we should trigger an exit.
+                    d.trigger_exit();
+                    return Action::Exit;
+                }
+
+                Action::Continue // Indicates the worker thread should continue as normal
+            }),
+        )?;
+
+        Ok(())
+    }
+
     fn register_monitor(&self, path: String) -> Result<(), Error> {
         self.queue.new_periodic_event(
             Box::new(move |d, _| {
diff --git a/src/device/epoll.rs b/src/device/epoll.rs
index 71e63591..bad2d7be 100644
--- a/src/device/epoll.rs
+++ b/src/device/epoll.rs
@@ -382,6 +382,10 @@ impl<'a, H> EventGuard<'a, H> {
         std::mem::forget(self); // Don't call the regular drop that would enable the event
     }
 
+    pub fn fd(&self) -> i32 {
+        return self.event.fd;
+    }
+
     /// Change the event flags to enable or disable notifying when the fd is writable
     pub fn notify_writable(&mut self, enabled: bool) {
         let flags = if enabled {
diff --git a/src/device/kqueue.rs b/src/device/kqueue.rs
index 457ac8e4..babcd127 100644
--- a/src/device/kqueue.rs
+++ b/src/device/kqueue.rs
@@ -328,4 +328,9 @@ impl<'a, H> EventGuard<'a, H> {
         unsafe { self.poll.clear_event_by_fd(self.event.event.ident as RawFd) };
         std::mem::forget(self); // Don't call the regular drop that would enable the event
     }
+
+    /// Stub: only used for Linux-specific features.
+    pub fn fd(&self) -> i32 {
+        return -1;
+    }
 }
diff --git a/src/device/mod.rs b/src/device/mod.rs
index 8014fa94..f955dd5c 100644
--- a/src/device/mod.rs
+++ b/src/device/mod.rs
@@ -134,6 +134,8 @@ pub struct DeviceConfig {
     pub logger: Logger,
     #[cfg(target_os = "linux")]
     pub use_multi_queue: bool,
+    #[cfg(target_os = "linux")]
+    pub uapi_fd: i32,
 }
 
 impl Default for DeviceConfig {
@@ -144,6 +146,8 @@ impl Default for DeviceConfig {
             logger: Logger::root(Discard, o!()),
             #[cfg(target_os = "linux")]
             use_multi_queue: true,
+            #[cfg(target_os = "linux")]
+            uapi_fd: -1,
         }
     }
 }
@@ -174,6 +178,9 @@ pub struct Device<T: Tun, S: Sock> {
     mtu: AtomicUsize,
 
     rate_limiter: Option<Arc<RateLimiter>>,
+
+    #[cfg(target_os = "linux")]
+    uapi_fd: i32,
 }
 
 struct ThreadData<T: Tun> {
@@ -251,6 +258,11 @@ impl<T: Tun, S: Sock> DeviceHandle<T, S> {
             iface: Arc::clone(&device.read().iface),
         };
 
+        #[cfg(not(target_os = "linux"))]
+        let uapi_fd = -1;
+        #[cfg(target_os = "linux")]
+        let uapi_fd = device.read().uapi_fd;
+
         loop {
             // The event loop keeps a read lock on the device, because we assume write access is rarely needed
             let mut device_lock = device.read();
@@ -270,6 +282,10 @@ impl<T: Tun, S: Sock> DeviceHandle<T, S> {
                         }
                     }
                     WaitResult::EoF(handler) => {
+                        if uapi_fd >= 0 && uapi_fd == handler.fd() {
+                            device_lock.trigger_exit();
+                            return;
+                        }
                         handler.cancel();
                     }
                     WaitResult::Error(e) => error!(device_lock.config.logger, "Poll error {:}", e),
@@ -373,6 +389,11 @@ impl<T: Tun, S: Sock> Device<T, S> {
         let iface = Arc::new(T::new(name)?.set_non_blocking()?);
         let mtu = iface.mtu()?;
 
+        #[cfg(not(target_os = "linux"))]
+        let uapi_fd = -1;
+        #[cfg(target_os = "linux")]
+        let uapi_fd = config.uapi_fd;
+
         let mut device = Device {
             queue: Arc::new(poll),
             iface,
@@ -391,9 +412,15 @@ impl<T: Tun, S: Sock> Device<T, S> {
             cleanup_paths: Default::default(),
             mtu: AtomicUsize::new(mtu),
             rate_limiter: None,
+            #[cfg(target_os = "linux")]
+            uapi_fd,
         };
 
-        device.register_api_handler()?;
+        if uapi_fd >= 0 {
+            device.register_api_fd(uapi_fd)?;
+        } else {
+            device.register_api_handler()?;
+        }
         device.register_iface_handler(Arc::clone(&device.iface))?;
         device.register_notifiers()?;
         device.register_timers()?;
diff --git a/src/device/tun_linux.rs b/src/device/tun_linux.rs
index 2131d1b3..fce4c400 100644
--- a/src/device/tun_linux.rs
+++ b/src/device/tun_linux.rs
@@ -76,11 +76,19 @@ impl TunSocket {
 
 impl Tun for TunSocket {
     fn new(name: &str) -> Result<TunSocket, Error> {
+        // If the provided name appears to be a FD, use that.
+        let provided_fd = name.parse::<i32>();
+        if provided_fd.is_ok() {
+            return Ok(TunSocket {
+                fd: provided_fd.unwrap(),
+                name: name.to_string(),
+            });
+        }
+
         let fd = match unsafe { open(b"/dev/net/tun\0".as_ptr() as _, O_RDWR) } {
             -1 => return Err(Error::Socket(errno_str())),
             fd => fd,
         };
-
         let iface_name = name.as_bytes();
         let mut ifr = ifreq {
             ifr_name: [0; IFNAMSIZ],
@@ -119,6 +127,11 @@ impl Tun for TunSocket {
 
     /// Get the current MTU value
     fn mtu(&self) -> Result<usize, Error> {
+        let provided_fd = self.name.parse::<i32>();
+        if provided_fd.is_ok() {
+            return Ok(1500);
+        }
+
         let fd = match unsafe { socket(AF_INET, SOCK_STREAM, IPPROTO_IP) } {
             -1 => return Err(Error::Socket(errno_str())),
             fd => fd,
diff --git a/src/main.rs b/src/main.rs
index 53013726..d66d87e9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -62,6 +62,16 @@ fn main() {
                 .possible_values(&["error", "info", "debug", "trace"])
                 .help("Log verbosity")
                 .default_value("error"),
+            Arg::with_name("uapi-fd")
+                .long("uapi-fd")
+                .env("WG_UAPI_FD")
+                .help("File descriptor for the user API")
+                .default_value("-1"),
+            Arg::with_name("tun-fd")
+                .long("tun-fd")
+                .env("WG_TUN_FD")
+                .help("File descriptor for an already-existing TUN device")
+                .default_value("-1"),
             Arg::with_name("log")
                 .takes_value(true)
                 .long("log")
@@ -84,7 +94,13 @@ fn main() {
         .get_matches();
 
     let background = !matches.is_present("foreground");
-    let tun_name = matches.value_of("INTERFACE_NAME").unwrap();
+    #[cfg(target_os = "linux")]
+    let uapi_fd = value_t!(matches.value_of("uapi-fd"), i32).unwrap_or_else(|e| e.exit());
+    let tun_fd = value_t!(matches.value_of("tun-fd"), isize).unwrap_or_else(|e| e.exit());
+    let mut tun_name = matches.value_of("INTERFACE_NAME").unwrap();
+    if tun_fd >= 0 {
+        tun_name = matches.value_of("tun-fd").unwrap();
+    }
     let n_threads = value_t!(matches.value_of("threads"), usize).unwrap_or_else(|e| e.exit());
     let log_level =
         value_t!(matches.value_of("verbosity"), slog::Level).unwrap_or_else(|e| e.exit());
@@ -139,6 +155,8 @@ fn main() {
     let config = DeviceConfig {
         n_threads,
         logger: logger.clone(),
+        #[cfg(target_os = "linux")]
+        uapi_fd: uapi_fd,
         use_connected_socket: !matches.is_present("disable-connected-udp"),
         #[cfg(target_os = "linux")]
         use_multi_queue: !matches.is_present("disable-multi-queue"),


Test patch:
diff --git a/src/device/integration_tests/mod.rs b/src/device/integration_tests/mod.rs
index ac7acdd0..0bd9be2b 100644
--- a/src/device/integration_tests/mod.rs
+++ b/src/device/integration_tests/mod.rs
@@ -278,6 +278,8 @@ mod tests {
                     use_connected_socket: true,
                     #[cfg(target_os = "linux")]
                     use_multi_queue: true,
+                    #[cfg(target_os = "linux")]
+                    uapi_fd: -1,
                 },
             )
         }
@@ -568,6 +570,8 @@ mod tests {
                 use_connected_socket: false,
                 #[cfg(target_os = "linux")]
                 use_multi_queue: true,
+                #[cfg(target_os = "linux")]
+                uapi_fd: -1,
             },
         );
 
@@ -728,6 +732,8 @@ mod tests {
                 use_connected_socket: false,
                 #[cfg(target_os = "linux")]
                 use_multi_queue: true,
+                #[cfg(target_os = "linux")]
+                uapi_fd: -1,
             },
         );