crates/wg-appliance-core/src/addr.rs
//! The ONE ioctl **address writer** for appliance consumers — `SIOCSIFADDR` +
//! `SIOCSIFNETMASK` and `IFF_UP`, no `ip(8)`, no shell, no netlink.
//!
//! LIFTED from tunnr's `payloads/wg-appliance/src/linux.rs` (the suite's
//! measured ioctl configurator), not rewritten — the same lift `keys` made.
//! The crate-root doc's "no address writer here" paragraph is amended by this
//! module: gunnar keeps its own acknowledged-netlink writer (its declared
//! surface, unchanged by prior ruling); every consumer WITHOUT an existing
//! writer routes through this one instead of growing a third.
//!
//! # What the kernel gives you for free, and what stays honest
//!
//! Assigning `10.77.0.7/16` installs the CONNECTED-SUBNET route for
//! `10.77.0.0/16` on the interface as a side effect of the ioctl pair — which
//! is exactly what a hub-and-spoke appliance needs to answer its hub. Routes
//! BEYOND the connected subnet need netlink `RTM_NEWROUTE`, which this module
//! does not carry (tunnr's configurator reports the same op unsupported
//! rather than faking it); declare the appliance's address with its real
//! site prefix and the routing question mostly disappears.
//!
//! The ioctls need `CAP_NET_ADMIN` and a real interface, so the green proof
//! rides the container robot; what is unit-testable here (netmask math,
//! refusals) is.
use std::io;
use std::net::Ipv4Addr;
const IFF_UP: libc::c_short = 0x0001;
const SIOCGIFFLAGS: libc::c_ulong = 0x8913;
const SIOCSIFFLAGS: libc::c_ulong = 0x8914;
const SIOCSIFADDR: libc::c_ulong = 0x8916;
const SIOCSIFNETMASK: libc::c_ulong = 0x891c;
/// Minimal `struct ifreq` (name + a 24-byte union payload) — verbatim tunnr.
#[repr(C)]
struct IfReq {
ifr_name: [libc::c_char; 16],
ifr_union: [u8; 24],
}
impl IfReq {
fn new(name: &str) -> IfReq {
let mut r = IfReq { ifr_name: [0; 16], ifr_union: [0; 24] };
for (i, b) in name.bytes().take(15).enumerate() {
r.ifr_name[i] = b as libc::c_char;
}
r
}
}
/// The ABI assumption every ioctl below rests on, checked by the COMPILER
/// rather than asserted in a comment: `struct ifreq` is `IFNAMSIZ` (16) plus a
/// 24-byte union, so 40 bytes on every LP64 Linux target. The kernel copies
/// exactly that many bytes in and out, so a smaller [`IfReq`] would be a buffer
/// overflow and this line would stop the build.
const _: () = assert!(std::mem::size_of::<IfReq>() == 40);
fn ctl_socket() -> io::Result<libc::c_int> {
// SAFETY: `socket(2)` takes three integers and no pointers, so there is no
// memory for it to touch; the arguments are `libc`'s own constants for a UDP
// socket, the conventional carrier for the `SIOC*` interface ioctls. A
// negative return is checked below, so no caller receives a non-descriptor.
let s = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if s < 0 {
return Err(io::Error::last_os_error());
}
Ok(s)
}
/// The `ifreq`-shaped ioctls this module issues.
///
/// An enum rather than a raw request number, because [`ifreq_ioctl`] is a SAFE
/// function whose soundness rests on the request being one that reads and
/// writes exactly `sizeof(struct ifreq)` bytes. With a bare `c_ulong` parameter
/// a later caller could hand it any request at all — `SIOCGIFCONF`, say, which
/// treats the union as a pointer and a length — and the safety comment below
/// would quietly stop being true. The compiler now enforces what the comment
/// claims.
#[derive(Clone, Copy)]
enum IfReqIoctl {
/// `SIOCSIFADDR`.
SetAddr,
/// `SIOCSIFNETMASK`.
SetNetmask,
/// `SIOCGIFFLAGS`.
GetFlags,
/// `SIOCSIFFLAGS`.
SetFlags,
}
impl IfReqIoctl {
fn code(self) -> libc::c_ulong {
match self {
IfReqIoctl::SetAddr => SIOCSIFADDR,
IfReqIoctl::SetNetmask => SIOCSIFNETMASK,
IfReqIoctl::GetFlags => SIOCGIFFLAGS,
IfReqIoctl::SetFlags => SIOCSIFFLAGS,
}
}
}
/// Run an `ifreq`-shaped ioctl (`SIOCSIFADDR`, `SIOCSIFNETMASK`,
/// `SIOCGIFFLAGS`, `SIOCSIFFLAGS`) and return its raw result.
fn ifreq_ioctl(fd: libc::c_int, code: IfReqIoctl, req: &mut IfReq) -> libc::c_int {
// SAFETY: the four ioctls this is called with are the `ifreq` family, which
// read and write EXACTLY `sizeof(struct ifreq)` bytes at the pointer. The
// `const _` assertion above proves `IfReq` is that size, `&mut IfReq` gives
// a unique, non-null, live pointer for the duration of the call, and
// `#[repr(C)]` fixes the field order to the kernel's. `IfReq` has alignment
// 1 where the C type has 8; that is sound because the kernel moves the
// struct with `copy_{from,to}_user` (byte-wise, no aligned loads) and
// nothing here ever dereferences the pointer as a `struct ifreq` — the
// union is written and read with `copy_from_slice`/`from_ne_bytes`.
unsafe { libc::ioctl(fd, code.code() as libc::Ioctl, req as *mut IfReq) }
}
/// `close(2)` a control socket this module opened.
fn close_fd(fd: libc::c_int) {
// SAFETY: every `fd` reaching here came from `ctl_socket`, which returns
// `Err` on a negative result, and each is closed exactly once on every path
// out of its caller. `close(2)` touches no memory of ours; its result is
// unused because there is nothing a caller could do about a failed close of
// a socket it is finished with.
unsafe { libc::close(fd) };
}
/// Write an IPv4 address (host byte order) as a `sockaddr_in` into an ifreq
/// union — verbatim tunnr.
fn write_sockaddr_in(buf: &mut [u8; 24], addr_host_order: u32) {
buf[..2].copy_from_slice(&(libc::AF_INET as u16).to_ne_bytes());
buf[2..4].copy_from_slice(&0u16.to_ne_bytes());
buf[4..8].copy_from_slice(&addr_host_order.to_be_bytes());
}
/// The netmask a prefix length spells, host byte order. Public so the math is
/// unit-testable where the ioctls are not.
pub fn netmask(prefix: u8) -> u32 {
debug_assert!(prefix <= 32);
if prefix == 0 {
0
} else {
u32::MAX << (32 - prefix)
}
}
/// `SIOCSIFADDR` + `SIOCSIFNETMASK`: put `addr/prefix` on `interface`. The
/// kernel installs the connected-subnet route as a side effect. Needs
/// `CAP_NET_ADMIN`; the error is the OS's, annotated with the ioctl name.
pub fn assign_ipv4(interface: &str, addr: Ipv4Addr, prefix: u8) -> io::Result<()> {
if prefix > 32 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("/{prefix} is not an IPv4 prefix"),
));
}
let s = ctl_socket()?;
let close_and = |rc: libc::c_int, what: &str| -> io::Result<()> {
if rc < 0 {
let e = io::Error::last_os_error();
close_fd(s);
Err(io::Error::new(e.kind(), format!("{what}: {e}")))
} else {
Ok(())
}
};
let mut req = IfReq::new(interface);
write_sockaddr_in(&mut req.ifr_union, u32::from(addr));
close_and(ifreq_ioctl(s, IfReqIoctl::SetAddr, &mut req), "SIOCSIFADDR")?;
let mut req = IfReq::new(interface);
write_sockaddr_in(&mut req.ifr_union, netmask(prefix));
let rc = ifreq_ioctl(s, IfReqIoctl::SetNetmask, &mut req);
let out = close_and(rc, "SIOCSIFNETMASK");
close_fd(s);
out
}
/// `SIOCGIFFLAGS` | `IFF_UP` → `SIOCSIFFLAGS`: bring `interface` up.
pub fn link_up(interface: &str) -> io::Result<()> {
let s = ctl_socket()?;
let mut req = IfReq::new(interface);
let rc = ifreq_ioctl(s, IfReqIoctl::GetFlags, &mut req);
if rc < 0 {
let e = io::Error::last_os_error();
close_fd(s);
return Err(io::Error::new(e.kind(), format!("SIOCGIFFLAGS: {e}")));
}
let mut flags = libc::c_short::from_ne_bytes([req.ifr_union[0], req.ifr_union[1]]);
flags |= IFF_UP;
req.ifr_union[..2].copy_from_slice(&flags.to_ne_bytes());
let rc = ifreq_ioctl(s, IfReqIoctl::SetFlags, &mut req);
let e = io::Error::last_os_error();
close_fd(s);
if rc < 0 {
return Err(io::Error::new(e.kind(), format!("SIOCSIFFLAGS: {e}")));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
/// The mask math, including the two edges a shift gets wrong (`/0` shifts
/// by 32 = UB without the guard; `/32` must be all-ones).
#[test]
fn netmask_math_covers_the_edges() {
assert_eq!(netmask(0), 0);
assert_eq!(netmask(8), 0xff00_0000);
assert_eq!(netmask(16), 0xffff_0000);
assert_eq!(netmask(24), 0xffff_ff00);
assert_eq!(netmask(32), 0xffff_ffff);
}
/// An impossible prefix refuses before any syscall.
#[test]
fn an_impossible_prefix_is_refused() {
let err = assign_ipv4("wg0", Ipv4Addr::new(10, 0, 0, 1), 33).unwrap_err();
assert!(err.to_string().contains("/33"), "{err}");
}
/// Without CAP_NET_ADMIN the ioctl fails WITH ITS NAME in the error — the
/// fail-loud contract the appliance's FAILED line rides on. (Skip as root,
/// where it might succeed against a real interface.)
#[test]
fn without_cap_net_admin_the_refusal_names_the_ioctl() {
// SAFETY: `geteuid(2)` takes no arguments, touches no memory of ours and
// cannot fail; it is `unsafe` only for crossing the `extern "C"`
// boundary.
if unsafe { libc::geteuid() } == 0 {
eprintln!("SKIP without_cap_net_admin_the_refusal_names_the_ioctl: euid 0");
return;
}
let err = assign_ipv4("lo", Ipv4Addr::new(203, 0, 113, 7), 32).unwrap_err();
assert!(err.to_string().contains("SIOCSIFADDR"), "{err}");
}
}