diff --git a/build/services/net-zone/run b/build/services/net-zone/run index 1363a848..a6154d4d 100755 --- a/build/services/net-zone/run +++ b/build/services/net-zone/run @@ -17,5 +17,12 @@ if ! have_nic; then sleep 3 echo "net-zone: an interface has appeared" fi +# --ready-fd: nobody waits to hear that the zone is up, but with it the launcher +# keeps this service's log and hands the zone a pipe. Each line the zone +# writes is then marked as its own and cannot pass for the launcher's or a +# zone 0 service's, its control bytes are replaced, and a megabyte a start is +# the most it logs (spawn.rs, ZoneOutput). A zone that says more goes unlogged +# until its next start; filling the allowance again by the hour is the way on +# if that proves too little. exec /usr/bin/kryptikd run net --zones /usr/lib/kryptik/zones --rootfs /var/lib/kryptik/zones \ - -- /usr/libexec/kryptik/netzone-init.sh + --ready-fd 3 -- /usr/libexec/kryptik/netzone-init.sh 3>/dev/null diff --git a/compartments/kryptikd/src/spawn.rs b/compartments/kryptikd/src/spawn.rs index f210edb7..9fc85040 100644 --- a/compartments/kryptikd/src/spawn.rs +++ b/compartments/kryptikd/src/spawn.rs @@ -398,8 +398,11 @@ fn serve_until_exit(pid: libc::pid_t, listen_fd: RawFd, s: &broker::Served, out: let pfd = pfds[0]; if n > 0 && pfd.revents & libc::POLLIN != 0 { match broker::serve_one(listen_fd, s) { + /* Not the calls a net zone makes by the minute: they would use + * up the log's bound in days, and what they decide is logged + * where it is decided. */ + Ok(Some("update-poll" | "update-put" | "version")) | Ok(None) => {} Ok(Some(verb)) => log(&format!("kryptikd[zone {zone}]: broker request: {verb}")), - Ok(None) => {} Err(e) => log(&format!("kryptikd[zone {zone}]: broker: {e}")), } } diff --git a/docs/design/net-zone.md b/docs/design/net-zone.md index 937a5191..8dc351ad 100755 --- a/docs/design/net-zone.md +++ b/docs/design/net-zone.md @@ -109,6 +109,12 @@ netzone: READY uplink= nat=yes dns= wifi= (forwarding off) ``` + The zone writes to a pipe, never to the log: its launcher marks each line + `zone net| ` in the catch-all log, replaces control bytes, and logs at most + a megabyte a start. A line without the mark is the launcher's or a zone 0 + service's, whatever it says. The zone prints words from the network (an + SSID, a server's refusal) as they came, never as escapes. + ## The uplinks' own networks A zone needs the gateway only as a next hop, and its names go through the diff --git a/tools/net/netzone-init.sh b/tools/net/netzone-init.sh index c77a6984..77358c20 100755 --- a/tools/net/netzone-init.sh +++ b/tools/net/netzone-init.sh @@ -8,7 +8,9 @@ # time= ... # netzone: NOT READY (forwarding is off) set -u -say() { echo "netzone: $*"; } +# printf, not echo: a word from the network (an SSID, a server's reason for a +# refusal) is printed as it came, and dash's echo would act on its backslashes. +say() { printf 'netzone: %s\n' "$*"; } BR=kryptik0 STATUS=/run/netzone-status WPA_CONF=/etc/wpa_supplicant.conf @@ -326,14 +328,17 @@ UPDATE_CONF=/etc/kryptik/update.conf UPDATE_FETCH="${KRYPTIK_UPDATE_FETCH:-/usr/libexec/kryptik/update-fetch.py}" UPDATE_BROUGHT=/run/kryptik-update-statement-brought UPDATE_PID="" -update_run() { # update_run latest|poll: in the background, one at a time +update_run() { # update_run latest|poll: in the background, one at a time; 1 while the last one runs { command -v python3 >/dev/null 2>&1 && [ -r "$UPDATE_FETCH" ] && [ -r "$UPDATE_CONF" ]; } || return 0 - [ -n "$UPDATE_PID" ] && kill -0 "$UPDATE_PID" 2>/dev/null && return 0 + [ -n "$UPDATE_PID" ] && kill -0 "$UPDATE_PID" 2>/dev/null && return 1 ( - out="$(python3 "$UPDATE_FETCH" "$1" --broker "$BROKER" 2>&1 | tail -1)" - case "$1:$out" in - poll:idle|*:) ;; - latest:ok*) : > "$UPDATE_BROUGHT"; say "update: zone 0 on the statement of what is current: ${out}" ;; + out="$(python3 "$UPDATE_FETCH" "$1" --broker "$BROKER" 2>&1)"; rc=$? + out="$(printf '%s\n' "$out" | tail -1)" + # Zone 0's answer counts only from a fetch that ended well: the last + # line of one that failed is the far host's words. + case "$rc:$1:$out" in + 0:poll:idle|*:*:) ;; + 0:latest:ok*) : > "$UPDATE_BROUGHT"; say "update: zone 0 on the statement of what is current: ${out}" ;; *) say "update $1: ${out}" ;; esac ) & @@ -411,7 +416,8 @@ while :; do if [ -n "$(uplink_addr "$@")" ]; then if [ -e "$UPDATE_BROUGHT" ]; then statement_every=8640; else statement_every=180; fi if [ "$statement_ticks" -ge "$statement_every" ]; then - statement_ticks=0; update_run latest + # Asked again at the next pass when a poll was still running. + update_run latest && statement_ticks=0 elif [ "$update_ticks" -ge 6 ]; then update_ticks=0; update_run poll fi diff --git a/tools/net/update-fetch.py b/tools/net/update-fetch.py index affe9f86..f0447c43 100755 --- a/tools/net/update-fetch.py +++ b/tools/net/update-fetch.py @@ -114,7 +114,8 @@ def main(): try: return latest(args) if args.what == "latest" else poll(args) except (OSError, ValueError) as e: # urllib's errors are OSErrors - print("update-fetch: %s" % e, file=sys.stderr) + # One line: the text can hold a host's own words, line ends among them. + print("update-fetch: %s" % " ".join(str(e).split()), file=sys.stderr) return 1 diff --git a/tools/tests/netzone-words.sh b/tools/tests/netzone-words.sh new file mode 100755 index 00000000..ed92c19d --- /dev/null +++ b/tools/tests/netzone-words.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Tests for the words the net zone prints and takes from the network +# (tools/net/netzone-init.sh, say and update_run): a backslash is printed, not +# acted on, and the update helper's last line counts as zone 0's answer only +# when the helper ended well. Offline, under each POSIX shell here. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCRIPT="${ROOT}/tools/net/netzone-init.sh" + +PASS=0; FAIL=0 +green() { printf ' PASS %s\n' "$1"; PASS=$((PASS + 1)); } +red() { printf ' FAIL %s\n' "$1"; FAIL=$((FAIL + 1)); [[ $# -gt 1 ]] && printf ' %s\n' "$2"; } +same() { if [[ "$2" == "$3" ]]; then green "$1"; else red "$1" "got '$(tr '\n' '|' <<<"$2")', want '$(tr '\n' '|' <<<"$3")'"; fi; } +command -v python3 >/dev/null 2>&1 || { echo "python3 is not installed here; cannot run this test"; exit 77; } +T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT + +{ sed -n '/^say() {/p' "$SCRIPT"; sed -n '/^update_run() {/,/^}/p' "$SCRIPT"; } > "$T/functions.sh" +grep -q '^say() {' "$T/functions.sh" && grep -q '^update_run() {' "$T/functions.sh" \ + || { echo "could not find say or update_run in ${SCRIPT#"$ROOT"/} (did their first lines move?)"; exit 1; } +grep -q -- '--ready-fd 3 -- /usr/libexec/kryptik/netzone-init.sh 3>/dev/null' "${ROOT}/build/services/net-zone/run" \ + && green "the service starts the zone on a pipe, so its launcher marks and bounds what it says" \ + || red "build/services/net-zone/run hands the zone the service's own log" + +# The update helper's stand-in: prints what it is told to, ends as it is told to. +cat > "$T/fetch.py" <<'EOF' +import os, sys +sys.stdout.write(os.environ.get("SAYS", "")) +sys.exit(int(os.environ.get("ENDS", "0"))) +EOF +: > "$T/update.conf" +cat > "$T/harness.sh" </dev/null 2>&1 || continue + run() { rm -f "$T/brought"; "$sh" "$T/harness.sh" "$@" 2>&1; } + echo "-- ${sh}" + same "a backslash from the network is printed as it came" \ + "$(run say 'wifi: Caf\x65\nnetzone: READY \033[2J')" 'netzone: wifi: Caf\x65\nnetzone: READY \033[2J' + same "zone 0's ok to a statement is taken, and the statement is not asked for again that day" \ + "$(SAYS='ok current' ENDS=0 run run latest)" "$(printf 'netzone: update: zone 0 on the statement of what is current: ok current\nrc=0\nbrought')" + same "the same words at the end of a fetch that failed are the far host's: said, and not taken" \ + "$(SAYS=$'update-fetch: HTTP Error 302: loop\nok current' ENDS=1 run run latest)" "$(printf 'netzone: update latest: ok current\nrc=0\nnot-brought')" + same "an idle poll says nothing" "$(SAYS='idle' ENDS=0 run run poll)" "$(printf 'rc=0\nnot-brought')" + same "a poll that failed is said, whatever its last line" "$(SAYS='idle' ENDS=1 run run poll)" "$(printf 'netzone: update poll: idle\nrc=0\nnot-brought')" + same "while one runs, the next is not started and says so by its status" "$(run busy)" "rc=1" +done + +echo +echo "${PASS} passed, ${FAIL} failed" +[[ "$FAIL" -eq 0 ]]