Add priority bypass routing action

Replace user-facing direct routing action with bypass, add priority nft bypass handling, real DNS for bypass domains, migration cleanup, and the related runtime/frontend tests.

Also includes the previously pending runtime log hygiene cleanup and regression coverage that was already in the working tree.
This commit is contained in:
ushan0v
2026-07-11 00:10:54 +03:00
parent ae1e0373ee
commit 9ce314e5c4
39 changed files with 1081 additions and 739 deletions
@@ -84,13 +84,13 @@ function uiValues(field) {
return [...result].sort();
}
function assertCurrentKeepsStableValues(name, required = []) {
function assertCurrentKeepsStableValues(name, required = [], retired = []) {
const field = matrix.fields.find((item) => item.name === name);
if (!field) fail(`expected enum field is absent from matrix: ${name}`);
const stableValues = uiValues(field.stable);
const currentValues = uiValues(field.current);
const missingStable = stableValues.filter((value) => !currentValues.includes(value));
const missingStable = stableValues.filter((value) => !currentValues.includes(value) && !retired.includes(value));
if (missingStable.length) {
fail(`current UI contract for ${name} is missing stable values: ${missingStable.join(", ")}`);
}
@@ -101,7 +101,7 @@ function assertCurrentKeepsStableValues(name, required = []) {
}
}
assertCurrentKeepsStableValues("action", ["proxy", "vpn", "direct", "block", "zapret", "zapret2", "byedpi", "outbound"]);
assertCurrentKeepsStableValues("action", ["proxy", "vpn", "bypass", "block", "zapret", "zapret2", "byedpi", "outbound"], ["direct"]);
assertCurrentKeepsStableValues("protocol", ["tailscale", "vless", "vmess", "trojan", "shadowsocks", "hysteria2", "socks", "mtproto", "json_inbound"]);
assertCurrentKeepsStableValues("security", ["reality", "tls", "none"]);
assertCurrentKeepsStableValues("transport", ["tcp", "ws", "grpc", "http", "httpupgrade", "xhttp"]);
+31 -1
View File
@@ -30,7 +30,8 @@ const fixture = {
settings: {
'.name': 'settings',
'.type': 'settings',
download_lists_via_proxy: '1'
download_lists_via_proxy: '1',
routing_excluded_ips: [ '192.0.2.0/24' ]
},
rule: [
{
@@ -70,6 +71,18 @@ const fixture = {
urltest_enabled: '1',
detect_server_country: '0'
},
{
'.name': 'legacy-direct',
'.type': 'section',
action: 'direct',
ip_cidr: [ '198.51.100.0/24' ]
},
{
'.name': 'legacy-exclusion',
'.type': 'section',
connection_type: 'exclusion',
ip_cidr: [ '203.0.113.0/24' ]
},
{
'.name': 'legacy-zap',
'.type': 'section',
@@ -121,6 +134,7 @@ assert(config.settings.update_interval === '1d', 'missing update_interval should
assert(config.settings.download_lists_via_proxy === '1', 'download_lists_via_proxy should be preserved');
assert(config.settings.download_subscriptions_via_proxy === '1', 'download_subscriptions_via_proxy should be copied');
assert(config.settings.download_components_via_proxy === '1', 'download_components_via_proxy should be copied');
absent(config.settings, 'routing_excluded_ips', 'settings');
const legacyUrl = sections['legacy-url'];
assert(legacyUrl['.type'] === 'section', 'legacy rule should become section');
@@ -154,6 +168,13 @@ assert(legacyUrltest.detect_server_country === 'flag_emoji', 'detect server coun
assert(JSON.stringify(legacyUrltest.selector_proxy_links) === JSON.stringify(['vmess://a', 'trojan://b']), 'urltest links deduped');
absent(legacyUrltest, 'urltest_proxy_links', 'legacy-urltest');
const legacyDirect = sections['legacy-direct'];
assert(legacyDirect.action === 'bypass', 'direct action migrated to bypass');
const legacyExclusion = sections['legacy-exclusion'];
assert(legacyExclusion.action === 'bypass', 'legacy exclusion connection type migrated to bypass');
absent(legacyExclusion, 'connection_type', 'legacy-exclusion');
const legacyZap = sections['legacy-zap'];
assert(legacyZap.nfqws_opt === process.env.ZAPRET_DEFAULT_NFQWS_OPT, 'zapret legacy default migrated');
assert(legacyZap.byedpi_cmd_opts === '--legacy-bye', 'cmd_opts copied to byedpi_cmd_opts');
@@ -172,11 +193,15 @@ NODE
cat >"$WORK_DIR/runtime-migrate.state" <<'EOF_UCI'
podkop-plus.settings=settings
podkop-plus.settings.routing_excluded_ips=192.0.2.0/24
podkop-plus.legacy=rule
podkop-plus.legacy.enabled=1
podkop-plus.legacy.connection_type=proxy
podkop-plus.legacy.proxy_config_type=url
podkop-plus.legacy.proxy_string=vless://one
podkop-plus.old_direct=section
podkop-plus.old_direct.enabled=1
podkop-plus.old_direct.action=direct
EOF_UCI
mkdir -p "$WORK_DIR/runtime" "$WORK_DIR/persistent-cache"
: >"$WORK_DIR/runtime-migrate.log"
@@ -193,8 +218,13 @@ grep -Fxq 'podkop-plus.legacy=section' "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must convert legacy rule type through core.uci"
grep -Fxq 'podkop-plus.legacy.action=proxy' "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must write migrated action through core.uci"
grep -Fxq 'podkop-plus.old_direct.action=bypass' "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must convert direct action to bypass through core.uci"
grep -Fxq 'podkop-plus.legacy.selector_proxy_links=vless://one' "$WORK_DIR/runtime-migrate.state" ||
fail "runtime migration must write migrated list option through core.uci"
if grep -Fq 'podkop-plus.settings.routing_excluded_ips=' "$WORK_DIR/runtime-migrate.state"; then
fail "runtime migration must delete removed routing_excluded_ips through core.uci"
fi
if grep -Fq 'podkop-plus.legacy.proxy_string=' "$WORK_DIR/runtime-migrate.state"; then
fail "runtime migration must delete legacy proxy_string through core.uci"
fi
@@ -52,6 +52,6 @@ assert_rejects "empty target" "no download section is selected" "" 0 0 0 proxy 1
assert_rejects "missing target" "references missing rule 'missing'" missing 0 0 0 proxy 1 proxy
assert_rejects "disabled target" "references disabled rule 'proxy'" proxy 0 0 0 proxy 0 proxy
assert_rejects "provider missing" "cannot provide an outbound" zap 0 0 0 zap 1 zapret
assert_rejects "unsupported action" "cannot provide an outbound" direct 0 0 0 direct 1 direct
assert_rejects "unsupported action" "cannot provide an outbound" bypass 0 0 0 bypass 1 bypass
printf 'config validator download section regression checks passed\n'
@@ -83,6 +83,13 @@ cat >"$WORK_DIR/valid.json" <<'JSON'
"outbound_detour_enabled": "1",
"outbound_detour_section": "detour"
},
{
".name": "bypass",
".type": "section",
"enabled": "1",
"action": "bypass",
"domain_suffix": [ "bypass.example" ]
},
{
".name": "zap",
".type": "section",
@@ -96,6 +103,16 @@ JSON
validate_fixture "$WORK_DIR/valid.json"
cat >"$WORK_DIR/bad-direct-action.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
"section": [
{ ".name": "old", ".type": "section", "enabled": "1", "action": "direct" }
]
}
JSON
assert_rejects "bad direct action" "$WORK_DIR/bad-direct-action.json" "unsupported action 'direct'"
cat >"$WORK_DIR/bad-port.json" <<'JSON'
{
"settings": { ".name": "settings", ".type": "settings" },
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
reject_runtime_pattern() {
local pattern="$1"
local label="$2"
if rg -n --fixed-strings "$pattern" "$ROOT_DIR/podkop/files" >/dev/null 2>&1; then
rg -n --fixed-strings "$pattern" "$ROOT_DIR/podkop/files" >&2
fail "$label"
fi
}
reject_runtime_regex() {
local pattern="$1"
local label="$2"
if rg -n "$pattern" "$ROOT_DIR/podkop/files" >/dev/null 2>&1; then
rg -n "$pattern" "$ROOT_DIR/podkop/files" >&2
fail "$label"
fi
}
reject_runtime_pattern "ucode sing-box runtime generator" \
"sing-box logs must not expose the implementation language"
reject_runtime_pattern "runtime generator" \
"sing-box generator errors must describe config generation, not the internal generator"
reject_runtime_pattern "Reload signature changes:" \
"reload logs must not dump internal signature vectors"
reject_runtime_pattern "Reload plan: sing-box=" \
"reload logs must not dump boolean plan internals"
reject_runtime_pattern "Trying subscription User-Agent" \
"subscription logs must not expose automatic User-Agent fallback attempts"
reject_runtime_pattern "Selected subscription User-Agent" \
"subscription logs must not expose automatic User-Agent fallback selection"
reject_runtime_pattern "Current sing-box config hash:" \
"sing-box config logs must not emit duplicate hash lines"
reject_runtime_pattern "Temporary sing-box config hash:" \
"sing-box config logs must not emit duplicate hash lines"
reject_runtime_pattern "Podkop Plus did not reach a stable running state after start" \
"startup rollback logs must not be fatal secondary errors"
reject_runtime_pattern "Flush nft" \
"stop logs must not expose routine nft cleanup as user-facing info"
reject_runtime_pattern "Flush ip rule" \
"stop logs must not expose routine ip-rule cleanup as user-facing info"
reject_runtime_pattern "Flush ip route" \
"stop logs must not expose routine route cleanup as user-facing info"
reject_runtime_pattern "Create IPv4 marking rule" \
"nft logs must use polished wording"
reject_runtime_pattern "Create IPv6 marking rule" \
"nft logs must use polished wording"
reject_runtime_pattern "marking rule exist" \
"nft logs must use correct grammar"
reject_runtime_pattern "br_netfilter enabled detected. Disabling" \
"nft logs must use polished bridge-netfilter wording"
reject_runtime_regex 'log_message\("subscription/cache\.uc: ' \
"runtime logs must not expose subscription/cache.uc as a user-facing prefix"
reject_runtime_regex 'log_message\("singbox/runtime\.uc: ' \
"runtime logs must not expose singbox/runtime.uc as a user-facing prefix"
printf 'log hygiene regression checks passed\n'
+103 -25
View File
@@ -44,6 +44,21 @@ assert_eq() {
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
}
assert_line_before() {
local file="$1"
local first="$2"
local second="$3"
local label="$4"
local first_line
local second_line
first_line="$(grep -Fn "$first" "$file" | head -n 1 | cut -d: -f1 || true)"
second_line="$(grep -Fn "$second" "$file" | head -n 1 | cut -d: -f1 || true)"
[ -n "$first_line" ] || fail "$label: missing first line '$first'"
[ -n "$second_line" ] || fail "$label: missing second line '$second'"
[ "$first_line" -lt "$second_line" ] || fail "$label: expected first line before second"
}
if grep -n 'require("uci").cursor' "$NFT_RUNTIME" >/dev/null 2>&1 ||
grep -n 'uci -q' "$NFT_RUNTIME" >/dev/null 2>&1 ||
grep -n '"uci", "-q"' "$NFT_RUNTIME" >/dev/null 2>&1 ||
@@ -189,11 +204,15 @@ assert_contains "$NFT_LOG" $'nft\tadd\tset\tinet\tPodkopPlusTable\tpodkop_plus_i
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_interfaces\t{ br-lan }' "runtime br-lan interface"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_interfaces\t{ tun0 }' "runtime tun0 interface"
assert_contains "$NFT_LOG" $'nft\tadd\tchain\tinet\tPodkopPlusTable\tmangle\t{ type filter hook prerouting priority -150; policy accept; }' "runtime mangle chain"
assert_contains "$NFT_LOG" $'nft\tadd\tchain\tinet\tPodkopPlusTable\tpriority_rules\t{ }' "runtime priority chain"
assert_contains "$NFT_LOG" $'nft\tadd\tchain\tinet\tPodkopPlusTable\tpriority_output_rules\t{ }' "runtime priority output chain"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tmangle\tjump\tpriority_rules' "runtime priority jump"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tmangle\tiifname\t@podkop_plus_interfaces\tip\tdaddr\t@podkop_plus_subnets\tmeta\tl4proto\ttcp\tmeta\tmark\tset\t0x00100000\tcounter' "runtime common tcp rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tmangle\tiifname\t@podkop_plus_interfaces\tip6\tdaddr\t@podkop_plus_subnets6\tmeta\tl4proto\ttcp\tmeta\tmark\tset\t0x00100000\tcounter' "runtime common6 tcp rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tproxy\tmeta\tmark\t&\t0x00100000\t==\t0x00100000\tmeta\tl4proto\ttcp\ttproxy\tip\tto\t:1602\tcounter' "runtime proxy tcp rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tproxy\tmeta\tmark\t&\t0x00100000\t==\t0x00100000\tmeta\tl4proto\ttcp\ttproxy\tip6\tto\t[::1]:1602\tcounter' "runtime proxy6 tcp rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tmangle_output\tmeta\tmark\t0x00200000\tcounter\treturn' "runtime outbound return"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tmangle_output\tjump\tpriority_output_rules' "runtime priority output jump"
assert_contains "$NFT_LOG" $'nft\tinsert\trule\tinet\tPodkopPlusTable\tmangle\tudp\tdport\t123\treturn' "runtime ntp exclusion"
cat >"$WORK_DIR/runtime-base-uci.state" <<'EOF_UCI'
@@ -228,7 +247,7 @@ assert_contains "$NFT_LOG" $'nft\tlist\ttable\tinet\tPodkopPlusTable' "nft table
cat >"$WORK_DIR/provider-rules.json" <<'JSON'
{
"section": [
{ ".name": "direct", "enabled": "1", "action": "direct" },
{ ".name": "direct", "enabled": "1", "action": "bypass" },
{ ".name": "zapret_one", "enabled": "1", "action": "zapret" },
{ ".name": "zapret_disabled", "enabled": "0", "action": "zapret" },
{ ".name": "zapret_two", "enabled": "1", "action": "zapret" },
@@ -255,6 +274,46 @@ if [ -s "$NFT_LOG" ]; then
fail "missing provider binary should skip provider nft output rules"
fi
cat >"$WORK_DIR/priority-rules.json" <<'JSON'
{
"section": [
{
".name": "bypass_first",
".type": "section",
"enabled": "1",
"action": "bypass",
"ip_cidr": [ "198.51.100.0/24" ]
},
{
".name": "wide_proxy",
".type": "section",
"enabled": "1",
"action": "proxy",
"ip_cidr": [ "0.0.0.0/0" ]
},
{
".name": "port_bypass",
".type": "section",
"enabled": "1",
"action": "bypass",
"ports": [ "8443" ]
}
]
}
JSON
: > "$NFT_LOG"
nft_ucode nft-add-section-priority-rules-fixture "$WORK_DIR/priority-rules.json" PodkopPlusTable podkop_plus_interfaces localv4 localv6 0x00100000
assert_contains "$NFT_LOG" $'nft\tadd\tset\tinet\tPodkopPlusTable\tpodkop_plus_rule_bypass_first_subnets\t{ type ipv4_addr; flags interval; auto-merge; }' "bypass priority subnet set"
assert_contains "$NFT_LOG" $'nft\tadd\tset\tinet\tPodkopPlusTable\tpodkop_plus_rule_wide_proxy_subnets\t{ type ipv4_addr; flags interval; auto-merge; }' "proxy priority subnet set"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tpriority_rules\tiifname\t@podkop_plus_interfaces\tip\tdaddr\t!=\t@localv4\tip\tdaddr\t@podkop_plus_rule_bypass_first_subnets\tcounter\taccept' "bypass priority accept rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tpriority_rules\tiifname\t@podkop_plus_interfaces\tip\tdaddr\t!=\t@localv4\tip\tdaddr\t@podkop_plus_rule_wide_proxy_subnets\tmeta\tmark\tset\t0x00100000\tcounter\taccept' "proxy priority capture rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tpriority_rules\tiifname\t@podkop_plus_interfaces\tip\tdaddr\t!=\t@localv4\ttcp\tdport\t@podkop_plus_rule_port_bypass_ports\tcounter\taccept' "port-only bypass priority rule"
assert_line_before "$NFT_LOG" \
$'nft\tadd\trule\tinet\tPodkopPlusTable\tpriority_rules\tiifname\t@podkop_plus_interfaces\tip\tdaddr\t!=\t@localv4\tip\tdaddr\t@podkop_plus_rule_bypass_first_subnets\tcounter\taccept' \
$'nft\tadd\trule\tinet\tPodkopPlusTable\tpriority_rules\tiifname\t@podkop_plus_interfaces\tip\tdaddr\t!=\t@localv4\tip\tdaddr\t@podkop_plus_rule_wide_proxy_subnets\tmeta\tmark\tset\t0x00100000\tcounter\taccept' \
"bypass priority order"
: > "$IP_LOG"
: > "$LOGGER_LOG"
rt_tables="$WORK_DIR/rt_tables"
@@ -264,10 +323,10 @@ assert_contains "$IP_LOG" $'ip\troute\tadd\tlocal\t0.0.0.0/0\tdev\tlo\ttable\tpo
assert_contains "$IP_LOG" $'ip\t-6\troute\tadd\tlocal\t::/0\tdev\tlo\ttable\tpodkop' "tproxy route6 add"
assert_contains "$IP_LOG" $'ip\t-4\trule\tadd\tfwmark\t0x00100000/0x00100000\ttable\tpodkop\tpriority\t105' "tproxy marking rule add"
assert_contains "$IP_LOG" $'ip\t-6\trule\tadd\tfwmark\t0x00100000/0x00100000\ttable\tpodkop\tpriority\t105' "tproxy marking rule6 add"
assert_contains "$LOGGER_LOG" "[debug] Added IPv4 route for tproxy" "tproxy route creation log"
assert_contains "$LOGGER_LOG" "[debug] Added IPv6 route for tproxy" "tproxy route6 creation log"
assert_contains "$LOGGER_LOG" "[debug] Create IPv4 marking rule" "tproxy marking rule creation log"
assert_contains "$LOGGER_LOG" "[debug] Create IPv6 marking rule" "tproxy marking rule6 creation log"
assert_contains "$LOGGER_LOG" "[debug] Added IPv4 TPROXY route" "tproxy route creation log"
assert_contains "$LOGGER_LOG" "[debug] Added IPv6 TPROXY route" "tproxy route6 creation log"
assert_contains "$LOGGER_LOG" "[debug] Creating IPv4 TPROXY marking rule" "tproxy marking rule creation log"
assert_contains "$LOGGER_LOG" "[debug] Creating IPv6 TPROXY marking rule" "tproxy marking rule6 creation log"
: > "$IP_LOG"
: > "$LOGGER_LOG"
@@ -280,10 +339,10 @@ IP_ROUTE_OUTPUT='local default dev lo scope host' \
if grep -Fq $'\tadd\t' "$IP_LOG"; then
fail "existing tproxy route/rule should not be added again"
fi
assert_contains "$LOGGER_LOG" "[debug] IPv4 route for tproxy exists" "existing tproxy route log"
assert_contains "$LOGGER_LOG" "[debug] IPv6 route for tproxy exists" "existing tproxy route6 log"
assert_contains "$LOGGER_LOG" "[debug] IPv4 marking rule exist" "existing tproxy marking rule log"
assert_contains "$LOGGER_LOG" "[debug] IPv6 marking rule exist" "existing tproxy marking rule6 log"
assert_contains "$LOGGER_LOG" "[debug] IPv4 TPROXY route already exists" "existing tproxy route log"
assert_contains "$LOGGER_LOG" "[debug] IPv6 TPROXY route already exists" "existing tproxy route6 log"
assert_contains "$LOGGER_LOG" "[debug] IPv4 TPROXY marking rule already exists" "existing tproxy marking rule log"
assert_contains "$LOGGER_LOG" "[debug] IPv6 TPROXY marking rule already exists" "existing tproxy marking rule6 log"
IP_ROUTE_OUTPUT='local default dev lo scope host' \
IP_ROUTE6_OUTPUT='local default dev lo metric 1024 pref medium' \
IP_RULE_OUTPUT='105: from all fwmark 0x100000/0x100000 lookup podkop' \
@@ -303,7 +362,7 @@ LSMOD_OUTPUT='br_netfilter 32768 0' SYSCTL_BRIDGE_NF_CALL_IPTABLES=1 nft_ucode e
assert_contains "$SYSCTL_LOG" $'sysctl\t-n\tnet.bridge.bridge-nf-call-iptables' "bridge netfilter sysctl check"
assert_contains "$SYSCTL_LOG" $'sysctl\t-w\tnet.bridge.bridge-nf-call-iptables=0' "bridge netfilter ipv4 disable"
assert_contains "$SYSCTL_LOG" $'sysctl\t-w\tnet.bridge.bridge-nf-call-ip6tables=0' "bridge netfilter ipv6 disable"
assert_contains "$LOGGER_LOG" "[debug] br_netfilter enabled detected. Disabling" "bridge netfilter disable log"
assert_contains "$LOGGER_LOG" "[debug] br_netfilter is enabled; disabling it for transparent proxy routing" "bridge netfilter disable log"
: > "$SYSCTL_LOG"
LSMOD_OUTPUT='' SYSCTL_BRIDGE_NF_CALL_IPTABLES=1 nft_ucode ensure-bridge-netfilter-disabled
@@ -341,6 +400,7 @@ cat >"$WORK_DIR/populate-fixture.json" <<'JSON'
".name": "disabled",
".type": "section",
"enabled": "0",
"action": "proxy",
"ip_cidr": [ "192.0.2.1" ],
"ports": [ "53" ],
"fully_routed_ips": [ "192.168.1.50/32" ]
@@ -349,6 +409,7 @@ cat >"$WORK_DIR/populate-fixture.json" <<'JSON'
".name": "deferred",
".type": "section",
"enabled": "1",
"action": "proxy",
"ip_cidr": [ "192.0.2.2" ],
"ports": [ "54" ],
"fully_routed_ips": [ "192.168.1.51/32" ]
@@ -357,6 +418,7 @@ cat >"$WORK_DIR/populate-fixture.json" <<'JSON'
".name": "inline",
".type": "section",
"enabled": "1",
"action": "proxy",
"ip_cidr": [ "198.51.100.1", "203.0.113.0/24", "2001:db8::1" ],
"ports": [ "80", "443-444" ],
"fully_routed_ips": [ "192.168.1.20/32", "192.168.1.20/32", "2001:db8::20/128" ]
@@ -365,12 +427,14 @@ cat >"$WORK_DIR/populate-fixture.json" <<'JSON'
".name": "inline_no_ports",
".type": "section",
"enabled": "1",
"action": "bypass",
"ip_cidr": [ "198.51.100.200", "2001:db8::200" ]
},
{
".name": "ports_only",
".type": "section",
"enabled": "1",
"action": "proxy",
"ports": [ "53", "853" ],
"ports_text": "5353, 853"
},
@@ -378,6 +442,7 @@ cat >"$WORK_DIR/populate-fixture.json" <<'JSON'
".name": "ports_with_domain",
".type": "section",
"enabled": "1",
"action": "proxy",
"domain_suffix": [ "example.org" ],
"ports": [ "8443" ]
}
@@ -394,8 +459,8 @@ EOF_INPUT
: > "$NFT_LOG"
nft_ucode nft-add-subnet-file-for-section-fixture "$WORK_DIR/populate-fixture.json" inline_no_ports "$plain_subnets" PodkopPlusTable podkop_plus_subnets podkop_plus_ip_ports 5000
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_subnets\t{ 198.51.100.210,203.0.113.0/24 }' "plain subnet import without ports"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_subnets6\t{ 2001:db8::210 }' "plain subnet6 import without ports"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_subnets\t{ 198.51.100.210,203.0.113.0/24 }' "plain subnet import without ports"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_subnets6\t{ 2001:db8::210 }' "plain subnet6 import without ports"
: > "$NFT_LOG"
nft_ucode nft-add-subnet-file-for-section-fixture "$WORK_DIR/populate-fixture.json" ports_only "$plain_subnets" PodkopPlusTable podkop_plus_subnets podkop_plus_ip_ports 3
@@ -426,9 +491,9 @@ JSON
unscoped_json="$WORK_DIR/unscoped-json.txt"
scoped_json="$WORK_DIR/scoped-json.txt"
nft_ucode nft-add-json-ruleset-subnets-for-section-fixture "$WORK_DIR/populate-fixture.json" inline_no_ports "$json_ruleset" "fixture json" PodkopPlusTable podkop_plus_subnets podkop_plus_ip_ports "$unscoped_json" "$scoped_json" 5000
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_subnets\t{ 198.51.100.220 }' "json ruleset unscoped import"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_subnets6\t{ 2001:db8::220 }' "json ruleset unscoped6 import"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_ip_ports\t{ 198.51.100.221 . 443,198.51.100.222 . 1000-1002 }' "json ruleset own port filters"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_subnets\t{ 198.51.100.220 }' "json ruleset unscoped import"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_subnets6\t{ 2001:db8::220 }' "json ruleset unscoped6 import"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_ip_ports\t{ 198.51.100.221 . 443,198.51.100.222 . 1000-1002 }' "json ruleset own port filters"
: > "$NFT_LOG"
unscoped_json="$WORK_DIR/unscoped-json-ports.txt"
@@ -441,16 +506,11 @@ fi
: > "$NFT_LOG"
nft_ucode nft-add-community-subnet-file-for-section-fixture "$WORK_DIR/populate-fixture.json" inline_no_ports discord "$plain_subnets" PodkopPlusTable podkop_plus_subnets podkop_plus_ip_ports podkop_plus_interfaces podkop_plus_discord_subnets 0x00100000 5000
assert_contains "$NFT_LOG" $'nft\tadd\tset\tinet\tPodkopPlusTable\tpodkop_plus_discord_subnets\t{ type ipv4_addr; flags interval; auto-merge; }' "discord community set"
assert_contains "$NFT_LOG" $'nft\tadd\tset\tinet\tPodkopPlusTable\tpodkop_plus_discord_subnets6\t{ type ipv6_addr; flags interval; auto-merge; }' "discord community6 set"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tmangle\tiifname\t@podkop_plus_interfaces\tip\tdaddr\t@podkop_plus_discord_subnets\tudp\tdport\t{ 19000-20000, 50000-65535 }\tmeta\tmark\tset\t0x00100000\tcounter' "discord community udp rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tPodkopPlusTable\tmangle\tiifname\t@podkop_plus_interfaces\tip6\tdaddr\t@podkop_plus_discord_subnets6\tudp\tdport\t{ 19000-20000, 50000-65535 }\tmeta\tmark\tset\t0x00100000\tcounter' "discord community udp6 rule"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_discord_subnets\t{ 198.51.100.210,203.0.113.0/24 }' "discord community subnet import"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_subnets\t{ 198.51.100.210,203.0.113.0/24 }' "discord community subnet import"
: > "$NFT_LOG"
NFT_MANGLE_CHAIN_OUTPUT=$'iifname @podkop_plus_interfaces ip daddr @podkop_plus_discord_subnets udp dport { 19000-20000, 50000-65535 } meta mark set 0x00100000 counter\niifname @podkop_plus_interfaces ip6 daddr @podkop_plus_discord_subnets6 udp dport { 19000-20000, 50000-65535 } meta mark set 0x00100000 counter' \
nft_ucode nft-add-community-subnet-file-for-section-fixture "$WORK_DIR/populate-fixture.json" inline_no_ports discord "$plain_subnets" PodkopPlusTable podkop_plus_subnets podkop_plus_ip_ports podkop_plus_interfaces podkop_plus_discord_subnets 0x00100000 5000
assert_contains "$NFT_LOG" $'nft\tadd\tset\tinet\tPodkopPlusTable\tpodkop_plus_discord_subnets\t{ type ipv4_addr; flags interval; auto-merge; }' "discord community existing set"
if grep -Fq $'nft\tadd\trule' "$NFT_LOG"; then
fail "discord community existing rule should not insert"
fi
@@ -466,9 +526,11 @@ assert_contains "$NFT_LOG" $'198.51.100.210 . 53,198.51.100.210 . 853,198.51.100
nft_ucode nft-populate-runtime-sets-fixture "$WORK_DIR/populate-fixture.json" 1 "deferred" PodkopPlusTable podkop_plus_subnets podkop_plus_ports podkop_plus_ip_ports podkop_plus_interfaces localv4 0x00100000
assert_contains "$NFT_LOG" $'198.51.100.1 . 80,198.51.100.1 . 443-444,203.0.113.0/24 . 80' "populate inline ip-port first chunk"
assert_contains "$NFT_LOG" $'203.0.113.0/24 . 443-444' "populate inline ip-port second chunk"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_subnets\t{ 198.51.100.200 }' "populate inline ip without ports"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_subnets6\t{ 2001:db8::200 }' "populate inline ip6 without ports"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_ports\t{ 53,853,5353 }' "populate ports-only set"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_ip_ports\t{ 198.51.100.1 . 80,198.51.100.1 . 443-444,203.0.113.0/24 . 80,203.0.113.0/24 . 443-444 }' "populate inline priority ip-port set"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_ip6_ports\t{ 2001:db8::1 . 80,2001:db8::1 . 443-444 }' "populate inline priority ip6-port set"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_subnets\t{ 198.51.100.200 }' "populate inline ip without ports"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_inline_no_ports_subnets6\t{ 2001:db8::200 }' "populate inline ip6 without ports"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_ports_only_ports\t{ 53,853,5353 }' "populate ports-only set"
assert_contains "$NFT_LOG" $'nft\tinsert\trule\tinet\tPodkopPlusTable\tmangle\tiifname\t@podkop_plus_interfaces\tip\tsaddr\t192.168.1.20/32\tmeta\tl4proto\ttcp\tmeta\tmark\tset\t0x00100000\tcounter' "populate fully routed tcp"
assert_contains "$NFT_LOG" $'nft\tinsert\trule\tinet\tPodkopPlusTable\tmangle\tiifname\t@podkop_plus_interfaces\tip6\tsaddr\t2001:db8::20/128\tmeta\tl4proto\ttcp\tmeta\tmark\tset\t0x00100000\tcounter' "populate fully routed6 tcp"
if grep -Fq '192.0.2.1' "$NFT_LOG"; then
@@ -477,7 +539,7 @@ fi
if grep -Fq '192.0.2.2' "$NFT_LOG"; then
fail "deferred section should not populate nft"
fi
if grep -Fq $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_ports\t{ 8443 }' "$NFT_LOG"; then
if grep -Fq $'nft\tadd\telement\tinet\tPodkopPlusTable\tpodkop_plus_rule_ports_with_domain_ports\t{ 8443 }' "$NFT_LOG"; then
fail "ports with destination matchers should not populate global port set"
fi
if [ "$(grep -F $'ip\tsaddr\t192.168.1.20/32' "$NFT_LOG" | wc -l | tr -d ' ')" != "3" ]; then
@@ -495,6 +557,7 @@ cat >"$WORK_DIR/signature-fixture.json" <<'JSON'
".name": "disabled",
".type": "section",
"enabled": "0",
"action": "proxy",
"ip_cidr": [ "192.0.2.10" ],
"ports": [ "1234" ],
"community_lists": [ "discord" ]
@@ -503,6 +566,7 @@ cat >"$WORK_DIR/signature-fixture.json" <<'JSON'
".name": "text_rule",
".type": "section",
"enabled": "1",
"action": "bypass",
"ip_cidr_text_mode": "1",
"ip_cidr_text": "198.51.100.1\nbad-value\n203.0.113.0/24",
"ip_cidr": [ "10.0.0.0/8" ],
@@ -517,6 +581,7 @@ cat >"$WORK_DIR/signature-fixture.json" <<'JSON'
{
".name": "default_enabled",
".type": "section",
"action": "proxy",
"ip_cidr": [ "10.0.0.0/8", "192.0.2.1" ],
"ports_text": "53 853",
"community_lists": [ "youtube", "cloudflare", "roblox" ]
@@ -530,8 +595,12 @@ cat >"$WORK_DIR/signature-expected.txt" <<'EOF_EXPECTED'
br-lan tun0
[settings.exclude_ntp]
1
[rule.text_rule.action]
bypass
[rule.text_rule.ip_cidr]
198.51.100.1,203.0.113.0/24
[rule.text_rule.source_ip_cidr]
[rule.text_rule.ports]
443,80,443-444
[rule.text_rule.fully_routed_ips]
@@ -544,8 +613,12 @@ https://example.com/subnets.lst
/tmp/local.json
[rule.text_rule.domain_ip_lists]
https://example.com/mixed.lst
[rule.default_enabled.action]
proxy
[rule.default_enabled.ip_cidr]
10.0.0.0/8,192.0.2.1
[rule.default_enabled.source_ip_cidr]
[rule.default_enabled.ports]
53,853
[rule.default_enabled.fully_routed_ips]
@@ -573,6 +646,7 @@ podkop-plus.disabled.enabled=0
podkop-plus.disabled.ip_cidr=192.0.2.10
podkop-plus.enabled=section
podkop-plus.enabled.enabled=1
podkop-plus.enabled.action=bypass
podkop-plus.enabled.ip_cidr=10.0.0.0/8 192.0.2.1
podkop-plus.enabled.ports=443 bad-value
podkop-plus.enabled.ports_text=80 443-444 65536
@@ -587,8 +661,12 @@ cat >"$WORK_DIR/signature-uci-expected.txt" <<'EOF_EXPECTED'
br-lan tun0
[settings.exclude_ntp]
1
[rule.enabled.action]
bypass
[rule.enabled.ip_cidr]
10.0.0.0/8,192.0.2.1
[rule.enabled.source_ip_cidr]
[rule.enabled.ports]
443,80,443-444
[rule.enabled.fully_routed_ips]
+1 -1
View File
@@ -38,7 +38,7 @@ fi
assert_eq "2" "$(rows | rules_ucode count zapret)" "enabled zapret count"
assert_eq "1" "$(rows | rules_ucode count byedpi)" "enabled byedpi count"
assert_eq "1" "$(rows | rules_ucode count zapret2)" "enabled zapret2 count"
assert_eq "0" "$(rows | rules_ucode count direct)" "missing action count"
assert_eq "0" "$(rows | rules_ucode count bypass)" "non-provider action count"
assert_eq "1" "$(rows | rules_ucode index zapret alpha)" "first zapret index"
assert_eq "2" "$(rows | rules_ucode index zapret delta)" "second zapret index skips disabled rows"
@@ -264,7 +264,6 @@ cat >"$WORK_DIR/sing-box-signature.json" <<'JSON'
"dns_rewrite_ttl": "120",
"output_network_interface": "wan",
"disable_quic": "on",
"routing_excluded_ips": [ "192.0.2.0/24" ],
"list_update_enabled": "1",
"update_interval": "2h",
"cache_path": "/tmp/cache.db",
@@ -392,8 +391,6 @@ tcp
wan
[settings.disable_quic]
1
[settings.routing_excluded_ips]
192.0.2.0/24
[settings.update_interval]
2h
[settings.cache_path]
+16 -4
View File
@@ -211,8 +211,7 @@ cat >"$WORK_DIR/runtime-matchers-fixture.json" <<'JSON'
".type": "settings",
"config_path": "/tmp/sing-box/config.json",
"dns_server": "1.1.1.1",
"service_listen_address": "127.0.0.1",
"routing_excluded_ips": [ "192.168.1.5/32", "2001:db8::5/128" ]
"service_listen_address": "127.0.0.1"
},
"section": [
{
@@ -223,13 +222,22 @@ cat >"$WORK_DIR/runtime-matchers-fixture.json" <<'JSON'
"outbound_json": "{\"type\":\"direct\"}",
"domain_suffix": [ "detour.example" ]
},
{
".name": "bypass",
".type": "section",
"enabled": "1",
"action": "bypass",
"domain_suffix": [ "example.org" ],
"ip_cidr": [ "198.51.100.0/24" ],
"source_ip_cidr": [ "10.0.0.3/32" ]
},
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "outbound",
"outbound_json": "{\"type\":\"socks\",\"server\":\"127.0.0.1\",\"server_port\":1080}",
"domain_suffix": [ "example.org" ],
"domain_suffix": [ "proxy.example.org" ],
"source_ip_cidr": [ "10.0.0.2/32", "2001:db8::2/128" ],
"outbound_detour_enabled": "1",
"outbound_detour_section": "detour",
@@ -645,12 +653,16 @@ assert(matchers.dns.strategy == "prefer_ipv4", "DNS strategy allows IPv6 answers
assert(dns_server(matchers, r => r.tag == "fakeip-server" && r.inet6_range == "fc00::/18") != null, "FakeIP IPv6 range");
assert(inbound(matchers, "tproxy6-in") != null, "IPv6 TProxy inbound");
assert(inbound(matchers, "tproxy6-in").listen == "::1", "IPv6 TProxy listen address");
assert(outbound(matchers, "bypass-out").type == "direct", "bypass fallback outbound");
assert(outbound(matchers, "proxy-out").detour == "detour-out", "outbound detour");
let mixed = inbound(matchers, "proxy-mixed-in");
assert(mixed && mixed.listen_port == 19090 && mixed.users[0].username == "user", "mixed inbound auth");
assert(route_rule(matchers, r => r.inbound == "proxy-mixed-in" && r.outbound == "proxy-out") != null, "mixed inbound route");
assert(dns_rule(matchers, r => contains(r.domain_suffix, "example.org")).server == "dns-server", "bypass domain real DNS rule");
assert(dns_rule(matchers, r => contains(r.domain_suffix, "proxy.example.org")).server == "fakeip-server", "proxy domain FakeIP DNS rule");
assert(route_rule(matchers, r => r.outbound == "bypass-out" && contains(r.domain_suffix, "example.org") && contains(r.source_ip_cidr, "10.0.0.3/32")) != null, "bypass fallback route");
assert(route_rule(matchers, r => contains(r.inbound, "tproxy-in") && contains(r.inbound, "tproxy6-in") && r.outbound == "proxy-out") != null, "section route dual tproxy inbound");
assert(route_rule(matchers, r => r.outbound == "direct-out" && contains(r.source_ip_cidr, "192.168.1.5/32") && contains(r.source_ip_cidr, "2001:db8::5/128")) != null, "routing excluded source");
assert(route_rule(matchers, r => r.outbound == "direct-out" && contains(r.source_ip_cidr, "192.168.1.5/32")) == null, "routing excluded source removed");
assert(route_rule(matchers, r => r.outbound == "proxy-out" && contains(r.source_ip_cidr, "10.0.0.2/32") && contains(r.source_ip_cidr, "2001:db8::2/128")) != null, "source_ip_cidr matcher");
let urltest = cfg("urltest");
@@ -104,6 +104,27 @@ if cache_ucode subscription-metadata-path '../bad' >/dev/null 2>&1; then
fail "subscription metadata path mode should reject unsafe section names"
fi
cat >"$WORK_DIR/normalized-subscription.json" <<'JSON'
{
"version": 1,
"format": "uri-list",
"skipped": 2,
"outbounds": [
{ "type": "vless", "tag": "one" },
{ "type": "trojan", "tag": "two" }
]
}
JSON
assert_eq "2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-import-stats "$WORK_DIR/normalized-subscription.json")" \
"subscription import stats"
assert_eq "Subscription source 1 for rule 'proxy' imported: 2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-source-summary proxy 1 "$WORK_DIR/normalized-subscription.json" imported)" \
"subscription source imported summary"
assert_eq "Subscription source 1 for rule 'proxy' is unchanged: 2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-source-summary proxy 1 "$WORK_DIR/normalized-subscription.json" unchanged)" \
"subscription source unchanged summary"
runtime_env() {
TMP_SING_BOX_FOLDER="$WORK_DIR/runtime/tmp-sing-box" \
TMP_RULESET_FOLDER="$WORK_DIR/runtime/tmp-sing-box/rulesets" \
@@ -172,7 +193,7 @@ cat >"$WORK_DIR/cache-maintenance.json" <<'JSON'
{
".name": "direct",
"enabled": "1",
"action": "direct"
"action": "bypass"
},
{
".name": "disabled",
+1 -1
View File
@@ -103,7 +103,7 @@ if ui_ucode service-action-expected-running invalid >/dev/null 2>&1; then
fi
ui_ucode latency-type-valid proxy_list >/dev/null ||
fail "proxy_list latency type should be valid"
if ui_ucode latency-type-valid direct >/dev/null 2>&1; then
if ui_ucode latency-type-valid invalid >/dev/null 2>&1; then
fail "invalid latency type should be rejected"
fi