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
25 changes: 25 additions & 0 deletions build/guest-tests/zones-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,31 @@ 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.
ninit="$(cut -d' ' -f1 /run/kryptik/zones/net/init.pid 2>/dev/null)"
named="$(nsenter -t "${ninit:-0}" -n sh -c 'ip link set dev eth0 down && ip link set dev eth0 name "-x\"y" && echo NAMED' 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
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
# 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
21 changes: 21 additions & 0 deletions compartments/kryptikd/src/netlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,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
26 changes: 26 additions & 0 deletions compartments/kryptikd/src/netlink/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,29 @@ fn isolated_ports_block_zone_to_zone() {
other => panic!("bridge isolation test failed at step {other}"),
}
}

/// A name a net zone could leave on a NIC is replaced by index, the kernel numbering `nic%d`.
#[test]
fn rename_by_index_takes_a_template() {
let rc = in_userns_netns(|| {
let r: Result<(), i32> = (|| {
step(1, create_veth("x\"y", "pb", None, None))?;
let idx = index_of("x\"y").map_err(|_| 2)?;
let now = rename_index(idx, "nic%d").map_err(|e| {
eprintln!("rename_index: {e}");
3
})?;
if now != "nic0" || index_of("x\"y").is_ok() || index_of("nic0").ok() != Some(idx) {
eprintln!("renamed {now:?}");
return Err(4);
}
Ok(())
})();
r.err().unwrap_or(0)
});
match rc {
0 => {}
77 => eprintln!("no unprivileged user namespace; skipping"),
other => panic!("rename test failed at step {other}"),
}
}
49 changes: 40 additions & 9 deletions compartments/kryptikd/src/netzone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
//! Runs in the root parent while the zone waits at its handshake; an unprivileged launch gets
//! loopback only. NAT, forwarding policy and the resolver belong to tools/net/netzone-init.sh.

use std::ffi::CStr;
use std::ffi::{CStr, OsString};
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
use std::path::Path;

Expand Down Expand Up @@ -221,22 +222,52 @@ fn carry_nic(nic: &str, zone_ns: i32) -> Result<Uplink, NetError> {
}

/// Interfaces on a bus device (`/sys/class/net/<n>/device`): what `[network] nic = "*"` moves.
///
/// A NIC comes back to zone 0 under whatever name the net zone that held it gave it, and the
/// next zone's script, nft set and dhcpcd would take that name as it is: one that is not plain
/// becomes `nic<N>` first. It came back down, as a rename needs.
pub fn physical_interfaces() -> io::Result<Vec<String>> {
physical_interfaces_under(Path::new("/sys/class/net"))
let mut out = Vec::new();
for (name, idx) in physical_interfaces_under(Path::new("/sys/class/net"))? {
if plain_name(name.as_bytes()) {
out.push(name.to_string_lossy().into_owned());
continue;
}
match netlink::rename_index(idx, "nic%d") {
Ok(now) => {
eprintln!("kryptikd: interface {name:?} renamed {now:?} before it moves");
out.push(now);
}
Err(e) => eprintln!("kryptikd: interface {name:?} left in zone 0: {e}"),
}
}
out.sort();
Ok(out)
}

/// `physical_interfaces` over any sysfs `class/net` directory, for tests.
fn physical_interfaces_under(class_net: &Path) -> io::Result<Vec<String>> {
/// A name as the kernel and eudev give one: a lower-case letter, then lower-case letters and
/// digits, so no option, quote, glob or control byte. Not `BRIDGE`, which the start makes in
/// the zone after the NICs move: a NIC by that name would fail it.
fn plain_name(name: &[u8]) -> bool {
name.len() < 16
&& name != BRIDGE.as_bytes()
&& name.first().is_some_and(|b| b.is_ascii_lowercase())
&& name.iter().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
}

/// Each physical interface's name, as the kernel holds it, and index, under any sysfs
/// `class/net` directory, for tests.
fn physical_interfaces_under(class_net: &Path) -> io::Result<Vec<(OsString, u32)>> {
let mut out = Vec::new();
for e in std::fs::read_dir(class_net)? {
let e = e?;
let name = e.file_name().to_string_lossy().into_owned();
if name == "lo" {
let name = e.file_name();
if name == "lo" || !e.path().join("device").exists() {
continue;
}
if e.path().join("device").exists() {
out.push(name);
}
let idx = std::fs::read_to_string(e.path().join("ifindex"))?.trim().parse::<u32>();
let idx = idx.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "a sysfs ifindex is not a number"))?;
out.push((name, idx));
}
out.sort();
Ok(out)
Expand Down
13 changes: 13 additions & 0 deletions compartments/kryptikd/src/netzone/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,16 @@ fn replumb_empty_directory_attempts_nothing() {
assert!(replumb_routed_zones(&dir, -1).is_empty());
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn only_plain_names_pass_on() {
for ok in ["eth0", "wlan0", "enp0s31f6", "wlp2s0", "enx001122334455"] {
assert!(plain_name(ok.as_bytes()), "{ok}");
}
// What a net zone could leave behind: an option, a quote that ends nft's string, an escape,
// the bridge's own name.
for bad in ["-k", "x\"y", "x\",\"kryptik0", "e\u{1b}th0", "kryptik0", "Eth0", "eth0.1", "0eth", "", "abcdefghijklmnop"] {
assert!(!plain_name(bad.as_bytes()), "{bad:?}");
}
assert!(!plain_name(b"\xff"));
}
15 changes: 12 additions & 3 deletions docs/design/net-zone.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,14 @@ as `SIGSYS` in the zone's log and a `wifi=connecting` that never changes.
started, so one started before any net zone resolves names once attached;
kryptikd never edits a running zone's sealed root.
- The kernel returns the physical interface to the initial namespace, down
and unaddressed, under its own name (`dev<N>` only if zone 0 has an
interface by that name by then); kryptikd leaves it so until the next net
zone start, which takes it again whatever its name.
and unaddressed, under the name it had there (`dev<N>` only if zone 0 has
an interface by that name by then); kryptikd leaves it so until the next
net zone start, which takes it again. That name was the net zone's to
choose, anything but `/`, `:` and whitespace, so the start first renames
one that is not plain (a lower-case letter, then lower-case letters and
digits, and not `kryptik0`, the bridge it makes there) `nic<N>`: no option,
quote or escape a past net zone chose reaches the next one's script, nft set
or dhcpcd, and no name of its own stops it.

## What this guarantees

Expand Down Expand Up @@ -332,6 +337,10 @@ as `SIGSYS` in the zone's log and a `wifi=connecting` that never changes.
(`netlink::tests::wireless_moves_by_wiphy`).
- `wifi.rs` unit tests and the serve and cli suites cover the credentials
file and `kryptik wifi`.
- The zones suite has the net zone rename its NIC `-x"y` and restarts it: the
NIC is back in zone 0 under that name, and the next start comes up `READY`
with it as `nic<N>`. `netlink::tests::rename_by_index_takes_a_template`
renames a veth by its index to `nic%d`.
- `tools/tests/netzone-uplink.sh`: the zones a definition lets through, by the
address kryptikd derives for each, every host's addresses pinned to its own
MAC and the MAC the same as `netlink::zone_mac`, the gateway sets as nft is
Expand Down
1 change: 1 addition & 0 deletions tools/image/zones-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ grep 'ZT FAIL' <<<"$T2" | sed 's/^/ /'
# The key verdicts one by one, so a pass is not a single line.
for name in kernel-support policies net-ready net-dns dhcpcd-separated dnsmasq-unprivileged zone0-nic zone0-no-route zone0-offline routed-egress routed-ping routed-ping6 routed-dns net-lease-names-resolver dns-follows-lease dns-after-reload routed-ipv6-noglobal zone-separation volume-hidden home-hidden fail-closed net-restart-ready reattach-after-restart uplink-returned uplink-retaken reattach-egress uplink-refused wifi-beyond routed-restart-path uplink-address-refused zone-flows-capped \
wifi-module wifi-ap wifi-add wifi-associated wifi-lease wifi-egress wifi-forget \
uplink-renamed-plain \
time-floor-ran time-clamp time-floor-forged time-claim-stepped time-claim-floor time-claim-consent pids-limit ephemeral-size-bound cpu-max-set lifecycle-repeat lifecycle-registry resolver-after-attach dns-cache-off dns-counters-hidden net-zone-sysfs-nics-only \
terminal-terminfo man-page text-browser tls-trust \
volume-init zone-source-pinned bridge-ports-closed encrypted-zone-start stop-closes-volume wrong-passphrase persist-reopen no-mapping-after ephemeral-gone concurrent-start-refused full-volume header-restore volume-destroy vault-offline vault-ping no-passphrase-leak \
Expand Down
2 changes: 1 addition & 1 deletion tools/net/netzone-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ wifi_last="$(wifi_state)"
# --- uplink: DHCP if anyone answers, else what zone 0 carried over ---------
uplink_addr() { # the first IPv4 address any uplink holds
for n in "$@"; do
a="$(ip -4 -o addr show "$n" 2>/dev/null | awk '{print $4}' | head -1)"
a="$(ip -4 -o addr show dev "$n" 2>/dev/null | awk '{print $4}' | head -1)"
[ -n "$a" ] && { echo "$a"; return; }
done
}
Expand Down
Loading