Files
NeoTachyon/tests/guest_mode.sh
T

267 lines
9.6 KiB
Bash

#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
NFT_RUNTIME="$ROOT_DIR/tachyon/files/usr/lib/nft/apply.uc"
VALIDATOR_RUNTIME="$ROOT_DIR/tachyon/files/usr/lib/config/validator.uc"
PARENTAL_QUOTA="$ROOT_DIR/tachyon/files/usr/lib/service/parental_quota.uc"
WORK_DIR="$(mktemp -d)"
NFT_LOG="$WORK_DIR/nft.log"
nft_ucode() {
ucode -L "$TACHYON_LIB" "$NFT_RUNTIME" "$@"
}
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
cat "$NFT_LOG" >&2 2>/dev/null || true
exit 1
}
assert_contains() {
local file="$1"
local expected="$2"
local label="$3"
grep -Fq "$expected" "$file" || fail "$label: expected '$expected'"
}
assert_not_contains() {
local file="$1"
local unexpected="$2"
local label="$3"
if grep -Fq "$unexpected" "$file"; then
fail "$label: unexpected '$unexpected' found"
fi
}
mkdir -p "$WORK_DIR/bin"
cat >"$WORK_DIR/bin/nft" <<NFT
#!/usr/bin/env bash
set -eo pipefail
{
printf 'nft'
for arg in "\$@"; do
printf '\t%s' "\$arg"
done
printf '\n'
} >>"$NFT_LOG"
exit 0
NFT
chmod +x "$WORK_DIR/bin/nft"
export PATH="$WORK_DIR/bin:$PATH"
# ─── 1. Selected Mode Test ───────────────────────────────────────────────────
cat >"$WORK_DIR/guest_selected.json" <<'JSON'
{
"guest_mode": [
{
".name": "guest_mode",
"enabled": "1",
"mode": "selected",
"guest_devices": [ "192.168.1.150", "aa:bb:cc:dd:ee:ff" ],
"isolate_lan": "1",
"block_router_admin": "1",
"start_time": "08:00",
"end_time": "22:00",
"days": [ "mon", "tue", "wed", "thu", "fri" ]
}
]
}
JSON
rm -f "$NFT_LOG"
nft_ucode nft-add-guest-mode-rules-fixture "$WORK_DIR/guest_selected.json" "tachyon" "tachyon_ifaces" "localv4" "localv6"
# Router admin drop in guest_input
assert_contains "$NFT_LOG" "guest_input ether saddr @tachyon_guest_mac counter drop comment \"tachyon-guest-admin-drop\"" "selected mac admin drop"
assert_contains "$NFT_LOG" "guest_input ip saddr @tachyon_guest_ip counter drop comment \"tachyon-guest-admin-drop\"" "selected ip admin drop"
# Essential services allowed in guest_input
assert_contains "$NFT_LOG" "guest_input udp dport { 67, 68 } accept" "DHCP accepted"
assert_contains "$NFT_LOG" "guest_input udp dport 53 accept" "DNS UDP accepted"
assert_contains "$NFT_LOG" "guest_input tcp dport 53 accept" "DNS TCP accepted"
assert_contains "$NFT_LOG" "guest_input icmp type echo-request accept" "ICMP echo accepted"
# LAN isolation in guest_forward
assert_contains "$NFT_LOG" "guest_forward ether saddr @tachyon_guest_mac ip daddr @localv4 counter drop comment \"tachyon-guest-lan-drop\"" "mac lan drop"
assert_contains "$NFT_LOG" "guest_forward ip saddr @tachyon_guest_ip ip daddr @localv4 counter drop comment \"tachyon-guest-lan-drop\"" "ip lan drop"
# Byte accounting counters
assert_contains "$NFT_LOG" "guest_forward ether saddr aa:bb:cc:dd:ee:ff counter comment \"tachyon-guest-byte:aa:bb:cc:dd:ee:ff\"" "mac byte counter"
assert_contains "$NFT_LOG" "guest_forward ip saddr 192.168.1.150 counter comment \"tachyon-guest-byte:192.168.1.150\"" "ip byte counter"
# Schedule restriction drops only guest devices outside window
assert_contains "$NFT_LOG" "guest_forward ether saddr @tachyon_guest_mac counter drop comment \"guest-time-window-closed\"" "mac time window drop"
assert_contains "$NFT_LOG" "guest_forward ip saddr @tachyon_guest_ip counter drop comment \"guest-time-window-closed\"" "ip time window drop"
# ─── 2. Inverted Mode Test ───────────────────────────────────────────────────
cat >"$WORK_DIR/guest_inverted.json" <<'JSON'
{
"guest_mode": [
{
".name": "guest_mode",
"enabled": "1",
"mode": "inverted",
"trusted_devices": [ "192.168.1.10", "11:22:33:44:55:66" ],
"isolate_lan": "1",
"block_router_admin": "1",
"start_time": "09:00",
"end_time": "21:00"
}
]
}
JSON
rm -f "$NFT_LOG"
nft_ucode nft-add-guest-mode-rules-fixture "$WORK_DIR/guest_inverted.json" "tachyon" "tachyon_ifaces" "localv4" "localv6"
# Trusted devices return early
assert_contains "$NFT_LOG" "guest_input ether saddr @tachyon_trusted_mac return" "trusted mac returns in input"
assert_contains "$NFT_LOG" "guest_input ip saddr @tachyon_trusted_ip return" "trusted ip returns in input"
assert_contains "$NFT_LOG" "guest_forward ether saddr @tachyon_trusted_mac return" "trusted mac returns in forward"
assert_contains "$NFT_LOG" "guest_forward ip saddr @tachyon_trusted_ip return" "trusted ip returns in forward"
# All non-trusted devices dropped
assert_contains "$NFT_LOG" "guest_input iifname @tachyon_ifaces counter drop comment \"tachyon-guest-admin-drop\"" "inverted admin drop"
assert_contains "$NFT_LOG" "guest_forward iifname @tachyon_ifaces ip daddr @localv4 counter drop comment \"tachyon-guest-lan-drop\"" "inverted lan drop"
assert_contains "$NFT_LOG" "guest_forward iifname @tachyon_ifaces counter comment \"tachyon-guest-traffic\"" "inverted traffic counter"
assert_contains "$NFT_LOG" "guest_forward iifname @tachyon_ifaces counter drop comment \"guest-time-window-closed\"" "inverted time window drop"
# ─── 3. Validator Tests ──────────────────────────────────────────────────────
cat >"$WORK_DIR/valid_config.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"guest_mode": [
{
".name": "guest_mode",
"enabled": "1",
"mode": "selected",
"guest_devices": [ "192.168.1.50", "aa:bb:cc:dd:ee:ff" ],
"daily_time_limit": "120",
"daily_traffic_limit": "1024",
"start_time": "08:00",
"end_time": "20:00"
}
]
}
JSON
if ! ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/valid_config.json" "{}" >/dev/null 2>&1; then
fail "Validator should accept valid guest_mode config"
fi
cat >"$WORK_DIR/invalid_mode.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"guest_mode": [
{
".name": "guest_mode",
"enabled": "1",
"mode": "invalid_mode_name"
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_mode.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject invalid guest mode"
fi
cat >"$WORK_DIR/invalid_device.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"guest_mode": [
{
".name": "guest_mode",
"enabled": "1",
"mode": "selected",
"guest_devices": [ "not-an-ip-or-mac" ]
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_device.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject invalid guest device format"
fi
cat >"$WORK_DIR/invalid_quota.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"guest_mode": [
{
".name": "guest_mode",
"enabled": "1",
"daily_time_limit": "99999"
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_quota.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject out-of-range daily_time_limit"
fi
# ─── 4. Parental Quota Daemon Guest Integration ──────────────────────────────
STATE_FILE="$WORK_DIR/quotas.json"
UCI_STATE="$WORK_DIR/uci_state"
cat >"$UCI_STATE" <<'EOF'
tachyon.guest_mode=guest_mode
tachyon.guest_mode.enabled=1
tachyon.guest_mode.mode=selected
tachyon.guest_mode.guest_devices=192.168.1.88 aa:bb:cc:dd:ee:ff
tachyon.guest_mode.daily_time_limit=60
tachyon.guest_mode.daily_traffic_limit=500
tachyon.guest_mode.notify=0
EOF
# Prepopulate state file with 59 minutes (1 minute below limit)
cat >"$STATE_FILE" <<'EOF'
{"day":"2026-09-08","devices":{},"guest_devices":{"192.168.1.88":{"minutes":59,"bytes":0,"blocked":false}}}
EOF
# Mock `date`, `ip`, `logger`
cat >"$WORK_DIR/bin/date" <<'EOF'
#!/usr/bin/env bash
echo "2026-09-08"
EOF
chmod +x "$WORK_DIR/bin/date"
cat >"$WORK_DIR/bin/logger" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
chmod +x "$WORK_DIR/bin/logger"
cat >"$WORK_DIR/bin/ip" <<'EOF'
#!/usr/bin/env bash
if [ "$1" = "neigh" ] && [ "$2" = "show" ]; then
echo "192.168.1.88 dev br-lan lladdr aa:bb:cc:dd:ee:ff REACHABLE"
fi
exit 0
EOF
chmod +x "$WORK_DIR/bin/ip"
# Run tick with TACHYON_UCI_STATE_FILE
PARENTAL_QUOTA_STATE_FILE="$STATE_FILE" TACHYON_UCI_STATE_FILE="$UCI_STATE" ucode -L "$TACHYON_LIB" "$PARENTAL_QUOTA" tick
# Device should now have reached 60 minutes and be marked blocked
grep -Eq '"blocked":\s*true' "$STATE_FILE" || fail "Guest device should be blocked after reaching time limit"
# Test status command
STATUS_JSON="$(PARENTAL_QUOTA_STATE_FILE="$STATE_FILE" TACHYON_UCI_STATE_FILE="$UCI_STATE" ucode -L "$TACHYON_LIB" "$PARENTAL_QUOTA" status)"
echo "$STATUS_JSON" | grep -q '"guest_mode"' || fail "Status must contain guest_mode object"
echo "$STATUS_JSON" | grep -Eq '"time_limit":\s*60' || fail "Status must contain time_limit:60"
echo "$STATUS_JSON" | grep -Eq '"traffic_limit":\s*500' || fail "Status must contain traffic_limit:500"
printf 'All guest mode isolation, validation, and quota tests passed successfully\n'