Files
NeoTachyon/tests/parental_control.sh
T

358 lines
11 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"
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'"
}
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"
cat >"$WORK_DIR/schedule_fixture.json" <<'JSON'
{
"section": [
{
".name": "sec_youtube",
"enabled": "1",
"action": "connection",
"ip_cidr": [ "142.250.0.0/15" ]
},
{
".name": "sec_discord",
"enabled": "1",
"action": "connection",
"ip_cidr": [ "162.158.0.0/16" ]
}
],
"schedule": [
{
".name": "phone_night",
"enabled": "1",
"device_ip": "192.168.1.150",
"target": "all",
"action": "block",
"start_time": "22:00",
"end_time": "08:00",
"days": [ "mon", "tue", "wed", "thu", "fri" ]
},
{
".name": "tv_multi",
"enabled": "1",
"device_ip": "192.168.1.160",
"target": "sections",
"sections": [ "sec_youtube", "sec_discord" ],
"action": "block",
"start_time": "21:00",
"end_time": "07:00",
"days": [ "sat", "sun" ]
},
{
".name": "mac_rule",
"enabled": "1",
"device_ip": "11:22:33:44:55:66",
"target": "all",
"action": "block",
"start_time": "10:00",
"end_time": "12:00"
}
]
}
JSON
# Run nft-add-schedule-rules-fixture
nft_ucode nft-add-schedule-rules-fixture "$WORK_DIR/schedule_fixture.json" "tachyon"
# Verify rules in NFT_LOG
assert_contains "$NFT_LOG" "parental_forward ip saddr 192.168.1.150 meta day { Friday, Monday, Thursday, Tuesday, Wednesday } meta hour \"22:00:00\"-\"23:59:59\" counter drop" "midnight wrap part 1"
assert_contains "$NFT_LOG" "parental_forward ip saddr 192.168.1.150 meta day { Friday, Monday, Thursday, Tuesday, Wednesday } meta hour \"00:00:00\"-\"08:00:00\" counter drop" "midnight wrap part 2"
assert_contains "$NFT_LOG" "parental_control ip saddr 192.168.1.150 meta day { Friday, Monday, Thursday, Tuesday, Wednesday } meta hour \"22:00:00\"-\"23:59:59\" counter drop" "parental_control drop"
# Verify MAC address rule in NFT_LOG
assert_contains "$NFT_LOG" "parental_forward ether saddr 11:22:33:44:55:66 meta hour \"10:00:00\"-\"12:00:00\" counter drop" "mac ether saddr drop"
# Verify per-section blocking
assert_contains "$NFT_LOG" "ip saddr 192.168.1.160 meta day { Saturday, Sunday } meta hour \"21:00:00\"-\"23:59:59\" ip daddr @tachyon_rule_sec_youtube_subnets counter drop" "section youtube subnet drop"
assert_contains "$NFT_LOG" "ip saddr 192.168.1.160 meta day { Saturday, Sunday } meta hour \"21:00:00\"-\"23:59:59\" ip daddr @tachyon_rule_sec_discord_subnets counter drop" "section discord subnet drop"
# Verify signature changes when schedule is modified
SIG1="$(nft_ucode nft-runtime-signature-fixture "$WORK_DIR/schedule_fixture.json")"
cat >"$WORK_DIR/schedule_fixture2.json" <<'JSON'
{
"section": [
{
".name": "sec_youtube",
"enabled": "1",
"action": "connection"
}
],
"schedule": [
{
".name": "phone_night",
"enabled": "0",
"device_ip": "192.168.1.150",
"target": "all"
}
]
}
JSON
SIG2="$(nft_ucode nft-runtime-signature-fixture "$WORK_DIR/schedule_fixture2.json")"
[ "$SIG1" != "$SIG2" ] || fail "Signature must change when schedule is updated"
# Verify validator rejects invalid schedule time
cat >"$WORK_DIR/invalid_time.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"schedule": [
{
".name": "bad_time",
"enabled": "1",
"device_ip": "192.168.1.100",
"start_time": "25:00"
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_time.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject invalid start_time 25:00"
fi
# Verify validator rejects invalid schedule device IP
cat >"$WORK_DIR/invalid_ip.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"schedule": [
{
".name": "bad_ip",
"enabled": "1",
"device_ip": "999.999.999.999",
"start_time": "22:00",
"end_time": "08:00"
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_ip.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject invalid device_ip 999.999.999.999"
fi
# Verify validator rejects unknown target section
cat >"$WORK_DIR/invalid_target.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"schedule": [
{
".name": "bad_target",
"enabled": "1",
"device_ip": "192.168.1.100",
"target": "non_existent_section"
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_target.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject non-existent target section"
fi
# Verify validator rejects unknown profile reference in schedule
cat >"$WORK_DIR/invalid_profile_ref.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"schedule": [
{
".name": "bad_profile_ref",
"enabled": "1",
"profile": [ "non_existent_profile" ]
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_profile_ref.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject non-existent profile reference"
fi
# Verify validator rejects invalid profile device IP
cat >"$WORK_DIR/invalid_profile_ip.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings", "dns_server": ["77.88.8.8"], "bootstrap_dns_server": ["77.88.8.8"] },
"profile": [
{
".name": "bad_profile",
"enabled": "1",
"device_ip": [ "invalid-ip-or-mac" ]
}
]
}
JSON
if ucode -L "$TACHYON_LIB" "$VALIDATOR_RUNTIME" validate-runtime-fixture "$WORK_DIR/invalid_profile_ip.json" "{}" >/dev/null 2>&1; then
fail "Validator must reject invalid profile device IP"
fi
# ─── Family Profile inheritance fixture ──────────────────────────────────────
cat >"$WORK_DIR/profile_fixture.json" <<'JSON'
{
"profile": [
{
".name": "prof_kids",
"label": "Kids",
"avatar": "👶",
"enabled": "1",
"device_ip": [ "192.168.1.200", "22:33:44:55:66:77" ],
"block_doh": "1",
"safe_search": "1",
"daily_quota_minutes": "90"
}
],
"schedule": [
{
".name": "kids_schedule",
"enabled": "1",
"profile": [ "prof_kids" ],
"target": "all",
"action": "block",
"start_time": "23:00",
"end_time": "06:00"
}
]
}
JSON
rm -f "$NFT_LOG"
nft_ucode nft-add-schedule-rules-fixture "$WORK_DIR/profile_fixture.json" "tachyon"
# Schedule must inherit devices from prof_kids
assert_contains "$NFT_LOG" "parental_forward ip saddr 192.168.1.200 meta hour \"23:00:00\"-\"23:59:59\" counter drop" "profile IP inherited"
assert_contains "$NFT_LOG" "parental_forward ether saddr 22:33:44:55:66:77 meta hour \"23:00:00\"-\"23:59:59\" counter drop" "profile MAC inherited"
# ─── Router panel and Intranet bypass rules in Parental chains ───────────────
rm -f "$NFT_LOG"
nft_ucode nft-create-runtime-base TachyonTable localv4 tachyon_subnets tachyon_ports tachyon_ip_ports tachyon_interfaces "br-lan" 0x00100000 0x00200000 198.18.0.0/15 1602 0 "" "" "" "" "" 0
assert_contains "$NFT_LOG" "parental_control ip daddr @localv4 return" "parental_control localv4 bypass"
assert_contains "$NFT_LOG" "parental_control ip6 daddr @localv6 return" "parental_control localv6 bypass"
assert_contains "$NFT_LOG" "parental_forward ip daddr @localv4 return" "parental_forward localv4 bypass"
assert_contains "$NFT_LOG" "parental_forward ip6 daddr @localv6 return" "parental_forward localv6 bypass"
# Ensure in mangle chain that localv4 return comes BEFORE jump parental_control
local_return_line="$(grep -n "mangle iifname @tachyon_interfaces ip daddr @localv4 return" "$NFT_LOG" | cut -d: -f1)"
parental_jump_line="$(grep -n "mangle jump parental_control" "$NFT_LOG" | cut -d: -f1)"
[ -n "$local_return_line" ] || fail "mangle chain must contain localv4 return rule"
[ -n "$parental_jump_line" ] || fail "mangle chain must contain jump parental_control rule"
if [ "$local_return_line" -ge "$parental_jump_line" ]; then
fail "localv4 return (line $local_return_line) must precede jump parental_control (line $parental_jump_line) to protect router panel access"
fi
# ─── Domain-based schedule must NOT add global counter drop rules ────────────
cat >"$WORK_DIR/domain_schedule_fixture.json" <<'JSON'
{
"schedule": [
{
".name": "domain_only_sched",
"enabled": "1",
"device_ip": "192.168.1.180",
"target": "all",
"blocked_domains": [ "tiktok.com", "roblox.com" ],
"action": "block"
},
{
".name": "target_domains_sched",
"enabled": "1",
"device_ip": "192.168.1.181",
"target": "domains",
"action": "block"
}
]
}
JSON
rm -f "$NFT_LOG"
touch "$NFT_LOG"
nft_ucode nft-add-schedule-rules-fixture "$WORK_DIR/domain_schedule_fixture.json" "tachyon"
if grep -Eq "parental_(forward|control).*192\.168\.1\.180.*counter.*drop" "$NFT_LOG"; then
fail "Schedule with blocked_domains must NOT add global counter drop in parental_forward/parental_control"
fi
if grep -Eq "parental_(forward|control).*192\.168\.1\.181.*counter.*drop" "$NFT_LOG"; then
fail "Schedule with target=domains must NOT add global counter drop in parental_forward/parental_control"
fi
# ─── 24/7 Schedule referencing Profile with daily_quota_minutes must NOT drop statically
cat >"$WORK_DIR/quota_schedule_fixture.json" <<'JSON'
{
"profile": [
{
".name": "prof_quota_only",
"enabled": "1",
"device_ip": [ "192.168.1.222" ],
"daily_quota_minutes": "120"
}
],
"schedule": [
{
".name": "quota_24_7_sched",
"enabled": "1",
"profile": [ "prof_quota_only" ],
"target": "all",
"action": "block"
}
]
}
JSON
rm -f "$NFT_LOG"
touch "$NFT_LOG"
nft_ucode nft-add-schedule-rules-fixture "$WORK_DIR/quota_schedule_fixture.json" "tachyon"
if grep -Eq "parental_(forward|control).*192\.168\.1\.222.*counter.*drop" "$NFT_LOG"; then
fail "24/7 Schedule with profile daily_quota_minutes must NOT add static counter drop"
fi
printf 'Parental control, profiles and schedule tests passed\n'