Skip to content

Commit 6ea2d84

Browse files
committed
fix(packaging): allow dnsmasq to create SNP DHCP lease files
1 parent 2e3d17a commit 6ea2d84

6 files changed

Lines changed: 253 additions & 0 deletions

File tree

.github/workflows/build-deb-package-and-integration-tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ jobs:
4747
- name: Initialize git submodules
4848
run: git submodule init
4949

50+
- name: Test dnsmasq AppArmor package integration
51+
run: packaging/tests/test_dnsmasq_apparmor.sh
52+
5053
# Runner image 20260726.254 ships podman 5.8.4 but leaves a stale crun 1.14.1
5154
# at /usr/bin/crun, breaking every podman run/build with "unknown version
5255
# specified". Point podman at the current crun in /usr/local/bin.
@@ -69,6 +72,8 @@ jobs:
6972
dpkg --contents packaging/target/${{ matrix.artifact_name }} | grep /opt/aleph-vm/bin/supervisor-launcher
7073
dpkg --contents packaging/target/${{ matrix.artifact_name }} | grep /opt/aleph-vm/bin/aleph-vm-controller
7174
dpkg --contents packaging/target/${{ matrix.artifact_name }} | grep /opt/aleph-vm/bin/controller-launcher
75+
dpkg --contents packaging/target/${{ matrix.artifact_name }} | grep /usr/lib/aleph-vm/configure-dnsmasq-apparmor
76+
dpkg --contents packaging/target/${{ matrix.artifact_name }} | grep /usr/share/aleph-vm/apparmor/usr.sbin.dnsmasq
7277
7378
- uses: actions/upload-artifact@v4
7479
with:

packaging/aleph-vm/DEBIAN/postinst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ if ! id -u jailman > /dev/null 2>&1; then
55
useradd jailman
66
fi
77

8+
# The distro dnsmasq profile supports site-local additions. Keep our rule in a
9+
# package-owned fragment and add only its include to the administrator-owned
10+
# local file.
11+
/usr/lib/aleph-vm/configure-dnsmasq-apparmor install
12+
813
rm -fr /srv/jailer # Upgrade from < 0.1.11
914
rm -fr /tmp/aleph # Upgrade from < 0.1.11
1015
mkdir -p /var/lib/aleph/vm/jailer

packaging/aleph-vm/DEBIAN/prerm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ for unit in aleph-vm-agent.service aleph-vm-supervisor.service; do
66
systemctl stop "$unit" || true
77
systemctl reset-failed "$unit" 2>/dev/null || true
88
done
9+
10+
# On removal, drop only aleph-vm's managed include after stopping its services
11+
# but while the package-owned policy fragment is still present. Keep it across
12+
# upgrades; postinst refreshes the include and reloads the new policy.
13+
if [ "${1:-}" = "remove" ] && [ -x /usr/lib/aleph-vm/configure-dnsmasq-apparmor ]; then
14+
/usr/lib/aleph-vm/configure-dnsmasq-apparmor remove || true
15+
fi
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/bin/bash
2+
set -eu
3+
umask 022
4+
5+
APPARMOR_DIR="${ALEPH_VM_APPARMOR_DIR:-/etc/apparmor.d}"
6+
POLICY_FILE="${ALEPH_VM_DNSMASQ_APPARMOR_POLICY:-/usr/share/aleph-vm/apparmor/usr.sbin.dnsmasq}"
7+
PROFILE="$APPARMOR_DIR/usr.sbin.dnsmasq"
8+
LOCAL="$APPARMOR_DIR/local/usr.sbin.dnsmasq"
9+
BEGIN_MARKER="# BEGIN aleph-vm managed dnsmasq AppArmor rules"
10+
INCLUDE_LINE="include if exists \"$POLICY_FILE\""
11+
END_MARKER="# END aleph-vm managed dnsmasq AppArmor rules"
12+
13+
warn() {
14+
echo "aleph-vm: $*" >&2
15+
}
16+
17+
reload_profile() {
18+
[ -f "$PROFILE" ] || return 0
19+
command -v aa-enabled >/dev/null 2>&1 || return 0
20+
command -v apparmor_parser >/dev/null 2>&1 || return 0
21+
aa-enabled --quiet 2>/dev/null || return 0
22+
23+
if ! apparmor_parser -r -W -T "$PROFILE"; then
24+
# Match dh_apparmor behavior: a transient or pre-existing parser failure
25+
# must not leave the application package unconfigured.
26+
warn "could not reload the dnsmasq AppArmor profile"
27+
fi
28+
}
29+
30+
profile_includes_local_file() {
31+
grep -Eq \
32+
'^[[:space:]]*(#include|include)([[:space:]]+if[[:space:]]+exists)?[[:space:]]+<local/usr\.sbin\.dnsmasq>[[:space:]]*$' \
33+
"$PROFILE"
34+
}
35+
36+
resolve_local_file() {
37+
if [ -L "$LOCAL" ]; then
38+
readlink -f "$LOCAL" 2>/dev/null || true
39+
else
40+
printf '%s\n' "$LOCAL"
41+
fi
42+
}
43+
44+
rewrite_local_file() {
45+
action="$1"
46+
target="$(resolve_local_file)"
47+
48+
if [ -z "$target" ]; then
49+
warn "leaving dangling symlink untouched: $LOCAL"
50+
return 0
51+
fi
52+
if [ -e "$target" ] && [ ! -f "$target" ]; then
53+
warn "leaving non-regular local profile untouched: $LOCAL"
54+
return 0
55+
fi
56+
57+
mkdir -p "$(dirname "$target")"
58+
tmp="$(mktemp "$(dirname "$target")/.aleph-vm-dnsmasq.XXXXXX")"
59+
content_tmp=""
60+
trap 'rm -f "$tmp" ${content_tmp:+"$content_tmp"}' RETURN
61+
62+
if [ -f "$target" ]; then
63+
# Package targets use GNU coreutils, where --preserve=all retains owner,
64+
# mode, ACLs and extended attributes. The fallback keeps this fixture
65+
# runnable on non-GNU development hosts.
66+
cp --preserve=all -- "$target" "$tmp" 2>/dev/null || cp -p "$target" "$tmp"
67+
content_tmp="$(mktemp "$(dirname "$target")/.aleph-vm-dnsmasq-content.XXXXXX")"
68+
# Remove only lines owned by this helper. Any administrator additions,
69+
# including additions placed between our markers, remain intact.
70+
awk -v begin="$BEGIN_MARKER" -v include="$INCLUDE_LINE" -v end="$END_MARKER" \
71+
'$0 != begin && $0 != include && $0 != end { print }' \
72+
"$target" > "$content_tmp"
73+
cat "$content_tmp" > "$tmp"
74+
rm -f "$content_tmp"
75+
content_tmp=""
76+
else
77+
: > "$tmp"
78+
chmod 0644 "$tmp"
79+
fi
80+
81+
if [ "$action" = install ]; then
82+
if [ -s "$tmp" ] && [ -n "$(tail -n 1 "$tmp")" ]; then
83+
printf '\n' >> "$tmp"
84+
fi
85+
printf '%s\n%s\n%s\n' \
86+
"$BEGIN_MARKER" "$INCLUDE_LINE" "$END_MARKER" >> "$tmp"
87+
fi
88+
89+
mv -f "$tmp" "$target"
90+
trap - RETURN
91+
}
92+
93+
case "${1:-}" in
94+
install)
95+
if [ ! -f "$POLICY_FILE" ]; then
96+
warn "AppArmor policy fragment is missing: $POLICY_FILE"
97+
exit 0
98+
fi
99+
if [ -f "$PROFILE" ] && ! profile_includes_local_file; then
100+
warn "dnsmasq AppArmor profile does not include <local/usr.sbin.dnsmasq>; policy fragment was not enabled"
101+
exit 0
102+
fi
103+
rewrite_local_file install
104+
reload_profile
105+
;;
106+
remove)
107+
if [ -e "$LOCAL" ] || [ -L "$LOCAL" ]; then
108+
rewrite_local_file remove
109+
reload_profile
110+
fi
111+
;;
112+
*)
113+
echo "Usage: $0 {install|remove}" >&2
114+
exit 2
115+
;;
116+
esac
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# The SNP supervisor starts one dnsmasq process per VM and keeps its DHCP
2+
# leases here. This file is included from dnsmasq's site-local profile.
3+
/var/lib/aleph/vm/dhcp/*.leases rw,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
5+
HELPER="$REPO_ROOT/packaging/aleph-vm/usr/lib/aleph-vm/configure-dnsmasq-apparmor"
6+
POLICY_SOURCE="$REPO_ROOT/packaging/aleph-vm/usr/share/aleph-vm/apparmor/usr.sbin.dnsmasq"
7+
TMP_ROOT="$(mktemp -d)"
8+
trap 'rm -rf "$TMP_ROOT"' EXIT
9+
10+
APPARMOR_DIR="$TMP_ROOT/etc/apparmor.d"
11+
POLICY="$TMP_ROOT/usr/share/aleph-vm/apparmor/usr.sbin.dnsmasq"
12+
LOCAL="$APPARMOR_DIR/local/usr.sbin.dnsmasq"
13+
PARSER_LOG="$TMP_ROOT/apparmor-parser.log"
14+
FAKE_BIN="$TMP_ROOT/bin"
15+
16+
mkdir -p "$(dirname "$POLICY")" "$APPARMOR_DIR/local" "$FAKE_BIN"
17+
cp "$POLICY_SOURCE" "$POLICY"
18+
19+
cat > "$FAKE_BIN/aa-enabled" <<'EOF'
20+
#!/bin/sh
21+
exit "${AA_ENABLED_EXIT:-0}"
22+
EOF
23+
cat > "$FAKE_BIN/apparmor_parser" <<'EOF'
24+
#!/bin/sh
25+
printf '%s\n' "$*" >> "$PARSER_LOG"
26+
exit "${APPARMOR_PARSER_EXIT:-0}"
27+
EOF
28+
chmod +x "$FAKE_BIN/aa-enabled" "$FAKE_BIN/apparmor_parser"
29+
30+
run_helper() {
31+
PATH="$FAKE_BIN:$PATH" \
32+
ALEPH_VM_APPARMOR_DIR="$APPARMOR_DIR" \
33+
ALEPH_VM_DNSMASQ_APPARMOR_POLICY="$POLICY" \
34+
PARSER_LOG="$PARSER_LOG" \
35+
"$HELPER" "$@"
36+
}
37+
38+
assert_count() {
39+
expected="$1"
40+
pattern="$2"
41+
file="$3"
42+
actual="$(grep -Fxc "$pattern" "$file" || true)"
43+
[ "$actual" = "$expected" ] || {
44+
echo "expected $expected copies of '$pattern' in $file, got $actual" >&2
45+
exit 1
46+
}
47+
}
48+
49+
cat > "$LOCAL" <<'EOF'
50+
# Administrator-owned dnsmasq additions
51+
/srv/operator/dnsmasq/** r,
52+
EOF
53+
chmod 0640 "$LOCAL"
54+
cat > "$APPARMOR_DIR/usr.sbin.dnsmasq" <<'EOF'
55+
/usr/sbin/dnsmasq {
56+
include if exists <local/usr.sbin.dnsmasq>
57+
}
58+
EOF
59+
60+
run_helper install
61+
cp "$LOCAL" "$TMP_ROOT/local-after-first-install"
62+
run_helper install
63+
cmp "$LOCAL" "$TMP_ROOT/local-after-first-install"
64+
65+
grep -Fqx '/srv/operator/dnsmasq/** r,' "$LOCAL"
66+
[ "$(stat -f '%Lp' "$LOCAL" 2>/dev/null || stat -c '%a' "$LOCAL")" = 640 ]
67+
assert_count 1 '# BEGIN aleph-vm managed dnsmasq AppArmor rules' "$LOCAL"
68+
assert_count 1 "include if exists \"$POLICY\"" "$LOCAL"
69+
assert_count 1 '# END aleph-vm managed dnsmasq AppArmor rules' "$LOCAL"
70+
[ "$(wc -l < "$PARSER_LOG" | tr -d ' ')" = 2 ]
71+
grep -Fqx -- "-r -W -T $APPARMOR_DIR/usr.sbin.dnsmasq" "$PARSER_LOG"
72+
73+
run_helper remove
74+
grep -Fqx '/srv/operator/dnsmasq/** r,' "$LOCAL"
75+
if grep -Fq 'aleph-vm managed' "$LOCAL" || grep -Fq "$POLICY" "$LOCAL"; then
76+
echo "managed AppArmor lines remain after removal" >&2
77+
exit 1
78+
fi
79+
[ "$(wc -l < "$PARSER_LOG" | tr -d ' ')" = 3 ]
80+
81+
# A custom distro profile without the local hook cannot activate our fragment.
82+
# Leave the administrator file untouched, skip reload and emit a clear warning.
83+
cp "$LOCAL" "$TMP_ROOT/local-before-missing-hook"
84+
cat > "$APPARMOR_DIR/usr.sbin.dnsmasq" <<'EOF'
85+
/usr/sbin/dnsmasq {
86+
}
87+
EOF
88+
rm -f "$PARSER_LOG"
89+
missing_hook_stderr="$TMP_ROOT/missing-hook.stderr"
90+
run_helper install 2> "$missing_hook_stderr"
91+
cmp "$LOCAL" "$TMP_ROOT/local-before-missing-hook"
92+
[ ! -e "$PARSER_LOG" ]
93+
grep -Fq 'does not include <local/usr.sbin.dnsmasq>' "$missing_hook_stderr"
94+
95+
# Installing without a distro profile prepares the local include for a future
96+
# AppArmor installation but does not attempt a reload.
97+
rm -f "$APPARMOR_DIR/usr.sbin.dnsmasq" "$PARSER_LOG"
98+
run_helper install
99+
assert_count 1 "include if exists \"$POLICY\"" "$LOCAL"
100+
[ ! -e "$PARSER_LOG" ]
101+
102+
# Disabled AppArmor also leaves the package install successful and skips reload.
103+
cat > "$APPARMOR_DIR/usr.sbin.dnsmasq" <<'EOF'
104+
/usr/sbin/dnsmasq {
105+
#include if exists <local/usr.sbin.dnsmasq>
106+
}
107+
EOF
108+
AA_ENABLED_EXIT=1 run_helper install
109+
[ ! -e "$PARSER_LOG" ]
110+
111+
# A parser failure is reported but follows dh_apparmor's nonfatal behavior.
112+
APPARMOR_PARSER_EXIT=1 run_helper install 2> "$TMP_ROOT/parser-warning"
113+
grep -Fq 'could not reload the dnsmasq AppArmor profile' "$TMP_ROOT/parser-warning"
114+
115+
grep -Fqx '/var/lib/aleph/vm/dhcp/*.leases rw,' "$POLICY_SOURCE"
116+
117+
echo "dnsmasq AppArmor packaging tests passed"

0 commit comments

Comments
 (0)