zapret-kvn/tests/test_process_traffic_collector_bounded.py
loop-uh 92d61d57d9 Статистика процессов в TUN: проксируемый трафик больше не «напрямую»
Clash API sing-box отдаёт chains от листа к корню (common.Reverse в
trafficcontrol/tracker.go): у гибрида с xray-сайдкаром и у пула первым
идёт тег узла (__app_hybrid_relay_a), а proxy — последним. Сборщик
смотрел только chains[0], поэтому в TUN каждое соединение через VLESS/
VMess/Trojan показывалось как «напрямую», хотя шло через сервер.

Колонка «маршрут» теперь использует ту же классификацию по всей цепочке,
что и спрос на туннель (is_proxied_chain в proxy_demand).

Проверено e2e на win10: план v0.7.7 (sing-box TUN + xray-сайдкар) и
VLESS-сервер. curl ушёл через сервер (сервер получил google/example/
cloudflare), Clash API вернул chains ['__app_hybrid_relay_a', 'proxy'],
старый сборщик показал curl.exe route=direct.
2026-09-26 23:09:17 +03:00

78 lines
3.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Сбор трафика по процессам: состояние сессии ограничено живыми соединениями."""
from __future__ import annotations
import io
import json
import unittest
from unittest import mock
from xray_fluent.platform.windows import process_traffic_collector as collector
def _conn(cid: str, exe: str, up: int, down: int) -> dict:
return {
"id": cid, "upload": up, "download": down, "chains": ["proxy"],
"metadata": {"processPath": f"C:/Apps/{exe}", "host": "example.com"},
}
class _Response(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def _poll(connections: list[dict]):
payload = json.dumps({"connections": connections}).encode()
with mock.patch.object(collector.urllib.request, "urlopen", return_value=_Response(payload)):
return {s.exe.lower(): s for s in collector.collect_process_stats()}
class BoundedCollectorTests(unittest.TestCase):
def setUp(self) -> None:
collector.reset_connection_tracking()
def tearDown(self) -> None:
collector.reset_connection_tracking()
def test_closed_connections_leave_state_but_keep_bytes_and_count(self) -> None:
_poll([_conn("a", "chrome.exe", 10, 100), _conn("b", "chrome.exe", 5, 50)])
self.assertEqual(len(collector._conn_owner), 2)
stats = _poll([_conn("b", "chrome.exe", 5, 60)])
# «a» закрылось: его байты перешли в закрытые, а запись владельца удалена.
self.assertEqual(set(collector._conn_owner), {"b"})
self.assertEqual(stats["chrome.exe"].download, 160)
stats = _poll([])
self.assertEqual(collector._conn_owner, {})
self.assertEqual(stats, {})
stats = _poll([_conn("c", "chrome.exe", 1, 1)])
self.assertEqual(stats["chrome.exe"].download, 161)
def test_state_does_not_grow_with_connection_churn(self) -> None:
for batch in range(200):
_poll([_conn(f"{batch}-{i}", "app.exe", 1, 1) for i in range(20)])
self.assertLessEqual(len(collector._conn_owner), 20)
self.assertLessEqual(len(collector._conn_bytes), 20)
self.assertEqual(collector._conn_total["app.exe"], 200 * 20)
def test_route_follows_whole_leaf_first_chain(self) -> None:
# Форма цепочек снята с живого sing-box TUN (гибрид с xray-сайдкаром):
# Clash API идёт от листа к корню, поэтому ``proxy`` стоит последним.
tunneled = _conn("a", "curl.exe", 10, 600)
tunneled["chains"] = ["__app_hybrid_relay_a", "proxy"]
direct = _conn("b", "yandexmusic.exe", 5, 50)
direct["chains"] = ["direct"]
stats = _poll([tunneled, direct])
self.assertEqual(stats["curl.exe"].route, "proxy")
self.assertEqual(stats["curl.exe"].proxy_bytes, 610)
self.assertEqual(stats["curl.exe"].direct_bytes, 0)
self.assertEqual(stats["yandexmusic.exe"].route, "direct")
self.assertEqual(stats["yandexmusic.exe"].direct_bytes, 55)
if __name__ == "__main__":
unittest.main()