Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions build/guest-tests/zones-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,49 @@ else
fail "reattach-egress" "untrusted (init ${upid:-none}): gateway before ${out_before}; eth0 gone while down ${eth0_gone}; gateway while down ${out_down}; gateway after ${out_after}; $(tail -2 "$LOG/untrusted-bg.out" | tr '\n' ' ')"
fi
"$KD" stop untrusted >/dev/null 2>&1; wait "$UBG" 2>/dev/null
# A net zone can rename its NIC, which comes back to zone 0 under that name:
# a leading dash would reach the next zone's dhcpcd as an option, and a quote
# would end the name in its nft set. The next start renames it nic<N> first.
# Its address, MTU, an altname and an alias come back as that zone set them
# too; an altname kryptik0, which the zone takes once it has deleted its own
# bridge, would stop the next start making the bridge. The
# next start puts back the NIC's own address and MTU and drops the rest.
link_of() { sed -n 's/.* mtu \([0-9]*\) .*link\/ether \([0-9a-f:]*\) .*/\2 \1/p'; }
ninit="$(cut -d' ' -f1 /run/kryptik/zones/net/init.pid 2>/dev/null)"
own_link="$(nsenter -t "${ninit:-0}" -n ip -o link show dev eth0 2>/dev/null | link_of)"
# Seventy long altnames first: the link's listing then outgrows one 8 KiB read.
long="$(head -c 100 /dev/zero | tr '\0' a)"
named="$(nsenter -t "${ninit:-0}" -n sh -c 'ip link set dev eth0 down && ip link set dev eth0 address 02:00:5e:00:53:01 mtu 1400 && for i in $(seq 1 70); do ip link property add dev eth0 altname "$1$i" || exit 1; done && ip link del kryptik0 && ip link property add dev eth0 altname kryptik0 && ip link set dev eth0 alias left-by-the-zone && ip link set dev eth0 name "-x\"y" && echo NAMED' sh "$long" 2>&1)"
rn_before="$(ready_count)"
s6-svc -d /run/service/net-zone
back=""
for _ in $(seq 1 20); do back="$(physical)"; [[ -n "$back" ]] && break; sleep 0.5; done
back_link="$(ip -o link show dev '-x"y' 2>/dev/null | link_of)"
s6-svc -u /run/service/net-zone
took=""
for _ in $(seq 1 60); do [[ "$(ready_count)" -gt "$rn_before" ]] && { took="$(last_ready)"; break; }; sleep 1; done
plain_uplink='uplinks=nic[0-9]+( |$)'
if [[ "$named" == *NAMED* && "$back" == '-x"y ' && "$took" =~ $plain_uplink && -z "$(physical)" ]]; then
pass "uplink-renamed-plain" "the net zone named its NIC -x\"y, it came back to zone 0 so named, and the next start took it as ${took##*uplinks=}"
else
fail "uplink-renamed-plain" "renamed: ${named:-nothing}; back in zone 0: ${back:-nothing}; READY: ${took:-none}; $(netzone_said 'NOT READY' | tail -1); zone 0 holds: $(physical)"
fi
nnow="$(cut -d' ' -f1 /run/kryptik/zones/net/init.pid 2>/dev/null)"
taken_link="$(nsenter -t "${nnow:-0}" -n ip -o link show dev "${took##*uplinks=}" 2>/dev/null | link_of)"
taken_extra="$(nsenter -t "${nnow:-0}" -n ip -d -o link show dev "${took##*uplinks=}" 2>/dev/null | grep -o 'altname [^ ]*\|alias [^ ]*' | tr '\n' ' ')"
if [[ -n "$own_link" && "$back_link" == "02:00:5e:00:53:01 1400" && "$taken_link" == "${own_link% *} 1500" && -n "$taken_link" && -z "$taken_extra" ]]; then
pass "uplink-state-reset" "the net zone left its NIC at 02:00:5e:00:53:01, MTU 1400, 71 altnames (kryptik0 among them) and an alias; the next start took it back to ${taken_link}, with neither"
else
fail "uplink-state-reset" "own: ${own_link:-unread}; back in zone 0: ${back_link:-unread}; in the next net zone: ${taken_link:-unread} ${taken_extra}; $(uncaught | grep -a 'kryptikd: .*\(hardware gives it\|altname\|Wake-on-LAN\)' | tail -3 | tr '\n' ' ')"
fi
info "uplink-wol virtio-net has no Wake-on-LAN, so this run shows the address and MTU being set back, not Wake-on-LAN"
# eth0 again, for what follows: the NIC is back in zone 0, down, while the net zone is.
s6-svc -d /run/service/net-zone
for _ in $(seq 1 20); do [[ -n "$(physical)" ]] && break; sleep 0.5; done
for d in /sys/class/net/*; do [[ -e "$d/device" ]] && ip link set dev "${d##*/}" name eth0; done
rn_before="$(ready_count)"
s6-svc -u /run/service/net-zone
for _ in $(seq 1 60); do [[ "$(ready_count)" -gt "$rn_before" ]] && break; sleep 1; done

# --- zones: the net zone over a radio -----------------------------------------------
# QEMU has no radio, so mac80211_hwsim makes two. phy1 goes into a network
Expand Down Expand Up @@ -659,6 +702,39 @@ if [[ "$ZOUT" == *AP-REACHED* && "${sta_seen:-0}" -ge 1 ]]; then
else
fail "wifi-egress" "$ZOUT; stations at the access point: ${sta_seen:-0}; $(tail -2 "$LOG/untrusted.err" | tr '\n' ' ')"
fi
# A net zone can leave its radio with a second netdev or with none, and the
# radio comes back to zone 0 so. Each round makes that change in the net
# zone, stops it, waits for phy0 in zone 0 and starts it: the next zone must
# get the radio with one fresh station, not one the last zone named (amon0
# sorts before the station), and join the access point again.
radio_rounds=""
radio_netdevs() { nsenter -t "${1:-0}" -n iw dev 2>/dev/null | awk '$1 == "Interface" { n++; s = s " " $2 } END { printf "%d%s", n, s }'; }
for round in extra none; do
ninit="$(cut -d' ' -f1 /run/kryptik/zones/net/init.pid 2>/dev/null)"
if [[ "$round" = extra ]]; then
# By index: iw finds a phy's name in sysfs, which is zone 0's here and no longer lists it.
phy="$(nsenter -t "${ninit:-0}" -n iw dev 2>/dev/null | sed -n 's/^phy#\([0-9]*\)$/\1/p' | head -1)"
nsenter -t "${ninit:-0}" -n iw "phy#${phy:-0}" interface add amon0 type monitor > "$LOG/radio-$round.out" 2>&1
else
for w in $(nsenter -t "${ninit:-0}" -n iw dev 2>/dev/null | awk '$1 == "Interface" { print $2 }'); do
nsenter -t "${ninit:-0}" -n iw dev "$w" del >> "$LOG/radio-$round.out" 2>&1
done
fi
left="$(radio_netdevs "$ninit")"
rb="$(ready_count)"
s6-svc -d /run/service/net-zone
for _ in $(seq 1 30); do [[ -e /sys/class/ieee80211/phy0 ]] && break; sleep 0.5; done
s6-svc -u /run/service/net-zone
line="$(ready_after "$rb" " wifi=$AP_SSID " 90)"
joined="did not join"; [[ "$line" == *" wifi=$AP_SSID "* ]] && joined=joined
radio_rounds="${radio_rounds}${round}: left ${left}, back with $(radio_netdevs "$(cut -d' ' -f1 /run/kryptik/zones/net/init.pid 2>/dev/null)"), ${joined}; "
done
fresh_radio='^extra: left 2 [^,]*, back with 1 nic[0-9]+, joined; none: left 0, back with 1 nic[0-9]+, joined; $'
if [[ "$radio_rounds" =~ $fresh_radio ]]; then
pass "radio-recarried" "netdevs on the radio as the net zone left it and as the next one got it: ${radio_rounds}"
else
fail "radio-recarried" "${radio_rounds}$(uncaught | grep -a 'kryptikd: \(wiphy\|interface\)' | tail -3 | tr '\n' ' ') $(tr '\n' ' ' < "$LOG/radio-extra.out")"
fi
# personal's definition does not open the radio's own network: once the net
# zone has taken the access point as a gateway, its address is refused
# personal, and what lies past it is not.
Expand Down
215 changes: 211 additions & 4 deletions compartments/kryptikd/src/netlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,24 @@ const GENL_ID_CTRL: u16 = 0x10;
const CTRL_CMD_GETFAMILY: u8 = 3;
const CTRL_ATTR_FAMILY_ID: u16 = 1;
const CTRL_ATTR_FAMILY_NAME: u16 = 2;
const NL80211_CMD_NEW_INTERFACE: u8 = 7;
const NL80211_CMD_DEL_INTERFACE: u8 = 8;
const NL80211_CMD_SET_WIPHY_NETNS: u8 = 49;
const NL80211_ATTR_WIPHY: u16 = 1;
const NL80211_ATTR_IFINDEX: u16 = 3;
const NL80211_ATTR_IFNAME: u16 = 4;
const NL80211_ATTR_IFTYPE: u16 = 5;
const NL80211_IFTYPE_STATION: u32 = 2;
const NL80211_ATTR_NETNS_FD: u16 = 219;
const NLA_TYPE_MASK: u16 = 0x3fff;

const RTM_NEWLINK: u16 = 16;
const RTM_DELLINK: u16 = 17;
const RTM_GETLINK: u16 = 18;
const RTM_SETLINK: u16 = 19;
const RTM_NEWADDR: u16 = 20;
const RTM_NEWROUTE: u16 = 24;
const RTM_DELLINKPROP: u16 = 109;
const NLMSG_ERROR: u16 = 2;
const NLMSG_DONE: u16 = 3;

Expand All @@ -35,11 +43,15 @@ const NLA_F_NESTED: u16 = 0x8000;

const IFLA_ADDRESS: u16 = 1;
const IFLA_IFNAME: u16 = 3;
const IFLA_MTU: u16 = 4;
const IFLA_MASTER: u16 = 10;
// A bridge acks and ignores an unknown attribute, so the isolation test checks sysfs and the wire.
const IFLA_PROTINFO: u16 = 12;
const IFLA_LINKINFO: u16 = 18;
const IFLA_IFALIAS: u16 = 20;
const IFLA_NET_NS_FD: u16 = 28;
const IFLA_PROP_LIST: u16 = 52;
const IFLA_ALT_IFNAME: u16 = 53;
const IFLA_INFO_KIND: u16 = 1;
const IFLA_INFO_DATA: u16 = 2;
const VETH_INFO_PEER: u16 = 1;
Expand Down Expand Up @@ -151,8 +163,9 @@ impl Msg {
}
}

/// Cap on the reply payload one request gathers; replies are a few hundred bytes.
const MAX_REPLY: usize = 64 * 1024;
/// Cap on the reply payload one request gathers: most are a few hundred bytes, and a link's
/// altnames, up to 64 KiB, are the most any reply here carries.
const MAX_REPLY: usize = 128 * 1024;

/// One request/ack exchange on a fresh NETLINK_ROUTE socket.
fn transact(msg: Vec<u8>, what: &str) -> io::Result<()> {
Expand Down Expand Up @@ -183,9 +196,22 @@ fn transact_on(proto: libc::c_int, msg: Vec<u8>, what: &str) -> io::Result<Vec<u
if sent < 0 {
return Err(io::Error::last_os_error());
}
let mut buf = [0u8; 8192];
let mut buf = Vec::new();
let mut replies = Vec::new();
loop {
// Each read sized to its message: a link's altnames alone can reach 64 KiB.
let need = unsafe { libc::recv(fd, std::ptr::null_mut(), 0, libc::MSG_PEEK | libc::MSG_TRUNC) };
if need < 0 {
let e = io::Error::last_os_error();
if e.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(e);
}
if need as usize > MAX_REPLY {
return Err(io::Error::new(io::ErrorKind::InvalidData, "netlink reply too large"));
}
buf.resize((need as usize).max(16), 0);
let n = unsafe { libc::recv(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) };
if n < 0 {
let e = io::Error::last_os_error();
Expand Down Expand Up @@ -247,6 +273,27 @@ fn check_name(name: &str) -> io::Result<()> {
Ok(())
}

/// Rename the interface at `idx`, which must be down, and return its new name: a `%d` in `name`
/// takes the first number free in this namespace.
pub fn rename_index(idx: u32, name: &str) -> io::Result<String> {
check_name(name)?;
let mut m = Msg::new(RTM_NEWLINK, 0, 1);
m.ifinfomsg(libc::AF_UNSPEC as u8, idx as i32, 0, 0);
m.attr_str(IFLA_IFNAME, name);
transact(m.finish(), &format!("rename interface {idx} to {name:?}"))?;
name_of(idx)
}

/// The name of the interface at `idx` in this namespace.
pub fn name_of(idx: u32) -> io::Result<String> {
let mut buf = [0 as libc::c_char; libc::IF_NAMESIZE];
// SAFETY: if_indextoname writes at most IF_NAMESIZE bytes, its NUL included.
if unsafe { libc::if_indextoname(idx, buf.as_mut_ptr()) }.is_null() {
return Err(io::Error::last_os_error());
}
Ok(unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }.to_string_lossy().into_owned())
}

/// Create a veth pair `a` <-> `b`, `b` born in namespace `peer_ns` and with MAC `peer_mac` when given.
pub fn create_veth(a: &str, b: &str, peer_ns: Option<RawFd>, peer_mac: Option<[u8; 6]>) -> io::Result<()> {
check_name(a)?;
Expand Down Expand Up @@ -377,6 +424,28 @@ pub fn set_wiphy_netns(phy: u32, ns_fd: RawFd) -> io::Result<()> {
transact_on(NETLINK_GENERIC, m.finish(), &format!("move wiphy {phy} into namespace")).map(|_| ())
}

/// `iw phy <phy> interface add <name> type managed`: a station netdev on a wiphy, `%d` in
/// `name` taking the first free number.
pub fn new_station(phy: u32, name: &str) -> io::Result<()> {
check_name(name)?;
let family = genl_family_id("nl80211")?;
let mut m = Msg::new(family, 0, 1);
m.genlmsghdr(NL80211_CMD_NEW_INTERFACE, 0);
m.attr_u32(NL80211_ATTR_WIPHY, phy);
m.attr_str(NL80211_ATTR_IFNAME, name);
m.attr_u32(NL80211_ATTR_IFTYPE, NL80211_IFTYPE_STATION);
transact_on(NETLINK_GENERIC, m.finish(), &format!("add a station to wiphy {phy}")).map(|_| ())
}

/// `iw dev <dev> del`: remove a radio's netdev, by index.
pub fn del_interface(idx: u32) -> io::Result<()> {
let family = genl_family_id("nl80211")?;
let mut m = Msg::new(family, 0, 1);
m.genlmsghdr(NL80211_CMD_DEL_INTERFACE, 0);
m.attr_u32(NL80211_ATTR_IFINDEX, idx);
transact_on(NETLINK_GENERIC, m.finish(), &format!("delete wireless interface {idx}")).map(|_| ())
}

pub fn add_addr4(dev: &str, addr: [u8; 4], prefix: u8) -> io::Result<()> {
let idx = index_of(dev)?;
let mut m = Msg::new(RTM_NEWADDR, NLM_F_CREATE | NLM_F_EXCL, 1);
Expand Down Expand Up @@ -439,7 +508,6 @@ pub fn is_up(dev: &str) -> io::Result<bool> {
}

/// `dev`'s MAC.
#[cfg(test)]
pub fn mac_of(dev: &str) -> io::Result<[u8; 6]> {
let c = CString::new(dev).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL"))?;
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
Expand All @@ -464,6 +532,145 @@ pub fn mac_of(dev: &str) -> io::Result<[u8; 6]> {
Ok(mac)
}

/// Set `dev`'s address and MTU, which needs it down, and drop its alias.
pub fn set_link(dev: &str, mac: Option<[u8; 6]>, mtu: Option<u32>, drop_alias: bool) -> io::Result<()> {
let idx = index_of(dev)?;
let mut m = Msg::new(RTM_NEWLINK, 0, 1);
m.ifinfomsg(libc::AF_UNSPEC as u8, idx as i32, 0, 0);
if let Some(a) = mac {
m.attr(IFLA_ADDRESS, &a);
}
if let Some(n) = mtu {
m.attr_u32(IFLA_MTU, n);
}
if drop_alias {
m.attr(IFLA_IFALIAS, &[]);
}
transact(m.finish(), &format!("set the address and MTU of {dev:?}"))
}

/// The (type, data) attributes in `buf` from `off`.
fn attrs(buf: &[u8], mut off: usize) -> Vec<(u16, &[u8])> {
let mut out = Vec::new();
while off + 4 <= buf.len() {
let len = u16::from_ne_bytes(buf[off..off + 2].try_into().unwrap()) as usize;
let kind = u16::from_ne_bytes(buf[off + 2..off + 4].try_into().unwrap()) & NLA_TYPE_MASK;
if len < 4 || off + len > buf.len() {
break;
}
out.push((kind, &buf[off + 4..off + len]));
off += align4(len);
}
out
}

/// `dev`'s altnames, which answer to a lookup as its name does, and whether it has an alias.
pub fn names_left(dev: &str) -> io::Result<(Vec<Vec<u8>>, bool)> {
let idx = index_of(dev)?;
let mut m = Msg::new(RTM_GETLINK, 0, 1);
m.ifinfomsg(libc::AF_UNSPEC as u8, idx as i32, 0, 0);
let reply = transact_on(NETLINK_ROUTE, m.finish(), &format!("read {dev:?}"))?;
let (mut alt, mut alias) = (Vec::new(), false);
// An ifinfomsg, 16 bytes, then the link's attributes.
for (kind, data) in attrs(&reply, 16) {
if kind == IFLA_PROP_LIST {
for (k, name) in attrs(data, 0) {
if k == IFLA_ALT_IFNAME {
alt.push(name.split(|b| *b == 0).next().unwrap_or(name).to_vec());
}
}
} else if kind == IFLA_IFALIAS {
alias = data.first().is_some_and(|b| *b != 0);
}
}
Ok((alt, alias))
}

/// Remove altnames from `dev`, 64 to a request: a property list's length is 16 bits, and one
/// altname can take 132 bytes of it.
pub fn del_altnames(dev: &str, names: &[Vec<u8>]) -> io::Result<()> {
let idx = index_of(dev)?;
for some in names.chunks(64) {
let mut m = Msg::new(RTM_DELLINKPROP, 0, 1);
m.ifinfomsg(libc::AF_UNSPEC as u8, idx as i32, 0, 0);
let list = m.begin_nested(IFLA_PROP_LIST);
for n in some {
m.attr(IFLA_ALT_IFNAME, &[n.as_slice(), b"\0"].concat());
}
m.end_nested(list);
transact(m.finish(), &format!("remove the altnames of {dev:?}"))?;
}
Ok(())
}

const SIOCETHTOOL: libc::c_ulong = 0x8946;
const ETHTOOL_GWOL: u32 = 5;
const ETHTOOL_SWOL: u32 = 6;
const ETHTOOL_GPERMADDR: u32 = 0x20;

// As linux/ethtool.h lays them out: the kernel reads fields this code never does.
#[allow(dead_code)]
#[repr(C)]
struct WolInfo {
cmd: u32,
supported: u32,
wolopts: u32,
sopass: [u8; 6],
}

#[allow(dead_code)]
#[repr(C)]
struct PermAddr {
cmd: u32,
size: u32,
data: [u8; 32],
}

/// One `SIOCETHTOOL` request on `dev`, whose answer the kernel writes back into `req`.
fn ethtool<T>(dev: &str, req: &mut T) -> io::Result<()> {
let c = CString::new(dev).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL"))?;
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
if sock < 0 {
return Err(io::Error::last_os_error());
}
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
for (i, b) in c.as_bytes_with_nul().iter().take(libc::IFNAMSIZ).enumerate() {
ifr.ifr_name[i] = *b as libc::c_char;
}
ifr.ifr_ifru.ifru_data = req as *mut T as *mut libc::c_char;
// SAFETY: the kernel reads and writes `req`, which outlives the call, as the command says.
let r = unsafe { libc::ioctl(sock, SIOCETHTOOL as _, &mut ifr) };
let e = io::Error::last_os_error();
unsafe { libc::close(sock) };
if r < 0 {
return Err(e);
}
Ok(())
}

/// The address the hardware came with, if its driver gives one.
pub fn perm_mac_of(dev: &str) -> io::Result<Option<[u8; 6]>> {
let mut p = PermAddr { cmd: ETHTOOL_GPERMADDR, size: 32, data: [0; 32] };
ethtool(dev, &mut p)?;
let mac: [u8; 6] = p.data[..6].try_into().unwrap();
Ok((p.size == 6 && mac != [0; 6]).then_some(mac))
}

/// Turn Wake-on-LAN off; false when it was off already or the driver has none.
pub fn wol_off(dev: &str) -> io::Result<bool> {
let mut w = WolInfo { cmd: ETHTOOL_GWOL, supported: 0, wolopts: 0, sopass: [0; 6] };
match ethtool(dev, &mut w) {
Err(e) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => return Ok(false),
r => r?,
}
if w.wolopts == 0 {
return Ok(false);
}
let mut off = WolInfo { cmd: ETHTOOL_SWOL, supported: 0, wolopts: 0, sopass: [0; 6] };
ethtool(dev, &mut off)?;
Ok(true)
}

/// Open a handle on a process's network namespace.
pub fn open_netns_of(pid: libc::pid_t) -> io::Result<RawFd> {
let p = CString::new(format!("/proc/{pid}/ns/net")).unwrap();
Expand Down
Loading
Loading