zapret-kvn/tests/test_proxy_readiness.py
loop-uh c624a581ac
Some checks failed
Windows project source guards / test (push) Has been cancelled
Подключение и переключения быстрее: без лишних ожиданий и PowerShell
- Ожидание освобождения TUN-адаптера ждало любой адаптер xftun*, а
  Wintun убитого ядра остаётся в системе выключенным: после первой же
  TUN-сессии каждое отключение и переключение в TUN стояло полные 10 с.
  Теперь ждём только адаптер своей сессии, пока он не перестанет быть Up.
- sing-box останавливается сразу kill(): terminate() консольный sing-box
  на Windows не обрабатывает, 500 мс «мягкой» остановки истекали всегда.
- «Осиротевшие» ядра и winws2 ищутся снимком WinAPI вместо PowerShell+WMI
  (~0,2 с на каждый запуск ядра, у Zapret — дважды); PowerShell — запасной.
- Готовность портов ядер, relay и Clash API сначала смотрится по таблице
  TCP: подключение к ещё закрытому порту Windows держит повторами SYN, и
  ранняя проба всегда выжидала свой таймаут (~0,35–0,5 с).
- sing-box check — только после неудачного запуска, ради понятной ошибки:
  параллельно с работающим ядром она для TUN-конфига мешает туннелю.
- cryptography грузится только при пароле данных и ссылках Happ.

Проверка готовности TUN оставлена прежней: при более ранней готовности
закрепление сервера через Clash API в первые ~0,4 с ломало TCP туннеля
в «родном» режиме sing-box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:41:18 +03:00

164 lines
6.9 KiB
Python

from __future__ import annotations
import unittest
from unittest.mock import Mock, patch
import struct
from xray_fluent.network import proxy_readiness
from xray_fluent.network.proxy_readiness import (
listening_ports_from_table,
ports_listening,
probe_http_proxy_listener,
probe_listener_role,
probe_socks5_listener,
)
def _client(response: bytes) -> Mock:
client = Mock()
client.__enter__ = Mock(return_value=client)
client.__exit__ = Mock(return_value=False)
client.recv.return_value = response
return client
class ProxyReadinessTests(unittest.TestCase):
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_socks_probe_requires_socks5_no_auth_reply(self, connect: Mock) -> None:
client = _client(b"\x05\x00")
connect.return_value = client
self.assertTrue(probe_socks5_listener(1390))
client.sendall.assert_called_once_with(b"\x05\x01\x00")
connect.assert_called_once_with(("127.0.0.1", 1390), timeout=0.35)
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_socks_probe_rejects_unrelated_tcp_listener(self, connect: Mock) -> None:
connect.return_value = _client(b"HTTP/1.1 400")
self.assertFalse(probe_socks5_listener(1390))
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_http_probe_requires_http_status_line(self, connect: Mock) -> None:
client = _client(b"HTTP/1.1 400 Bad Request")
connect.return_value = client
self.assertTrue(probe_http_proxy_listener(1391))
request = client.sendall.call_args.args[0]
self.assertTrue(request.startswith(b"OPTIONS * HTTP/1.1\r\n"))
@patch("xray_fluent.network.proxy_readiness.probe_socks5_listener", return_value=True)
@patch("xray_fluent.network.proxy_readiness.probe_http_proxy_listener", return_value=True)
def test_role_dispatch_is_protocol_specific(self, http_probe: Mock, socks_probe: Mock) -> None:
self.assertTrue(probe_listener_role(1390, "SOCKS"))
self.assertTrue(probe_listener_role(1391, "HTTP"))
socks_probe.assert_called_once_with(1390)
http_probe.assert_called_once_with(1391)
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_socks_probe_rejects_listener_without_usable_auth(self, connect: Mock) -> None:
# A listening SOCKS server is not ready when it rejects admission.
client = _client(b"\x05\xff")
connect.return_value = client
self.assertFalse(probe_socks5_listener(11808))
client.sendall.assert_called_once_with(b"\x05\x01\x00")
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_socks_probe_completes_password_subnegotiation(self, connect: Mock) -> None:
client = _client(b"")
client.recv.side_effect = [b"\x05\x02", b"\x01\x00"]
connect.return_value = client
self.assertTrue(
probe_socks5_listener(11808, username="sidecar-a1b2c3", password="secret")
)
greeting = client.sendall.call_args_list[0].args[0]
self.assertEqual(b"\x05\x01\x02", greeting)
auth_frame = client.sendall.call_args_list[1].args[0]
self.assertEqual(
b"\x01" + bytes((len(b"sidecar-a1b2c3"),)) + b"sidecar-a1b2c3"
+ bytes((len(b"secret"),)) + b"secret",
auth_frame,
)
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_socks_probe_rejects_failed_password_auth(self, connect: Mock) -> None:
client = _client(b"")
client.recv.side_effect = [b"\x05\x02", b"\x01\x01"]
connect.return_value = client
self.assertFalse(probe_socks5_listener(11808, username="user", password="bad"))
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_socks_probe_rejects_auth_downgrade(self, connect: Mock) -> None:
client = _client(b"\x05\x00")
connect.return_value = client
self.assertFalse(probe_socks5_listener(11808, username="user", password="pass"))
client.sendall.assert_called_once_with(b"\x05\x01\x02")
@patch("xray_fluent.network.proxy_readiness.socket.create_connection")
def test_socks_probe_rejects_unoffered_method_choice(self, connect: Mock) -> None:
connect.return_value = _client(b"\x05\x02")
self.assertFalse(probe_socks5_listener(11808))
@patch("xray_fluent.network.proxy_readiness.probe_socks5_listener", return_value=True)
def test_role_dispatch_forwards_credentials_only_to_socks(self, socks_probe: Mock) -> None:
self.assertTrue(
probe_listener_role(11808, "SOCKS", username="user", password="pass")
)
socks_probe.assert_called_once_with(11808, username="user", password="pass")
@patch("xray_fluent.network.proxy_readiness.probe_tcp_listener", return_value=True)
def test_role_dispatch_ignores_credentials_for_tcp_roles(self, tcp_probe: Mock) -> None:
self.assertTrue(probe_listener_role(19085, "API", username="user", password="pass"))
tcp_probe.assert_called_once_with(19085)
if __name__ == "__main__":
unittest.main()
def _row4(port: int) -> bytes:
# MIB_TCPROW_OWNER_PID: state, local addr, local port (сетевой порядок), ...
return struct.pack("<II", 2, 0) + struct.pack(">H", port) + b"\0\0" + struct.pack("<III", 0, 0, 1234)
def _row6(port: int) -> bytes:
# MIB_TCP6ROW_OWNER_PID: addr[16], scope, local port, raddr[16], scope, rport, state, pid
return (
b"\0" * 16 + struct.pack("<I", 0) + struct.pack(">H", port) + b"\0\0"
+ b"\0" * 16 + struct.pack("<IIII", 0, 0, 2, 1234)
)
class ListeningTableTests(unittest.TestCase):
def test_ipv4_and_ipv6_rows_decode_network_order_ports(self) -> None:
v4 = struct.pack("<I", 2) + _row4(1391) + _row4(10808)
v6 = struct.pack("<I", 1) + _row6(19090)
self.assertEqual(listening_ports_from_table(v4, 2), {1391, 10808})
self.assertEqual(listening_ports_from_table(v6, 23), {19090})
def test_ports_listening_answers_from_table_without_connecting(self) -> None:
tables = {2: {1391}, 23: {1390}}
with patch.object(proxy_readiness.os, "name", "nt"), patch.object(
proxy_readiness, "_listening_ports_of_family", side_effect=lambda family: tables[family]
), patch("xray_fluent.network.proxy_readiness.socket.create_connection") as connect:
self.assertTrue(ports_listening({1391: "HTTP", 1390: "SOCKS"}))
self.assertFalse(ports_listening((1391, 4000)))
connect.assert_not_called()
def test_ports_listening_unknown_when_table_fails(self) -> None:
with patch.object(proxy_readiness.os, "name", "nt"), patch.object(
proxy_readiness, "_listening_ports_of_family", side_effect=OSError(5, "denied")
):
self.assertIsNone(ports_listening((1391,)))