Add VLESS proxy latency tester with Xray-core approach#1
Add VLESS proxy latency tester with Xray-core approach#1
Conversation
Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
|
@copilot میخوام به جای اینکه بصورت آرگومان دیتا ها رو بهش بدم، توی خود اسکریپت بتونم عوضش کنم، یا مثلا برای tcp ping و full ping ی منو داشته باشه که بین این ۲ تا انتخاب کنم |
Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
Done in commit 1. USER SETTINGS block at the top of INPUT_FILE = "configs.txt" # your VLESS config file
OUTPUT_FILE = "sorted_configs.txt" # where sorted results go
TIMEOUT = 5 # ping timeout in seconds
CONCURRENCY = 10 # simultaneous pings
XRAY_PATH = "xray" # path to xray binary (Xray mode only)2. Interactive menu — run the script with no arguments and you get a mode-selection prompt: After choosing, the active settings are printed before pinging starts. CLI arguments still work as before for scripted/automated use. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot ی فایل راهنما برام بنویس که منظور از xray binary چیه و از کجا باید بیارمش |
Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
فایل راهنما در
|
|
@copilot این اسکریپته که زدی وقتی فقط tcp میگیره مثلا پینگ ۲۳ میده ولی وقتی کانفیگ رو توی برنامه وارد میکنم کار نمیکنه |
There was a problem hiding this comment.
Pull request overview
This PR introduces a Python utility script for testing VLESS proxy configurations by measuring their latency. The tool provides two ping modes: a fast TCP connection test and a more accurate full round-trip test through the Xray proxy binary. The implementation is clean and well-tested, with comprehensive unit tests covering URI parsing, configuration generation, and file I/O operations.
Changes:
- New
xray_ping.pyscript with TCP and Xray ping modes for VLESS configs - Comprehensive test suite covering parsing, config building, and CLI
- Documentation files (README, XRAY_SETUP guide in Persian) and example configs
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| xray_ping.py | Main script implementing VLESS URI parsing, dual-mode latency testing (TCP/Xray), SOCKS5 client, concurrent execution, and interactive CLI |
| tests/test_xray_ping.py | Unit tests covering URI parsing, stream settings generation, config building, file I/O, and CLI error handling |
| README.md | Usage documentation with feature overview, installation steps, mode comparison, and examples |
| XRAY_SETUP.md | Persian-language guide for downloading and configuring the Xray binary |
| configs_example.txt | Sample VLESS configuration file with WS/TLS/TCP examples |
| .gitignore | Standard Python exclusions plus generated output file |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| TEST_HOST, TEST_PORT, TEST_PATH, | ||
| timeout, | ||
| ) | ||
| except Exception: |
There was a problem hiding this comment.
The broad exception handler swallows all exceptions including KeyboardInterrupt and SystemExit, which prevents graceful shutdown. This could leave orphaned Xray processes running if the user tries to interrupt the script. Consider catching only expected exceptions (e.g., socket.error, OSError, ConnectionError) and allowing system exceptions to propagate after cleanup.
| except Exception: | |
| except Exception as exc: | |
| if isinstance(exc, (KeyboardInterrupt, SystemExit)): | |
| raise |
| def _free_port() -> int: | ||
| """Return an available local TCP port.""" | ||
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| s.bind(("127.0.0.1", 0)) | ||
| return s.getsockname()[1] |
There was a problem hiding this comment.
There's a race condition between finding a free port and using it. The socket is closed after getting the port number, so another process could bind to that port before Xray starts. While this is unlikely in practice with ephemeral ports, it could cause intermittent failures. Consider keeping the socket bound and passing it to the subprocess, or implementing retry logic if the port is already in use.
| # Give Xray a moment to bind the inbound port | ||
| time.sleep(XRAY_STARTUP_DELAY) |
There was a problem hiding this comment.
The hardcoded startup delay of 0.8 seconds may be too short for slow systems or too long for fast ones. While checking if the process exited (line 267) helps catch immediate failures, a short delay could cause a race condition where Xray hasn't bound the port yet, leading to connection refused errors. Consider implementing a retry loop that attempts to connect to the local port with exponential backoff, which would be both faster on success and more reliable on slow systems.
| http_req = ( | ||
| f"GET {path} HTTP/1.1\r\n" | ||
| f"Host: {dest_host}\r\n" | ||
| f"Connection: close\r\n\r\n" |
There was a problem hiding this comment.
The hostname from the VLESS URI is used directly in the HTTP Host header without sanitization. While HTTP headers typically handle this safely, a malicious hostname containing newline characters (\r\n) could potentially inject additional HTTP headers or even split the request. Although urllib.parse.urlparse should reject such hostnames, consider explicitly validating the hostname to prevent header injection attacks.
| timeout = args.timeout if args.timeout is not None else TIMEOUT | ||
| concurrency = args.concurrency if args.concurrency is not None else CONCURRENCY | ||
| xray_path = args.xray_path if args.xray_path is not None else XRAY_PATH | ||
|
|
There was a problem hiding this comment.
There's no validation that timeout and concurrency are positive values. A user could pass --timeout -5 or --concurrency 0, which would cause unexpected behavior or errors. Add validation to ensure timeout > 0 and concurrency >= 1 after parsing arguments.
| # Validate timeout and concurrency | |
| if timeout <= 0: | |
| print(f"Error: timeout must be a positive value (got {timeout}).", file=sys.stderr) | |
| return 1 | |
| if concurrency < 1: | |
| print(f"Error: concurrency must be at least 1 (got {concurrency}).", file=sys.stderr) | |
| return 1 |
| sock = socket.create_connection((proxy_host, proxy_port), timeout=timeout) | ||
| sock.settimeout(timeout) |
There was a problem hiding this comment.
The socket created here is not closed in the exception path. If an exception occurs before the finally block at line 238 (e.g., during SOCKS5 greeting or CONNECT), the socket will leak. Consider wrapping the socket creation in a context manager or moving the try-finally to immediately after socket creation to ensure cleanup in all cases.
| third-party libraries (RFC 1928). | ||
| """ | ||
| sock = socket.create_connection((proxy_host, proxy_port), timeout=timeout) | ||
| sock.settimeout(timeout) |
There was a problem hiding this comment.
The timeout set here applies to the entire socket lifetime, but it's already passed to socket.create_connection above. The settimeout call is redundant. Additionally, the timeout is used for both the SOCKS5 handshake and the HTTP request, meaning a slow handshake could leave very little time for the actual HTTP request that measures latency. Consider breaking this into separate timeouts or removing the redundant call.
| sock.settimeout(timeout) |
|
|
||
| start = time.perf_counter() | ||
| sock.sendall(http_req) | ||
| response = sock.recv(64) |
There was a problem hiding this comment.
The recv buffer size is very small (64 bytes). While this may be sufficient to read the HTTP status line, if the server sends headers or the response is larger, subsequent data will be left in the socket buffer. Although the socket is closed immediately after, this could cause issues if testing against certain servers. Consider using a larger buffer size (e.g., 1024 or 4096 bytes) to ensure the full response is consumed.
| response = sock.recv(64) | |
| response = sock.recv(4096) |
| start = time.perf_counter() | ||
| sock.sendall(http_req) | ||
| response = sock.recv(64) | ||
| elapsed = time.perf_counter() - start |
There was a problem hiding this comment.
The latency measurement starts after the SOCKS5 handshake completes (line 229), so it only captures the HTTP request/response time through the already-established proxy tunnel. This is likely intentional since the SOCKS5 connection is to localhost, but the documentation should clarify that "round-trip latency" means the HTTP RTT through the proxy, not the total time including SOCKS5 setup. Consider updating the function docstring and README to be explicit about what is being measured.
| Return True when the script was invoked with no arguments, meaning we | ||
| should show the interactive mode-selection menu. | ||
|
|
||
| argv=None → main() was called without an explicit argument list, so the | ||
| real sys.argv is used; interactive when sys.argv has no extras. | ||
| argv=[] → explicitly empty list passed (used in tests to simulate this). | ||
| """ | ||
| if argv is None: | ||
| return len(sys.argv) == 1 | ||
| return argv == [] |
There was a problem hiding this comment.
The interactive mode detection logic has a subtle issue. When argv=None (the default), sys.argv is used by argparse, which includes the script name as element 0. However, the check len(sys.argv) == 1 correctly detects no arguments. When argv=[] is passed explicitly (as in tests), this works correctly. The issue is that the function comment mentions argv=[] is used in tests, but this creates inconsistent behavior: main() with no args uses sys.argv, but main([]) uses an empty list. This works but could be clearer. Consider documenting this more explicitly or normalizing the behavior.
| Return True when the script was invoked with no arguments, meaning we | |
| should show the interactive mode-selection menu. | |
| argv=None → main() was called without an explicit argument list, so the | |
| real sys.argv is used; interactive when sys.argv has no extras. | |
| argv=[] → explicitly empty list passed (used in tests to simulate this). | |
| """ | |
| if argv is None: | |
| return len(sys.argv) == 1 | |
| return argv == [] | |
| Return True when the script was invoked with no user arguments, meaning we | |
| should show the interactive mode-selection menu. | |
| This function mirrors how argparse treats argv: | |
| - argv is None → argparse will use sys.argv[1:] as the argument list. | |
| - argv is a list → it is treated as the argument list directly, without | |
| the script name. | |
| In both cases, "interactive mode" means that the effective argument list | |
| (excluding the script name) is empty. | |
| """ | |
| if argv is None: | |
| args_list = sys.argv[1:] | |
| else: | |
| args_list = argv | |
| return len(args_list) == 0 |
|
@copilot apply changes based on the comments in this thread |
…detection Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
مشکل اصلی اینه که TCP ping فقط چک میکنه پورت سرور باز هست یا نه — این ربطی به صحت کانفیگ VLESS نداره. سرور میتونه روی TCP جواب بده ولی کانفیگ رو رد کنه (UUID اشتباه، path غلط و ...). سورس Xray-core رو بررسی کردم و چند باگ مهم پیدا کردم که در commit باگهای فیکس شده بر اساس سورس Xray-core:
توصیه: برای تست واقعی حتماً از Full Xray ping استفاده کن — TCP ping فقط نشون میده سرور onlineـه، نه اینکه کانفیگ کار میکنه. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…ection fix Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
All review comments applied in commit
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot گلم مطمئنی که اون بخش full ping رو درست نوشتی؟ چون من بهش xray binary رو دادم، بدون استثنا همه رو تایم اوت میکنه |
…imeout root cause Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
بله، مشکل پیدا شد. وقتی فایل رو از طریق GitHub آپلود کردی، نسخه قدیمی جایگزین شد و همه اصلاحاتم پاک شدند. دلیل اصلی all-timeout: کد قدیمی از در commit
توجه: لطفاً فایل Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot ببین گلم من از https://github.com/XTLS/Xray-core/releases/download/v26.2.6/Xray-android-arm64-v8a.zip استفاده میکنم فایل xray رو از توش استخراج کردم و گذاشتم بغل اسکریپت منتها بازم همه رو timeout میده |
Co-authored-by: CertMusashi <95465931+CertMusashi@users.noreply.github.com>
مشکل پیدا شد — ۱. تشخیص خودکار binary کنار اسکریپت ۲. بررسی binary قبل از شروع ping
اگه مشکلی باشه، پیام خطا با راهحل نشون داده میشه: ۳. فلگ python xray_ping.py configs.txt --mode xray -vراهحل فوری برای Android: chmod +x ./xrayو در بالای XRAY_PATH = "./xray"Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Script to batch-ping VLESS configs from a file and output them sorted by latency, using either raw TCP or a real Xray proxy round-trip.
Core script —
xray_ping.pyTwo ping modes:
tcp— TCP connect time tohost:port. Zero external dependencies. ⚠ Only checks server reachability — does not verify that the VLESS config is valid or functional.xray← recommended — spins up a temporaryxray runprocess per config with a local SOCKS5 inbound, routes an actual HTTP request towww.gstatic.com/generate_204through the proxy, and measures real round-trip latency. Only working configs receive a latency value. No third-party Python packages.Xray config builder — correct per Xray-core source:
h2/http(removed from Xray-core) →splithttpwithmode: stream-onexhttp→splithttp(xhttpSettings);httpupgrade→httpupgradeSettingswsSettings.hostas a direct field (deprecatedheaders["Host"]form removed)flowfield included in VLESS user config (required for XTLS/Vision + REALITY)Accurate RTT measurement:
_wait_for_port) instead of a fixed sleep — proceeds as soon as Xray is ready, fast-fails if the binary is missing or the config is invalid.Xray binary auto-detection and validation:
_resolve_xray_path()searches for the binary in order: path-as-given → script directory (same folder asxray_ping.py) → CWD. Users can dropxraynext to the script with no configuration needed._validate_xray_binary()runs a pre-flight check before any pinging begins: verifies existence, execute permission, and runsxray version. Displays a clear platform-specific error message (Linux/macOS/Android vs Windows) with fix instructions instead of silently timing out all configs.--verbose/-vflag:Key features:
INPUT_FILE,OUTPUT_FILE,TIMEOUT,CONCURRENCY,XRAY_PATHconfigurable directly at the top of the script;XRAY_PATHcomment shows the correct./xrayexample for local/Android useThreadPoolExecutor(-c / --concurrency)-t / --timeout); validated> 0andconcurrency >= 1Robustness:
ping_xrayre-raisesKeyboardInterrupt/SystemExitfor clean Ctrl+C shutdown (no orphaned Xray processes)withcontext manager — no resource leak on any exit path\r,\n,\x00) rejected inparse_vless_urito prevent HTTP header injectionSupporting files
configs_example.txt— sample input with WS/TLS/TCP configsREADME.md— usage docs, argument reference, mode comparison tableXRAY_SETUP.md— Persian-language guide: what the Xray binary is, download from official GitHub releases, install steps for Windows/Linux/macOS/Android, and how to setXRAY_PATHtests/test_xray_ping.py— 34 unit tests covering URI parsing, stream settings, config building, binary resolution, binary validation, file I/O, and CLI error paths (0 CodeQL alerts).gitignore— excludes__pycache__and generatedsorted_configs.txtOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.