328 lines
12 KiB
Bash
328 lines
12 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"
|
|
FUZZER="$ROOT_DIR/tachyon/files/usr/lib/diagnostics/fuzzer.uc"
|
|
BYEDPI_VALIDATOR="$ROOT_DIR/tachyon/files/usr/lib/providers/byedpi/validator.uc"
|
|
ZAPRET_VALIDATOR="$ROOT_DIR/tachyon/files/usr/lib/providers/zapret/validator.uc"
|
|
ZAPRET2_VALIDATOR="$ROOT_DIR/tachyon/files/usr/lib/providers/zapret2/validator.uc"
|
|
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# 1. Check strategies JSON output
|
|
strategies_json="$(ucode -L "$TACHYON_LIB" -- "$FUZZER" strategies)"
|
|
[ -n "$strategies_json" ] || fail "fuzzer strategies returned empty output"
|
|
|
|
# Validate JSON structure using node
|
|
JSON_VALUE="$strategies_json" node <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
if (typeof val.available_engines !== 'object') {
|
|
console.error("Missing available_engines");
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.zapret2) || val.zapret2.length === 0) {
|
|
console.error("Missing zapret2 strategies");
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.zapret) || val.zapret.length === 0) {
|
|
console.error("Missing zapret strategies");
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.byedpi) || val.byedpi.length === 0) {
|
|
console.error("Missing byedpi strategies");
|
|
process.exit(1);
|
|
}
|
|
NODE
|
|
|
|
# 2. Check each strategy passes its respective engine validator
|
|
byedpi_args="$(JSON_VALUE="$strategies_json" node - <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
for (const s of val.byedpi) {
|
|
console.log(s.args);
|
|
}
|
|
NODE
|
|
)"
|
|
|
|
while IFS= read -r args; do
|
|
[ -n "$args" ] || continue
|
|
check="$(ucode -L "$TACHYON_LIB" -- "$BYEDPI_VALIDATOR" validate-json "$args")"
|
|
JSON_VALUE="$check" node - <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
if (!val.valid) {
|
|
console.error("ByeDPI strategy invalid:", val);
|
|
process.exit(1);
|
|
}
|
|
NODE
|
|
done <<< "$byedpi_args"
|
|
|
|
zapret_args="$(JSON_VALUE="$strategies_json" node - <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
for (const s of val.zapret) {
|
|
console.log(s.args);
|
|
}
|
|
NODE
|
|
)"
|
|
|
|
while IFS= read -r args; do
|
|
[ -n "$args" ] || continue
|
|
check="$(ucode -L "$TACHYON_LIB" -- "$ZAPRET_VALIDATOR" validate-json nfqws "$args")"
|
|
JSON_VALUE="$check" node - <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
if (!val.valid) {
|
|
console.error("Zapret strategy invalid:", val);
|
|
process.exit(1);
|
|
}
|
|
NODE
|
|
done <<< "$zapret_args"
|
|
|
|
zapret2_args="$(JSON_VALUE="$strategies_json" node - <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
for (const s of val.zapret2) {
|
|
console.log(s.args);
|
|
}
|
|
NODE
|
|
)"
|
|
|
|
while IFS= read -r args; do
|
|
[ -n "$args" ] || continue
|
|
check="$(ucode -L "$TACHYON_LIB" -- "$ZAPRET2_VALIDATOR" validate-json nfqws2 "$args")"
|
|
JSON_VALUE="$check" node - <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
if (!val.valid) {
|
|
console.error("Zapret2 strategy invalid:", val);
|
|
process.exit(1);
|
|
}
|
|
NODE
|
|
done <<< "$zapret2_args"
|
|
|
|
# 3. Check status returns clean default state
|
|
status_json="$(ucode -L "$TACHYON_LIB" -- "$FUZZER" status)"
|
|
JSON_VALUE="$status_json" node <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
if (val.running !== false) {
|
|
console.error("Default fuzzer status should have running: false");
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.results)) {
|
|
console.error("Results should be an array");
|
|
process.exit(1);
|
|
}
|
|
NODE
|
|
|
|
# 4. Check tachyon CLI command map contains fuzzer commands
|
|
grep -q 'fuzzer_start:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_start"
|
|
grep -q 'fuzzer_status:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_status"
|
|
grep -q 'fuzzer_stop:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_stop"
|
|
grep -q 'fuzzer_apply:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_apply"
|
|
grep -q 'fuzzer_strategies:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_strategies"
|
|
grep -q 'fuzzer_generate:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_generate"
|
|
grep -q 'fuzzer_ai_synthesize:' "$TACHYON_BIN" || fail "tachyon CLI missing fuzzer_ai_synthesize"
|
|
|
|
# 5. Check combinatorial strategies generation
|
|
combo_tmp="$(mktemp "${TMPDIR:-/tmp}/fuzzer_combo_XXXXXX")"
|
|
trap 'rm -f "$combo_tmp"' EXIT
|
|
ucode -L "$TACHYON_LIB" -- "$FUZZER" strategies combinatorial > "$combo_tmp"
|
|
COMBO_FILE="$combo_tmp" node <<'NODE'
|
|
const fs = require('fs');
|
|
const raw = fs.readFileSync(process.env.COMBO_FILE, 'utf8');
|
|
const val = JSON.parse(raw);
|
|
if (!Array.isArray(val.zapret2) || val.zapret2.length < 30) {
|
|
console.error("Combinatorial zapret2 should have >= 30 strategies, got:", val.zapret2 ? val.zapret2.length : 0);
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.zapret) || val.zapret.length < 20) {
|
|
console.error("Combinatorial zapret should have >= 20 strategies, got:", val.zapret ? val.zapret.length : 0);
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.byedpi) || val.byedpi.length < 20) {
|
|
console.error("Combinatorial byedpi should have >= 20 strategies, got:", val.byedpi ? val.byedpi.length : 0);
|
|
process.exit(1);
|
|
}
|
|
console.log("Generated matrix: Zapret2=" + val.zapret2.length + ", Zapret=" + val.zapret.length + ", ByeDPI=" + val.byedpi.length);
|
|
NODE
|
|
|
|
# 6. Check zapret2 Lua library resolution from LIB_DIR
|
|
lua_res="$(TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" -e '
|
|
let fs = require("fs");
|
|
let LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
|
|
let candidate_dirs = [
|
|
getenv("ZAPRET2_PROVIDER_LUA_DIR"),
|
|
LIB_DIR + "/providers/zapret2/lua",
|
|
"/usr/lib/tachyon/providers/zapret2/lua"
|
|
];
|
|
let lua_scripts = [ "zapret-lib.lua", "zapret-antidpi.lua", "zapret-auto.lua" ];
|
|
let flags = "";
|
|
for (let script in lua_scripts) {
|
|
let found = null;
|
|
for (let d in candidate_dirs) {
|
|
if (!d || fs.stat(d) == null) continue;
|
|
let p = d + "/" + script;
|
|
if (fs.stat(p) != null) { found = p; break; }
|
|
if (fs.stat(p + ".gz") != null) { found = p + ".gz"; break; }
|
|
}
|
|
if (found != null) flags += sprintf("--lua-init=@%s ", found);
|
|
}
|
|
print(flags);
|
|
')"
|
|
echo "$lua_res" | grep -q 'zapret-lib.lua' || fail "missing zapret-lib.lua in resolved lua flags"
|
|
echo "$lua_res" | grep -q 'zapret-antidpi.lua' || fail "missing zapret-antidpi.lua in resolved lua flags"
|
|
echo "$lua_res" | grep -q 'zapret-auto.lua' || fail "missing zapret-auto.lua in resolved lua flags"
|
|
|
|
# 7. Check hardened runner, fail-closed validation, and absence of global killall curl
|
|
grep -q 'killall -9 curl' "$FUZZER" && fail "killall -9 curl must not be present in fuzzer.uc" || true
|
|
|
|
ucode -L "$TACHYON_LIB" -e '
|
|
let r = require("diagnostics.fuzzer_runner");
|
|
|
|
// 1. Fail-closed: unknown engine rejected
|
|
if (r.validate_strategy_args("unknown_engine", "--split-pos=1") !== false) {
|
|
warn("Failed: unknown engine was not rejected\n");
|
|
exit(1);
|
|
}
|
|
|
|
// 2. Fail-closed: shell metacharacters rejected
|
|
if (r.validate_strategy_args("zapret2", "--split-pos=1; rm -rf /") !== false) {
|
|
warn("Failed: semicolon command injection was not rejected\n");
|
|
exit(1);
|
|
}
|
|
if (r.validate_strategy_args("byedpi", "-s1 | reboot") !== false) {
|
|
warn("Failed: pipe command injection was not rejected\n");
|
|
exit(1);
|
|
}
|
|
if (r.validate_strategy_args("zapret", "--dpi-desync=split2 `reboot`") !== false) {
|
|
warn("Failed: backtick command injection was not rejected\n");
|
|
exit(1);
|
|
}
|
|
if (r.validate_strategy_args("zapret2", "--dpi-desync-split-pos=$(cat /etc/passwd)") !== false) {
|
|
warn("Failed: command substitution was not rejected\n");
|
|
exit(1);
|
|
}
|
|
|
|
// 3. Hostname and URL validation
|
|
if (!r.is_valid_hostname("youtube.com") || !r.is_valid_hostname("googlevideo.com") || !r.is_valid_hostname("1.1.1.1")) {
|
|
warn("Failed: valid hostname was rejected\n");
|
|
exit(1);
|
|
}
|
|
if (r.is_valid_hostname("evil.com;rm") || r.is_valid_hostname("evil.com`id`") || r.is_valid_hostname("")) {
|
|
warn("Failed: malicious hostname was accepted\n");
|
|
exit(1);
|
|
}
|
|
if (!r.is_valid_url("https://www.youtube.com/watch?v=dQw4w9WgXcQ")) {
|
|
warn("Failed: valid URL was rejected\n");
|
|
exit(1);
|
|
}
|
|
if (r.is_valid_url("http://evil.com/`rm -rf /`") || r.is_valid_url("https://evil.com/;reboot") || r.is_valid_url("")) {
|
|
warn("Failed: malicious URL was accepted\n");
|
|
exit(1);
|
|
}
|
|
|
|
// 4. Tokenizer limit checks
|
|
let huge = "";
|
|
for (let i = 0; i < 70; i++) huge += sprintf("--split-pos=%d ", i);
|
|
let tok = r.tokenize_strategy_args(huge);
|
|
if (tok.valid !== false) {
|
|
warn("Failed: token limit > 64 was not rejected\n");
|
|
exit(1);
|
|
}
|
|
|
|
// 5. System capabilities & stats calculation checks
|
|
let caps = r.get_system_capabilities();
|
|
if (type(caps) != "object" || type(caps.http2) != "bool" || type(caps.http3) != "bool") {
|
|
warn("Failed: invalid capabilities object returned\n");
|
|
exit(1);
|
|
}
|
|
|
|
let mem_kb = r.get_system_memory_kb();
|
|
if (type(mem_kb) != "int" || mem_kb <= 0) {
|
|
warn("Failed: invalid memory returned\n");
|
|
exit(1);
|
|
}
|
|
|
|
let test_vals = [ 100, 110, 120 ];
|
|
let med = r.calculate_median(test_vals);
|
|
if (med != 110) {
|
|
warn(sprintf("Failed: median should be 110, got %d\n", med));
|
|
exit(1);
|
|
}
|
|
|
|
let jitter = r.calculate_jitter(test_vals, med);
|
|
if (jitter != 6 && jitter != 7) {
|
|
warn(sprintf("Failed: jitter unexpected, got %d\n", jitter));
|
|
exit(1);
|
|
}
|
|
|
|
let p25 = r.calculate_p25([ 100, 200, 300, 400 ]);
|
|
if (p25 != 200 && p25 != 100) {
|
|
warn(sprintf("Failed: p25 unexpected, got %d\n", p25));
|
|
exit(1);
|
|
}
|
|
|
|
print("Hardened runner, capabilities, and statistics checks passed.\n");
|
|
'
|
|
|
|
# 8. Check TARGET_SUITES structure and requirements
|
|
ucode -L "$TACHYON_LIB" -- "$FUZZER" strategies presets | JSON_VALUE="$(cat)" node - <<'NODE'
|
|
const val = JSON.parse(process.env.JSON_VALUE);
|
|
const suites = val.target_suites;
|
|
if (!suites || typeof suites !== 'object') {
|
|
console.error("Missing target_suites");
|
|
process.exit(1);
|
|
}
|
|
|
|
const yt = suites.youtube_suite;
|
|
if (!yt || !Array.isArray(yt.urls) || yt.urls.length < 3) {
|
|
console.error("Invalid youtube_suite structure");
|
|
process.exit(1);
|
|
}
|
|
|
|
const streamItem = yt.urls.find(u => u.name && u.name.includes("Stream"));
|
|
if (!streamItem || streamItem.required !== true || streamItem.probe_kind !== 'streaming') {
|
|
console.error("YouTube Stream CDN must have required: true and probe_kind: streaming, got:", streamItem);
|
|
process.exit(1);
|
|
}
|
|
|
|
for (const key of Object.keys(suites)) {
|
|
const s = suites[key];
|
|
for (const u of s.urls) {
|
|
if (typeof u.weight !== 'number' || u.weight <= 0) {
|
|
console.error(`Missing valid weight in ${key}:`, u);
|
|
process.exit(1);
|
|
}
|
|
if (typeof u.probe_kind !== 'string') {
|
|
console.error(`Missing probe_kind in ${key}:`, u);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
console.log("Verified TARGET_SUITES weights, required flags, and probe kinds.");
|
|
NODE
|
|
|
|
# 9. Check Adaptive strategy generation
|
|
adaptive_tmp="$(mktemp "${TMPDIR:-/tmp}/fuzzer_adaptive_XXXXXX")"
|
|
trap 'rm -f "$adaptive_tmp" "$combo_tmp"' EXIT
|
|
ucode -L "$TACHYON_LIB" -- "$FUZZER" strategies adaptive > "$adaptive_tmp"
|
|
ADAPTIVE_FILE="$adaptive_tmp" node <<'NODE'
|
|
const fs = require('fs');
|
|
const val = JSON.parse(fs.readFileSync(process.env.ADAPTIVE_FILE, 'utf8'));
|
|
if (!Array.isArray(val.zapret2) || val.zapret2.length < 5) {
|
|
console.error("Adaptive zapret2 should have >= 5 strategies, got:", val.zapret2 ? val.zapret2.length : 0);
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.zapret) || val.zapret.length < 5) {
|
|
console.error("Adaptive zapret should have >= 5 strategies, got:", val.zapret ? val.zapret.length : 0);
|
|
process.exit(1);
|
|
}
|
|
if (!Array.isArray(val.byedpi) || val.byedpi.length < 5) {
|
|
console.error("Adaptive byedpi should have >= 5 strategies, got:", val.byedpi ? val.byedpi.length : 0);
|
|
process.exit(1);
|
|
}
|
|
console.log("Adaptive strategies generated: Zapret2=" + val.zapret2.length + ", Zapret=" + val.zapret.length + ", ByeDPI=" + val.byedpi.length);
|
|
NODE
|
|
|
|
printf 'PASS: fuzzer_strategy_cli\n'
|