fix(singbox): AWG-джанк в тегах и MTProto-секрет

- i1–i5/j1–j3: классический hex конвертируется в теги <b 0x…>;
  userspace WireGuard extended/lx молча игнорирует голый hex,
  handshake не завершался (#39 #41 #42)
- j1/j2/j3/itime эмитятся только для extended ≤2.6.0: в 2.6.1+
  поля удалены из схемы, sing-box падает на unknown fields
- j1–j3 больше не ломаются uci_bin_to_hex
- h1–h4: диапазоны «min-max» на обеих ветках
- MTProto: секрет канонизируется в ee-hex из любой формы
  (hex/base64), валидатор даёт понятную ошибку вместо
  FATAL decode psk (#23)
- XHTTP v2: парсинг и экспорт ключей подписок
- валидатор: warp/masque/openvpn/snell/mieru/sudoku — только extended
- тесты: stat→ucode (busybox), раннеры исключены из E2E,
  новые server_mtproto.sh и awg_variant_schema.sh
This commit is contained in:
Dushnilin
2026-08-24 10:13:33 +03:00
parent 5031eda81b
commit 7da51a62d7
12 changed files with 581 additions and 52 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
# ---- 1. Schema boundary helper (j1/j2/j3/itime removed in extended 2.6.1)
cat >"$WORK_DIR/check_schema.uc" <<'UC'
let common = require("core.common");
printf("%s", common.extended_awg_schema_has_junk_signatures(ARGV[1]) ? "yes" : "no");
UC
check() {
ucode -L "$TACHYON_LIB" "$WORK_DIR/check_schema.uc" x "$1"
}
[ "$(check '1.13.16-extended-2.6.0')" = "yes" ] || fail "2.6.0 must still accept j1-j3/itime"
[ "$(check '1.13.14-extended-2.5.3')" = "yes" ] || fail "2.5.x must accept j1-j3/itime"
[ "$(check '1.13.16-extended-2.6.1')" = "no" ] || fail "2.6.1 removed j1-j3/itime"
[ "$(check '1.13.18-extended-2.6.5')" = "no" ] || fail "2.6.5 removed j1-j3/itime"
[ "$(check '1.3.0-extended-3.0.0')" = "no" ] || fail "3.x must not emit j1-j3/itime"
[ "$(check '1.12.0')" = "no" ] || fail "stock sing-box has no junk-signature fields"
[ "$(check '')" = "no" ] || fail "unknown version must not emit j1-j3/itime"
# ---- 2. Generator omits amnezia.j1 for extended 2.6.5 but keeps it for 2.6.0
make_fixture() {
cat >"$WORK_DIR/awg_fixture.json" <<JSON
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1"
},
"section": [
{
".name": "my_awg",
".type": "section",
"enabled": "1",
"action": "awg",
"awg_private_key": "aW5ib3VuZF9wcml2YXRlX2tleQ==",
"awg_peer_public_key": "cGVlcl9wdWJsaWNfa2V5",
"awg_server_address": "192.168.1.100",
"awg_server_port": "51820",
"awg_local_address": ["10.0.0.1/24"],
"awg_j1": "<b 0x0102030405>",
"awg_itime": "30"
}
]
}
JSON
}
generate() {
mkdir -p "$WORK_DIR/out.section-cache"
SB_VERSION_STATE_FILE="$WORK_DIR/sing-box-version" \
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/singbox/generator.uc" generate-config-fixture \
"$WORK_DIR/awg_fixture.json" "$WORK_DIR/singbox_awg.json" "127.0.0.1" 0 0
}
# 2.6.5: fields removed upstream -> must not appear in the generated config.
printf '1.13.18-extended-2.6.5\n' >"$WORK_DIR/sing-box-version"
make_fixture
generate
grep -q '"amnezia"' "$WORK_DIR/singbox_awg.json" || fail "extended endpoint must keep the amnezia object"
grep -q '"j1"' "$WORK_DIR/singbox_awg.json" && fail "generated config must not carry amnezia.j1 on extended 2.6.5"
grep -q '"itime"' "$WORK_DIR/singbox_awg.json" && fail "generated config must not carry amnezia.itime on extended 2.6.5"
# 2.6.0: schema still knows the fields -> they are emitted verbatim.
printf '1.13.16-extended-2.6.0\n' >"$WORK_DIR/sing-box-version"
make_fixture
generate
grep -q '"j1": "<b 0x0102030405>"' "$WORK_DIR/singbox_awg.json" || fail "extended 2.6.0 must keep amnezia.j1 with the tag template"
grep -q '"itime": 30' "$WORK_DIR/singbox_awg.json" || fail "extended 2.6.0 must keep amnezia.itime"
# ---- 3. Classic plain-hex i1 payloads are converted to the tag-chain format
# for every extended/lx build (userspace WireGuard ignores bare hex).
printf '1.13.18-extended-2.6.5\n' >"$WORK_DIR/sing-box-version"
cat >"$WORK_DIR/awg_fixture.json" <<JSON
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1"
},
"section": [
{
".name": "my_awg",
".type": "section",
"enabled": "1",
"action": "awg",
"awg_private_key": "aW5ib3VuZF9wcml2YXRlX2tleQ==",
"awg_peer_public_key": "cGVlcl9wdWJsaWNfa2V5",
"awg_server_address": "192.168.1.100",
"awg_server_port": "51820",
"awg_local_address": ["10.0.0.1/24"],
"awg_i1": "494e56495445207369703a626f62",
"awg_i2": "<b 0x0102><r 12>",
"awg_i3": ""
}
]
}
JSON
generate
grep -q '"i1": "<b 0x494e56495445207369703a626f62>"' "$WORK_DIR/singbox_awg.json" ||
fail "plain-hex awg_i1 must be wrapped into a <b 0x..> tag"
grep -q '"i2": "<b 0x0102><r 12>"' "$WORK_DIR/singbox_awg.json" ||
fail "existing tag chains must pass through verbatim"
grep -q '"i3"' "$WORK_DIR/singbox_awg.json" && fail "empty cps options must stay absent"
printf 'AWG variant schema checks passed\n'
+2 -1
View File
@@ -201,7 +201,8 @@ echo "=== Normalizing line endings for test scripts ==="
echo "=== Running unit and integration tests inside OpenWrt container ==="
for f in tests/*.sh; do
if [ "$f" != "tests/docker_e2e_test.sh" ] && [ "$f" != "tests/container_entrypoint.sh" ]; then
if [ "$f" != "tests/docker_e2e_test.sh" ] && [ "$f" != "tests/container_entrypoint.sh" ] &&
[ "$f" != "tests/run_all.sh" ] && [ "$f" != "tests/ucode_syntax_lint.sh" ]; then
echo "Running $f inside container..."
"$DOCKER_BIN" exec -w /work -e SB_REQUIRED_VERSION="1.11.0" "$CONTAINER_NAME" bash "$f"
fi
+2 -1
View File
@@ -88,7 +88,8 @@ fi
cmp -s "$WORK_DIR/config" "$WORK_DIR/default-config" ||
fail "reset must restore the config file to factory defaults"
[ "$(stat -c %a "$WORK_DIR/config")" = "600" ] ||
# busybox-based images may lack stat(1); ucode's fs module is always present.
[ "$(ucode -e "let fs = require('fs'); printf('%o', fs.stat('$WORK_DIR/config').mode & 511);")" = "600" ] ||
fail "reset config must be chmod 600"
[ ! -e "$WORK_DIR/persistent/subscription-cache" ] ||
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
# Canonical serialized secret for key 367a189aee18fa31c190054efd4a8e95 with
# faketls host google.com.
CANONICAL='ee367a189aee18fa31c190054efd4a8e95676f6f676c652e636f6d'
fixture_with_secret() {
cat >"$WORK_DIR/mtproto_fixture.json" <<JSON
{
"settings": {
".name": "settings",
".type": "settings",
"enabled": "1"
},
"server": [
{
".name": "my_mtproto_server",
".type": "server",
"enabled": "1",
"protocol": "mtproto",
"listen_port": "443",
"mtproto_secret": "$1",
"mtproto_faketls": "google.com"
}
],
"section": [
{
".name": "default_direct",
".type": "section",
"enabled": "1",
"action": "bypass"
}
]
}
JSON
}
generate() {
mkdir -p "$WORK_DIR/out.section-cache"
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/singbox/generator.uc" generate-config-fixture \
"$WORK_DIR/mtproto_fixture.json" "$WORK_DIR/singbox_mtproto.json" "127.0.0.1" 0 0
}
assert_canonical_user_password() {
grep -q '"password": "'"$CANONICAL"'"' "$WORK_DIR/singbox_mtproto.json" ||
fail "sing-box config must carry the canonical serialized secret as user password"
}
# 1. Full ee-secret in hex passes through unchanged.
fixture_with_secret "$CANONICAL"
generate
assert_canonical_user_password
# 2. Bare 16-byte key in hex gets wrapped with the marker and faketls host.
fixture_with_secret '367a189aee18fa31c190054efd4a8e95'
generate
assert_canonical_user_password
# 3. Full secret in padded standard base64 is decoded and re-encoded as hex.
fixture_with_secret '7jZ6GJruGPoxwZAFTv1KjpVnb29nbGUuY29t'
generate
assert_canonical_user_password
# 4. Bare key in padded standard base64 gets wrapped.
fixture_with_secret 'NnoYmu4Y+jHBkAVO/UqOlQ=='
generate
assert_canonical_user_password
# 5. Full secret in raw url-safe base64 (as produced by mtg) is normalized.
fixture_with_secret '7jZ6GJruGPoxwZAFTv1KjpVnb29nbGUuY29t'
generate
assert_canonical_user_password
# 6. Url-safe alphabet (- and _ instead of + and /) is translated.
fixture_with_secret 'NnoYmu4Y-jHBkAVO_UqOlQ'
generate
assert_canonical_user_password
# 7. The validator accepts a canonical secret and rejects garbage with a
# clear message instead of a sing-box FATAL at start time.
cat >"$WORK_DIR/validate_secret.uc" <<'UC'
let common = require("core.common");
let secret = ARGV[1];
printf("%s", common.mtproto_secret_canonical(secret, "google.com") == null ? "invalid" : "valid");
UC
[ "$(ucode -L "$TACHYON_LIB" "$WORK_DIR/validate_secret.uc" x "$CANONICAL")" = "valid" ] ||
fail "validator helper must accept the canonical hex secret"
[ "$(ucode -L "$TACHYON_LIB" "$WORK_DIR/validate_secret.uc" x '7jZ6GJruGPoxwZAFTv1KjpVnb29nbGUuY29t')" = "valid" ] ||
fail "validator helper must accept the base64 form of the secret"
[ "$(ucode -L "$TACHYON_LIB" "$WORK_DIR/validate_secret.uc" x 'not a real secret')" = "invalid" ] ||
fail "validator helper must reject unparseable secrets"
[ "$(ucode -L "$TACHYON_LIB" "$WORK_DIR/validate_secret.uc" x '')" = "invalid" ] ||
fail "validator helper must reject an empty secret"
printf 'MTProto server configuration checks passed\n'
+3 -2
View File
@@ -88,7 +88,8 @@ SAVED_FILE="$(sed -n 's/.*"file": "\([^"]*\)".*/\1/p' "$WORK_DIR/result.json")"
fail "snapshot file name must be <name>-<stamp>.tar.gz: $SAVED_FILE"
[ -f "$WORK_DIR/snapshots/$SAVED_FILE" ] ||
fail "snapshot archive must exist on disk"
[ "$(stat -c %a "$WORK_DIR/snapshots/$SAVED_FILE")" = "600" ] ||
# busybox-based images may lack stat(1); ucode's fs module is always present.
[ "$(ucode -e "let fs = require('fs'); printf('%o', fs.stat('$WORK_DIR/snapshots/$SAVED_FILE').mode & 511);")" = "600" ] ||
fail "snapshot archive must be chmod 600"
grep -Fxq 'config/tachyon' <(tar -tzf "$WORK_DIR/snapshots/$SAVED_FILE") ||
fail "snapshot must contain config/tachyon member"
@@ -136,7 +137,7 @@ grep -Fxq 'restart' "$WORK_DIR/bin.log" ||
fail "snapshot-restore must restart the service through the backend entrypoint"
grep -Fq "option dns '8.8.8.8'" "$WORK_DIR/config/tachyon" ||
fail "snapshot-restore must replace the config with the snapshot content"
[ "$(stat -c %a "$WORK_DIR/config/tachyon")" = "600" ] ||
[ "$(ucode -e "let fs = require('fs'); printf('%o', fs.stat('$WORK_DIR/config/tachyon').mode & 511);")" = "600" ] ||
fail "restored config must be chmod 600"
[ "$(cat "$WORK_DIR/tachyon/data")" = 'snap-data' ] ||
fail "snapshot-restore must restore the persistent data"