magisk-zapret2/tests/shell/owner-generation.sh
loop-uh e14c46afb1 Publish the DNS hosts file from /data instead of the module tree
The DNS manager used to apply a selection by writing
system/etc/hosts into the live module directory and asking for a
reboot, so the root manager would pick it up while assembling
/system. That put the module's own tree in the path of whatever the
manager does between boots: KernelSU v3 moved that assembly into a
separate metamodule, a second hosts module publishing the same path
raced it silently, and an upgrade replaced the tree and discarded the
user's selection with it.

The published file now lives in /data/adb/zapret2-hosts and the module
bind-mounts it over /system/etc/hosts from post-fs-data.sh, which is
the same mechanism on Magisk, KernelSU and APatch. hosts-overlay.sh
owns that mount: it snapshots the untouched system file before
covering it, carries a pre-2.3.0 in-tree overlay over and takes it out
of the tree, relabels the source so netd can still read it, and yields
to another enabled hosts module instead of fighting it for the path.

Because the module publishes the mount itself, an edit now takes
effect immediately: publication is still an atomic rename, and --apply
republishes the mount rather than waiting for a reboot. A rename
leaves the live mount on the replaced inode, so the mount identity is
recorded when it is published and consulted only against a live mount
— a foreign mount is never released.

Rollback, uninstall and purge follow the file to its new home, and the
installer carries an existing selection across the upgrade that would
otherwise drop it.

Also releases the tethering runtime key and owner state v9, and lifts
the packaged lifecycle contract to 9.
2026-07-28 00:56:30 +03:00

177 lines
8.4 KiB
Shell

#!/bin/sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
TMP=${Z2_TEST_TMP:?}
CASE="$TMP/owner-generation"
MOD="$CASE/module"
STATE="$CASE/state"
MOCK="$CASE/bin"
fail() { echo "FAIL: owner-generation: $*" >&2; exit 1; }
command -v chmod >/dev/null 2>&1 || chmod() { :; }
command -v sync >/dev/null 2>&1 || sync() { :; }
mkdir -p "$MOD/zapret2/scripts" "$STATE" "$MOCK"
chmod 0700 "$STATE"
cp "$ROOT/zapret2/scripts/common.sh" "$MOD/zapret2/scripts/common.sh"
cp "$ROOT/zapret2/scripts/firewall-reconciler.sh" "$MOD/zapret2/scripts/firewall-reconciler.sh"
: > "$MOD/zapret2/nfqws2"
cat > "$MOD/zapret2/install-generation.meta" <<EOF
version=1
module_dir=$MOD
generation=owner-test-install
archive_sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
EOF
chmod 0600 "$MOD/zapret2/install-generation.meta"
cat > "$MOCK/iptables" <<'EOF'
#!/bin/sh
case " $* " in
*' -t mangle -L OUTPUT -n '*) exit 0 ;;
*' -t mangle -S '*)
echo '-N ZAPRET2_OUT'
echo '-N ZAPRET2_IN'
[ "${Z2_SNAPSHOT_MODE:-ok}" = missing-anchor ] ||
echo '-A OUTPUT -j ZAPRET2_OUT'
echo '-A INPUT -j ZAPRET2_IN'
if [ "${Z2_SNAPSHOT_MODE:-ok}" = split ]; then
tail=' -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass'
for p in 80 443; do
echo "-A ZAPRET2_OUT -p tcp --dport $p -m connbytes --connbytes 1:20 --connbytes-dir original --connbytes-mode packets$tail"
done
for p in 443 3478 5349 19302; do
echo "-A ZAPRET2_OUT -p udp --dport $p -m connbytes --connbytes 1:20 --connbytes-dir original --connbytes-mode packets$tail"
done
for p in 80 443; do
echo "-A ZAPRET2_IN -p tcp --sport $p -m connbytes --connbytes 1:10 --connbytes-dir reply --connbytes-mode packets$tail"
done
for p in 443 3478 5349 19302; do
echo "-A ZAPRET2_IN -p udp --sport $p -m connbytes --connbytes 1:10 --connbytes-dir reply --connbytes-mode packets$tail"
done
exit 0
fi
if [ "${Z2_SNAPSHOT_MODE:-ok}" = wrong-payload ]; then
echo '-A ZAPRET2_OUT -p tcp -m multiport --dports 80,443 -m connbytes --connbytes 1:21 --connbytes-dir original --connbytes-mode packets -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass'
else
echo '-A ZAPRET2_OUT -p tcp -m multiport --dports 80,443 -m connbytes --connbytes 1:20 --connbytes-dir original --connbytes-mode packets -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass'
fi
echo '-A ZAPRET2_OUT -p udp -m multiport --dports 443,3478,5349,19302 -m connbytes --connbytes 1:20 --connbytes-dir original --connbytes-mode packets -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass'
echo '-A ZAPRET2_IN -p tcp -m multiport --sports 80,443 -m connbytes --connbytes 1:10 --connbytes-dir reply --connbytes-mode packets -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass'
echo '-A ZAPRET2_IN -p udp -m multiport --sports 443,3478,5349,19302 -m connbytes --connbytes 1:10 --connbytes-dir reply --connbytes-mode packets -m mark ! --mark 0x40000000/0x40000000 -j NFQUEUE --queue-num 200 --queue-bypass'
[ "${Z2_SNAPSHOT_MODE:-ok}" != extra-payload ] ||
echo '-A ZAPRET2_OUT -j RETURN'
;;
*' -t mangle -C OUTPUT -j ZAPRET2_OUT '*)
[ "${Z2_SNAPSHOT_MODE:-ok}" != missing-anchor ]
;;
*' -t mangle -C INPUT -j ZAPRET2_IN '*) exit 0 ;;
*' -t mangle -C ZAPRET2_OUT '*)
[ "${Z2_SNAPSHOT_MODE:-ok}" != wrong-payload ]
;;
*' -t mangle -C ZAPRET2_IN '*) exit 0 ;;
*' -t mangle -S ZAPRET2_OUT '*)
echo '-N ZAPRET2_OUT'
echo '-A ZAPRET2_OUT -p tcp -j NFQUEUE'
echo '-A ZAPRET2_OUT -p udp -j NFQUEUE'
[ "${Z2_SNAPSHOT_MODE:-ok}" != extra-payload ] ||
echo '-A ZAPRET2_OUT -j RETURN'
;;
*' -t mangle -S ZAPRET2_IN '*)
echo '-N ZAPRET2_IN'
echo '-A ZAPRET2_IN -p tcp -j NFQUEUE'
echo '-A ZAPRET2_IN -p udp -j NFQUEUE'
;;
*) exit 1 ;;
esac
EOF
cp "$MOCK/iptables" "$MOCK/ip6tables"
chmod 0755 "$MOCK/iptables" "$MOCK/ip6tables"
Z2_REAL_AWK="$(command -v awk)" || fail "awk is unavailable"
export Z2_REAL_AWK
cat > "$MOCK/awk" <<'EOF'
#!/bin/sh
previous=""
newline='
'
for argument in "$@"; do
if [ "$previous" = -v ]; then
case "$argument" in
*"$newline"*)
echo "test awk: newline in -v assignment" >&2
exit 97
;;
esac
fi
previous="$argument"
done
exec "$Z2_REAL_AWK" "$@"
EOF
chmod 0755 "$MOCK/awk"
export STATE_DIR="$STATE" SCRIPT_DIR="$MOD/zapret2/scripts" ZAPRET_DIR="$MOD/zapret2" MODDIR="$MOD" PATH="$MOCK:$PATH"
. "$SCRIPT_DIR/common.sh"
# Host-independent security shims keep this pure parser/firewall-identity test
# executable in the stripped Git shell; ownership/mode checks have dedicated
# installer/package tests.
path_uid_is_root() { :; }; path_mode_is_0600() { :; }; path_nlink_is_one() { :; }
state_file_is_secure() { [ -f "$1" ]; }; state_file_target_is_safe() { [ ! -L "$1" ]; }
read_current_boot_id() { CURRENT_BOOT_ID=11111111-1111-1111-1111-111111111111; }
if ! command -v sha256sum >/dev/null 2>&1; then sha256sum() { cat >/dev/null; echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; }; fi
new_lifecycle_token() { printf '%s\n' 'AbCdEf1234-generation'; }
QNUM=200; PORTS_TCP=80,443; PORTS_UDP=443,3478,5349,19302; TCP_PKT_OUT=20; TCP_PKT_IN=10; UDP_PKT_OUT=20; UDP_PKT_IN=10; PKT_OUT=20; PKT_IN=10; DESYNC_MARK=0x40000000
IPV4_CONNBYTES=1; IPV4_MULTIPORT=1; IPV4_MARK=1; IPV6_CONNBYTES=1; IPV6_MULTIPORT=1; IPV6_MARK=1
ZAPRET2_LIFECYCLE_TOKEN=app-request-generation
export ZAPRET2_LIFECYCLE_TOKEN
prepare_new_firewall_identity || fail "schema-v9 firewall identity preparation failed"
[ "$FIREWALL_TAG:$ZAPRET2_OUT:$ZAPRET2_IN" = stable0001:ZAPRET2_OUT:ZAPRET2_IN ] ||
fail "stable firewall identity changed"
[ "$PENDING_OWNER_GENERATION" = "$ZAPRET2_LIFECYCLE_TOKEN" ] ||
fail "owner generation is not bound to the exact lifecycle request"
unset ZAPRET2_LIFECYCLE_TOKEN
prepare_owner_generation_spec 1 0 || fail "owner generation preparation failed"
write_owner_state 123 456 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 200 generation-a active || fail "owner publication failed"
read_owner_state || fail "owner v9 round trip failed"
[ "$OWNER_STATE_SCHEMA_VERSION:$OWNER_STATE_FIREWALL_TAG:$OWNER_STATE_OUT_CHAIN:$OWNER_STATE_IN_CHAIN" = 9:stable0001:ZAPRET2_OUT:ZAPRET2_IN ] ||
fail "owner identity fields changed"
owner_family_generation_healthy iptables ipv4 || fail "healthy per-rule generation was rejected"
for mode in missing-anchor extra-payload wrong-payload; do
Z2_SNAPSHOT_MODE="$mode"; export Z2_SNAPSHOT_MODE
if owner_family_generation_healthy iptables ipv4; then fail "$mode generation was accepted"; fi
done
unset Z2_SNAPSHOT_MODE
# A kernel without xt_multiport, or a port list past the fifteen values the
# parser accepts, publishes one rule per interval and records multiport=0.
# That is a generation this module created, so re-reading it must recognise it
# — refusing would make the start path rebuild the ruleset on every run for the
# entire life of such a device. mark is different: every rule carries it, so a
# record claiming otherwise describes something the module did not publish.
Z2_SNAPSHOT_MODE=split; export Z2_SNAPSHOT_MODE
OWNER_STATE_IPV4_MULTIPORT=0
OWNER_STATE_IPV4_RULES=12
owner_family_generation_healthy iptables ipv4 ||
fail "a generation published without multiport was refused by its own module"
# The record still has to describe what is actually loaded: the same split
# ruleset read as a multiport generation is a mismatch, not a detail.
OWNER_STATE_IPV4_MULTIPORT=1
OWNER_STATE_IPV4_RULES=4
if owner_family_generation_healthy iptables ipv4; then
fail "a split ruleset was accepted as a multiport generation"
fi
unset Z2_SNAPSHOT_MODE
OWNER_STATE_IPV4_MULTIPORT=1
OWNER_STATE_IPV4_MARK=0
if owner_family_generation_healthy iptables ipv4; then
fail "a generation claiming no mark match was accepted"
fi
OWNER_STATE_IPV4_MARK=1
OWNER_STATE_IPV4_MULTIPORT=maybe
if owner_family_generation_healthy iptables ipv4; then
fail "a non-boolean multiport record was accepted"
fi
OWNER_STATE_IPV4_MULTIPORT=1
echo "Owner generation shell tests passed"