refactor(xray-proxy): срезана accidental-сложность CLI, тестов и спеки
- Зачем:
- CLI и спека были переусложнены относительно принятой модели угроз
(один пользователь, доверенная машина): лишняя ось версий Xray и
защита от входов, которые не возникают при ручном редактировании.
- Что:
- убрана поддержка Xray 25.12.8 — поддерживается только 26.3.27;
allowInsecure валидируется, но никогда не эмитится в JSON.
- удалена проверка владения сокетом через /proc/net/tcp; идентичность
процесса и сверка адрес/порт со state сохранены.
- валидация INI/профиля/адреса/query сведена к отказам на реальные
опечатки; scoped IPv6 отклоняется одинаково на Python 3.8 и 3.9+.
- удалены fsync/O_EXCL-церемония, SIGALRM-таймер, zombie-инспекция,
мёртвый отпечаток профиля и двойной разбор extra/alpn.
- shell.bash принимает вывод env без ре-валидации, сохранив атомарность;
спека и issue 01–05 приведены к новому объёму (тикет 06, леджер).
- Проверка:
- python3 -m unittest discover -s tests — 34 теста OK.
- bash tests/test_proxy_launchers.sh — 8 проверок PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
71e16a795a
commit
98bfd6338a
12 files changed
+384
-508
No files matched your search
@@ -68,7 +68,7 @@ assert_five_proxy_commands() {
|
||||
assert_eq ready "$output" ".bash_custom не подключил оба независимых модуля"
|
||||
) && ok "обычное подключение предоставляет proxy-команды и update_hproxy"
|
||||
|
||||
# proxy_on атомарно применяет ровно восемь переменных и не запрашивает IP.
|
||||
# proxy_on применяет восемь известных переменных и не запрашивает IP.
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
_proxy_cli() {
|
||||
@@ -87,16 +87,12 @@ assert_five_proxy_commands() {
|
||||
assert_eq "$ALL_PROXY" "$all_proxy" "all_proxy"
|
||||
assert_eq "proxy.test,localhost,127.0.0.1,::1,[::1],.example.test" "$NO_PROXY" "NO_PROXY"
|
||||
assert_eq "$NO_PROXY" "$no_proxy" "no_proxy"
|
||||
) && ok "proxy_on применяет полный восьмиключевой вывод без запроса IP"
|
||||
) && ok "proxy_on применяет известные ключи без запроса IP"
|
||||
|
||||
# Любой неверный машинный вывод оставляет прежнее окружение целиком.
|
||||
missing_output="$(valid_env | sed '/^no_proxy=/d')"
|
||||
# Неизвестная строка в машинном выводе оставляет прежнее окружение целиком.
|
||||
unknown_output="$(valid_env; printf 'EXTRA_PROXY=bad\n')"
|
||||
duplicate_output="$(valid_env; printf 'HTTP_PROXY=duplicate\n')"
|
||||
for bad_output in \
|
||||
"$missing_output" \
|
||||
"$unknown_output" \
|
||||
"$duplicate_output" \
|
||||
"BROKEN-LINE"; do
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
@@ -117,7 +113,7 @@ for bad_output in \
|
||||
assert_eq old-no-lower "$no_proxy" "no_proxy изменился при ошибке"
|
||||
) || fail "атомарность нарушена для неверного вывода"
|
||||
done
|
||||
ok "неверный env-вывод отклоняется без частичного изменения окружения"
|
||||
ok "неизвестная env-строка отклоняется без частичного изменения окружения"
|
||||
|
||||
# Даже полный stdout не применяется, если CLI завершился с ошибкой.
|
||||
(
|
||||
@@ -137,37 +133,6 @@ ok "неверный env-вывод отклоняется без частичн
|
||||
done
|
||||
) && ok "stdout неуспешного env не изменяет окружение"
|
||||
|
||||
# CRLF и NUL считаются повреждённым машинным выводом и не могут быть молча
|
||||
# нормализованы перед атомарной проверкой.
|
||||
for malformed_kind in crlf nul; do
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
export HTTP_PROXY=old HTTPS_PROXY=old ALL_PROXY=old NO_PROXY=old
|
||||
export http_proxy=old https_proxy=old all_proxy=old no_proxy=old
|
||||
_proxy_cli() {
|
||||
case "$malformed_kind" in
|
||||
crlf)
|
||||
while IFS= read -r line; do
|
||||
printf '%s\r\n' "$line"
|
||||
done < <(valid_env)
|
||||
;;
|
||||
nul)
|
||||
printf 'HTTP_PROXY=http://proxy.test:12\00034\n'
|
||||
valid_env | sed '/^HTTP_PROXY=/d'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
if proxy_on >/dev/null 2>&1; then
|
||||
fail "proxy_on принял $malformed_kind в env-выводе"
|
||||
fi
|
||||
for key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY \
|
||||
http_proxy https_proxy all_proxy no_proxy; do
|
||||
assert_eq old "${!key}" "$key изменился после $malformed_kind"
|
||||
done
|
||||
) || fail "атомарность нарушена для $malformed_kind"
|
||||
done
|
||||
ok "CRLF и NUL отклоняются без изменения окружения"
|
||||
|
||||
# Обёртки передают CLI команду, аргументы и код возврата без искажений.
|
||||
(
|
||||
source "$SHELL_MODULE"
|
||||
|
||||
+36
-66
@@ -4,7 +4,6 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -13,7 +12,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
CLI = REPO_ROOT / "xray-proxy/.local/bin/xray-proxy"
|
||||
|
||||
|
||||
def checking_socket_setup(outcome="success", close_delay=0.0):
|
||||
def checking_socket_setup(outcome="success"):
|
||||
if outcome == "success":
|
||||
outcome_line = " return Connection()\n"
|
||||
elif outcome == "refused":
|
||||
@@ -21,13 +20,11 @@ def checking_socket_setup(outcome="success", close_delay=0.0):
|
||||
else:
|
||||
raise ValueError("unknown socket outcome: {}".format(outcome))
|
||||
return (
|
||||
"import json, os, socket, time\n"
|
||||
"import json, os, socket\n"
|
||||
"expected_connection = json.loads(os.environ['TEST_EXPECTED_CONNECTION'])\n"
|
||||
"class Connection:\n"
|
||||
" def close(self):\n"
|
||||
" time.sleep("
|
||||
+ repr(close_delay)
|
||||
+ ")\n"
|
||||
" pass\n"
|
||||
"def checked_create_connection(endpoint, timeout):\n"
|
||||
" actual_connection = [endpoint[0], endpoint[1], timeout]\n"
|
||||
" if actual_connection != expected_connection:\n"
|
||||
@@ -226,36 +223,27 @@ class XrayProxyCliTest(unittest.TestCase):
|
||||
self.assertIn("HTTP_PROXY=http://[2001:db8::1]:23456\n", result.stdout)
|
||||
self.assertIn("ALL_PROXY=socks5://[2001:db8::1]:23456\n", result.stdout)
|
||||
|
||||
def test_preflight_timeout_bounds_delayed_dns_resolution(self):
|
||||
self.write_config(12334, address="slow-dns.example")
|
||||
def test_scoped_ipv6_is_rejected_when_ipaddress_lacks_scope_support(self):
|
||||
self.write_config(23456, address="fe80::1%eth0", no_proxy_extra="")
|
||||
setup = (
|
||||
"import socket, time; "
|
||||
"socket.getaddrinfo = lambda *args, **kwargs: time.sleep(5)"
|
||||
"import ipaddress\n"
|
||||
"real_ip_address = ipaddress.ip_address\n"
|
||||
"def python38_ip_address(value):\n"
|
||||
" if '%' in value:\n"
|
||||
" raise ValueError('scope IDs unsupported')\n"
|
||||
" return real_ip_address(value)\n"
|
||||
"ipaddress.ip_address = python38_ip_address\n"
|
||||
+ SUCCESSFUL_SOCKET_SETUP
|
||||
)
|
||||
started = time.monotonic()
|
||||
result = self.run_cli_with_socket_setup(
|
||||
("status",), setup, {"PROXY_PREFLIGHT_TIMEOUT": "0.1"}
|
||||
)
|
||||
elapsed = time.monotonic() - started
|
||||
|
||||
self.assertEqual(1, result.returncode)
|
||||
self.assertLess(elapsed, 1.0)
|
||||
self.assertIn("таймаут", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
|
||||
def test_preflight_timer_is_cancelled_before_closing_successful_connection(self):
|
||||
self.write_config(23456)
|
||||
setup = checking_socket_setup(close_delay=0.1)
|
||||
result = self.run_cli_with_socket_setup(
|
||||
("status",),
|
||||
("env",),
|
||||
setup,
|
||||
{"PROXY_PREFLIGHT_TIMEOUT": "0.01"},
|
||||
expected_connection=("127.0.0.1", 23456, 0.01),
|
||||
expected_connection=("fe80::1%eth0", 23456, 2.0),
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn("доступен", result.stdout)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertEqual("", result.stdout)
|
||||
self.assertIn("address", result.stderr)
|
||||
|
||||
def test_unreachable_env_prints_no_partial_output(self):
|
||||
self.write_config(23456)
|
||||
@@ -371,30 +359,13 @@ class XrayProxyCliTest(unittest.TestCase):
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("run -- COMMAND", result.stderr)
|
||||
|
||||
def test_invalid_preflight_timeout_is_rejected_by_status_env_and_run(self):
|
||||
def test_invalid_preflight_timeout_is_rejected_clearly(self):
|
||||
self.write_config(12334)
|
||||
for value in ("", "zero", "0", "-1", "nan", "inf"):
|
||||
for command in (("status",), ("env",), ("run", "--", "true")):
|
||||
with self.subTest(value=value, command=command):
|
||||
result = self.run_cli(
|
||||
*command, environment={"PROXY_PREFLIGHT_TIMEOUT": value}
|
||||
)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("положительным числом", result.stderr)
|
||||
if command == ("env",):
|
||||
self.assertEqual("", result.stdout)
|
||||
|
||||
def test_extreme_positive_timeouts_fail_without_traceback(self):
|
||||
self.write_config(12334)
|
||||
for value in ("1e-7", "1e308"):
|
||||
with self.subTest(value=value):
|
||||
result = self.run_cli(
|
||||
"status", environment={"PROXY_PREFLIGHT_TIMEOUT": value}
|
||||
)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertEqual("", result.stdout)
|
||||
self.assertIn("Ошибка:", result.stderr)
|
||||
self.assertNotIn("Traceback", result.stderr)
|
||||
result = self.run_cli(
|
||||
"status", environment={"PROXY_PREFLIGHT_TIMEOUT": "not-a-number"}
|
||||
)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("положительным числом", result.stderr)
|
||||
|
||||
def test_ini_rejects_structure_key_and_value_grammar_errors(self):
|
||||
valid = (
|
||||
@@ -407,13 +378,9 @@ class XrayProxyCliTest(unittest.TestCase):
|
||||
)
|
||||
invalid_configs = {
|
||||
"unknown section": valid + "[other]\nkey = value\n",
|
||||
"default section": "[DEFAULT]\nfoo = bar\n" + valid,
|
||||
"unknown key": valid + "extra = value\n",
|
||||
"wrong key case": valid.replace("type = external", "Type = external"),
|
||||
"duplicate key": valid + "port = 12335\n",
|
||||
"missing key": valid.replace("name = Test\n", ""),
|
||||
"multiline value": valid.replace("name = Test", "name = Test\n continued"),
|
||||
"nul value": valid.replace("name = Test", "name = Te\0st").encode("utf-8"),
|
||||
"empty name": valid.replace("name = Test", "name = "),
|
||||
"empty address": valid.replace("address = 127.0.0.1", "address = "),
|
||||
"port zero": valid.replace("port = 12334", "port = 0"),
|
||||
@@ -431,8 +398,17 @@ class XrayProxyCliTest(unittest.TestCase):
|
||||
self.assertEqual("", result.stdout)
|
||||
self.assertIn("Ошибка:", result.stderr)
|
||||
|
||||
def test_address_accepts_only_ip_literals_or_ascii_dns_names(self):
|
||||
valid_addresses = ("127.0.0.1", "2001:db8::1", "proxy-1.example")
|
||||
def test_address_accepts_ip_literals_and_simple_ascii_names(self):
|
||||
valid_addresses = (
|
||||
"127.0.0.1",
|
||||
"2001:db8::1",
|
||||
"proxy-1.example",
|
||||
".leading.example",
|
||||
"trailing.example.",
|
||||
"bad..example",
|
||||
"-bad.example",
|
||||
"bad-.example",
|
||||
)
|
||||
for address in valid_addresses:
|
||||
with self.subTest(address=address):
|
||||
self.write_config(12334, address=address)
|
||||
@@ -441,18 +417,12 @@ class XrayProxyCliTest(unittest.TestCase):
|
||||
|
||||
invalid_addresses = (
|
||||
"http://proxy.example",
|
||||
"proxy.example:12334",
|
||||
"[::1]",
|
||||
"fe80::1%eth0",
|
||||
"has space",
|
||||
"proxy/path",
|
||||
"user@proxy",
|
||||
".leading.example",
|
||||
"trailing.example.",
|
||||
"bad..example",
|
||||
"-bad.example",
|
||||
"bad-.example",
|
||||
"прокси.example",
|
||||
"fe80::1%eth0",
|
||||
)
|
||||
for address in invalid_addresses:
|
||||
with self.subTest(address=address):
|
||||
|
||||
@@ -6,7 +6,6 @@ import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -85,8 +84,7 @@ record("run", config=config)
|
||||
inbound = config["inbounds"][0]
|
||||
server = socket.socket()
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listen_port = int(os.environ.get("FAKE_XRAY_LISTEN_PORT", inbound["port"]))
|
||||
server.bind((inbound["listen"], listen_port))
|
||||
server.bind((inbound["listen"], inbound["port"]))
|
||||
server.listen(16)
|
||||
server.settimeout(0.1)
|
||||
stopping = False
|
||||
@@ -223,14 +221,10 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
"#Synthetic".format(UUID, PBK, SID, extra)
|
||||
)
|
||||
|
||||
def xhttp_profile(self, extra="", modern=True):
|
||||
def xhttp_profile(self, extra=""):
|
||||
opaque = quote('{"downloadSettings":{"address":"127.0.0.1"}}', safe="")
|
||||
modern_fields = (
|
||||
"&pcs={}%2C{}&vcn=name-a.example%2Cname-b.example".format(
|
||||
PCS_A, PCS_B
|
||||
)
|
||||
if modern
|
||||
else ""
|
||||
modern_fields = "&pcs={}%2C{}&vcn=name-a.example%2Cname-b.example".format(
|
||||
PCS_A, PCS_B
|
||||
)
|
||||
return (
|
||||
"vless://{}@cdn.example:8443?type=xhttp&security=tls&encryption=none&"
|
||||
@@ -270,8 +264,7 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
return []
|
||||
return [json.loads(line) for line in self.log.read_text().splitlines()]
|
||||
|
||||
def start(self, profile=None, version="26.3.27"):
|
||||
self.environment["FAKE_XRAY_VERSION"] = "Xray {} (fake)".format(version)
|
||||
def start(self, profile=None):
|
||||
args = [str(CLI), "start"]
|
||||
if profile is not None:
|
||||
args.extend(["-f", str(profile)])
|
||||
@@ -297,12 +290,12 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
for secret in (profile, UUID, PBK, SID, PATH_SECRET):
|
||||
self.assertNotIn(secret, text)
|
||||
|
||||
def test_complete_managed_lifecycle_for_both_versions_and_profile_locations(self):
|
||||
profiles = (("26.3.27", self.xhttp_profile(), False), ("25.12.8", self.tcp_profile(), True))
|
||||
for version, profile_text, explicit in profiles:
|
||||
with self.subTest(version=version):
|
||||
def test_complete_managed_lifecycle_for_both_profile_locations(self):
|
||||
profiles = ((self.xhttp_profile(), False), (self.tcp_profile(), True))
|
||||
for profile_text, explicit in profiles:
|
||||
with self.subTest(explicit=explicit):
|
||||
profile = self.write_profile(profile_text, explicit=explicit)
|
||||
process = self.start(profile if explicit else None, version)
|
||||
process = self.start(profile if explicit else None)
|
||||
|
||||
status = self.run_cli("status")
|
||||
env = self.run_cli("env")
|
||||
@@ -318,7 +311,9 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
self.assertEqual(0, env.returncode, env.stderr)
|
||||
self.assertIn("HTTP_PROXY=http://127.0.0.1:{}".format(self.port), env.stdout)
|
||||
self.assertEqual(0, child.returncode, child.stderr)
|
||||
self.assertIn(str(self.xray.resolve()), process.stdout.readline())
|
||||
version_summary = process.stdout.readline()
|
||||
self.assertIn(str(self.xray.resolve()), version_summary)
|
||||
self.assertIn("26.3.27", version_summary)
|
||||
|
||||
with socket.create_connection(("127.0.0.1", self.port), timeout=1) as http:
|
||||
http.sendall(b"GET http://example.test/ HTTP/1.1\r\nHost: example.test\r\n\r\n")
|
||||
@@ -386,7 +381,17 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
self.assertEqual(str(self.xray.resolve()), state["xray_path"])
|
||||
self.assertEqual("127.0.0.1", state["address"])
|
||||
self.assertEqual(self.port, state["port"])
|
||||
self.assertEqual(64, len(state["profile_fingerprint"]))
|
||||
self.assertEqual(
|
||||
{
|
||||
"supervisor_pid",
|
||||
"xray_pid",
|
||||
"xray_path",
|
||||
"config_path",
|
||||
"address",
|
||||
"port",
|
||||
},
|
||||
set(state),
|
||||
)
|
||||
state_text = state_path.read_text(encoding="utf-8")
|
||||
self.assert_no_secrets(state_text, profile_text)
|
||||
stream = config["outbounds"][0]["streamSettings"]
|
||||
@@ -410,22 +415,9 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
argv_text = "\0".join(entry["argv"])
|
||||
self.assert_no_secrets(argv_text, profile_text)
|
||||
|
||||
def test_old_version_preserves_explicit_legacy_insecure(self):
|
||||
profile_text = self.xhttp_profile(
|
||||
"&allowInsecure=true&insecure=true", modern=False
|
||||
)
|
||||
self.write_profile(profile_text)
|
||||
process = self.start(version="25.12.8")
|
||||
stdout, stderr = self.stop_and_collect(process)
|
||||
config = next(entry["config"] for entry in self.log_entries() if entry["kind"] == "test")
|
||||
outbound = config["outbounds"][0]
|
||||
self.assertEqual("xhttp", outbound["streamSettings"]["network"])
|
||||
self.assertTrue(outbound["streamSettings"]["tlsSettings"]["allowInsecure"])
|
||||
self.assertIn("Предупреждение", stderr)
|
||||
self.assert_no_secrets(stdout + stderr, profile_text)
|
||||
|
||||
def test_versions_and_profile_failures_stop_before_test_or_run_without_leaks(self):
|
||||
invalid_versions = (
|
||||
"Xray 25.12.8",
|
||||
"Xray 26.3.270",
|
||||
"junk Xray 26.3.27",
|
||||
"Xray 26.5.9",
|
||||
@@ -456,6 +448,7 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
self.tcp_profile("&headerType=http"),
|
||||
self.tcp_profile("&flow=other"),
|
||||
self.tcp_profile("&sni=bad%0Aline"),
|
||||
self.tcp_profile().replace("sni=front.example", "sni=bad%FF"),
|
||||
self.tcp_profile().replace("@edge.example:443?", "@edge.example:443/path?"),
|
||||
self.tcp_profile().replace("type=tcp", "type=grpc"),
|
||||
self.tcp_profile().replace("&pbk={}".format(PBK), ""),
|
||||
@@ -496,22 +489,17 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
self.write_machine_config("xray")
|
||||
|
||||
cases = (
|
||||
("26.3.27", "", False, False),
|
||||
("26.3.27", "&allowInsecure=false", False, False),
|
||||
("26.3.27", "&allowInsecure=false&insecure=false", False, False),
|
||||
("26.3.27", "&insecure=true", True, False),
|
||||
("26.3.27", "&allowInsecure=true&insecure=true", True, False),
|
||||
("25.12.8", "", False, False),
|
||||
("25.12.8", "&insecure=false", False, False),
|
||||
("25.12.8", "&allowInsecure=false&insecure=false", False, False),
|
||||
("25.12.8", "&allowInsecure=true", True, True),
|
||||
("25.12.8", "&allowInsecure=true&insecure=true", True, True),
|
||||
("", False),
|
||||
("&allowInsecure=false", False),
|
||||
("&allowInsecure=false&insecure=false", False),
|
||||
("&insecure=true", True),
|
||||
("&allowInsecure=true&insecure=true", True),
|
||||
)
|
||||
for version, fields, warns, emitted in cases:
|
||||
with self.subTest(version=version, fields=fields):
|
||||
profile_text = self.xhttp_profile(fields, modern=version == "26.3.27")
|
||||
for fields, warns in cases:
|
||||
with self.subTest(fields=fields):
|
||||
profile_text = self.xhttp_profile(fields)
|
||||
self.write_profile(profile_text)
|
||||
process = self.start(version=version)
|
||||
process = self.start()
|
||||
stdout, stderr = self.stop_and_collect(process)
|
||||
config = next(
|
||||
entry["config"]
|
||||
@@ -519,7 +507,7 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
if entry["kind"] == "test"
|
||||
)
|
||||
tls = config["outbounds"][0]["streamSettings"]["tlsSettings"]
|
||||
self.assertEqual(emitted, tls.get("allowInsecure", False))
|
||||
self.assertNotIn("allowInsecure", tls)
|
||||
self.assertEqual(warns, "Предупреждение" in stderr)
|
||||
self.assert_no_secrets(stdout + stderr, profile_text)
|
||||
self.log.unlink()
|
||||
@@ -561,15 +549,6 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
self.assert_no_secrets(result.stdout + result.stderr, profile_text)
|
||||
self.log.unlink()
|
||||
|
||||
old_with_modern = self.xhttp_profile()
|
||||
self.write_profile(old_with_modern)
|
||||
result = self.run_cli(
|
||||
"start", environment={"FAKE_XRAY_VERSION": "Xray 25.12.8 (fake)"}
|
||||
)
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn("pcs/vcn", result.stderr)
|
||||
self.assertEqual(["version"], [entry["kind"] for entry in self.log_entries()])
|
||||
|
||||
def test_fake_run_test_rejects_non_json_constants(self):
|
||||
for constant in ("NaN", "Infinity", "-Infinity"):
|
||||
with self.subTest(constant=constant):
|
||||
@@ -686,7 +665,6 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
"port": self.port,
|
||||
"address": "127.0.0.1",
|
||||
"config_path": str(state_dir / "client.json"),
|
||||
"profile_fingerprint": "0" * 64,
|
||||
}
|
||||
(state_dir / "state.json").write_text(json.dumps(state), encoding="utf-8")
|
||||
(state_dir / "state.json").chmod(0o600)
|
||||
@@ -736,75 +714,6 @@ class ManagedXrayCliTest(unittest.TestCase):
|
||||
pass
|
||||
listener.close()
|
||||
|
||||
def test_verified_child_must_own_configured_listener(self):
|
||||
profile_text = self.tcp_profile()
|
||||
self.write_profile(profile_text)
|
||||
wrong_port = self.free_port()
|
||||
listener = socket.socket()
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind(("127.0.0.1", self.port))
|
||||
listener.listen(16)
|
||||
listener.settimeout(0.05)
|
||||
serving = threading.Event()
|
||||
serving.set()
|
||||
|
||||
def serve_foreign():
|
||||
while serving.is_set():
|
||||
try:
|
||||
client, _address = listener.accept()
|
||||
except socket.timeout:
|
||||
continue
|
||||
try:
|
||||
client.settimeout(0.2)
|
||||
request = client.recv(8192)
|
||||
if b"ifconfig.me" in request:
|
||||
body = b"203.0.113.88\n"
|
||||
client.sendall(
|
||||
b"HTTP/1.1 200 OK\r\nContent-Length: "
|
||||
+ str(len(body)).encode("ascii")
|
||||
+ b"\r\nConnection: close\r\n\r\n"
|
||||
+ body
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
thread = threading.Thread(target=serve_foreign, daemon=True)
|
||||
thread.start()
|
||||
process = subprocess.Popen(
|
||||
[str(CLI), "start"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=dict(self.environment, FAKE_XRAY_LISTEN_PORT=str(wrong_port)),
|
||||
)
|
||||
state_path = self.state_home / "xray-proxy/state.json"
|
||||
deadline = time.monotonic() + 3
|
||||
while time.monotonic() < deadline and not state_path.exists():
|
||||
if process.poll() is not None:
|
||||
break
|
||||
time.sleep(0.02)
|
||||
marker = self.root / "foreign-listener-child-ran"
|
||||
results = [
|
||||
self.run_cli("status"),
|
||||
self.run_cli("env"),
|
||||
self.run_cli("run", "--", "touch", str(marker)),
|
||||
self.run_cli("stop"),
|
||||
]
|
||||
stdout, stderr = process.communicate(timeout=4)
|
||||
serving.clear()
|
||||
thread.join(timeout=1)
|
||||
self.assertTrue(all(result.returncode != 0 for result in results))
|
||||
self.assertTrue(all("listener" in result.stderr for result in results))
|
||||
self.assertFalse(marker.exists())
|
||||
self.assertNotEqual(0, process.returncode)
|
||||
self.assertIn("listener", stderr)
|
||||
self.assert_no_secrets(stdout + stderr, profile_text)
|
||||
with socket.create_connection(("127.0.0.1", self.port), timeout=1):
|
||||
pass
|
||||
listener.close()
|
||||
|
||||
def test_supervisor_forwards_original_signal_to_child(self):
|
||||
for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
|
||||
with self.subTest(signum=signum):
|
||||
|
||||
Reference in new issue
Block a user