Skip to content

Add real-traffic node testing, speed tests, VLESS+Reality support, and subscription by-name refresh

Latest

Choose a tag to compare

@xhqing xhqing released this 02 Aug 10:38

Added

  • Real-traffic node testing by default. The test command now starts a temporary xray instance per node (dedicated SOCKS port, bound to 127.0.0.1 only) and requests generate_204 through it, separating "TCP reachable" from "proxy actually usable." This fixes the long-standing issue where a TCP-only check marked a node as healthy even though proxy traffic never flowed (expired credentials, protocol handshake failure, no outbound connectivity on the peer). Output now shows separate TCP and proxy columns; pass --tcp to keep the fast TCP-only check. Added check_real_traffic, batch_check_real, _curl_through_socks (uses socks5h so DNS resolves through the proxy, avoiding local DNS-pollution false negatives), _find_free_port, and _kill_proc in xpilot/health_checker.py.
  • auto_switch redesigned as active best-node selection. Every interval seconds it runs a real-traffic check across all nodes, picks the lowest-latency usable node as the best, and switches whenever the best is not the current node — no longer just a fallback that switches only when the current node fails. hysteresis (default 0) prevents flapping between nodes with similar latency; when the current node is unusable it ignores hysteresis and switches to the best; when every node is unreachable it refreshes the subscription by name (with backoff), re-checks, and switches. The threshold option is replaced by hysteresis. Rewrote _check_and_switch in xpilot/auto_switch.py and added _try_refresh_subscription, _switch_to (reused by manual_switch), and _log_usable_status.
  • Subscription import with by-name refresh. NodeManager.import_from_subscription gains an update_existing parameter; when true, it matches existing nodes by name and overwrites their connection fields (address, port, UUID, password, cipher, transport, TLS, SNI) while preserving id, group, and custom name — so when the provider rotates credentials or IPs, the same node is actually refreshed instead of a suffixed duplicate being created (jms-c56s1jms-c56s1_1). Added the SUBSCRIPTION_REFRESH_FIELDS constant and _refresh_node_fields.
  • status warns when auto_switch is off, reminding you that the proxy will not auto-switch if the current node fails.
  • VLESS+Reality support. _parse_vless_link parses Reality parameters (pbk public key, sid short ID, fp fingerprint, flow) and URL-decodes the fragment node name (fixing %40/%3A garble after import). ProxyManager._generate_outbound emits realitySettings for Reality nodes and writes flow (e.g. xtls-rprx-vision) into the VLESS user object. The storage field whitelist (add_node / update_node / subscription refresh) now includes the Reality fields.
  • Automatic subscription cleanup with source tagging. import_from_subscription gains a source parameter that tags imported/refreshed nodes (e.g. source: 'JMS') and prunes nodes the subscription no longer returns. When a subscription renames a node (e.g. JMS-c56s1JMS-1336028@c56s1) or drops one, the stale entry is removed so the local set always matches what the subscription returns. Includes cliff protection (skips pruning when the returned count drops below 50% of existing, to avoid mass deletion on a transient outage). Manually added nodes have no source and are never pruned. Both subscription update and the auto_switch auto-refresh use this mode.
  • Usable-node anomaly logging. After each real-traffic check, if fewer nodes are usable than the total, auto_switch logs a warning listing the unreachable nodes and likely causes (written to log_file, default /tmp/xpilot.log); after subscription import, if a source's node count does not match what the subscription returned, it logs the reason (with added/refreshed/failed/pruned counts) so you can tell why usable nodes dropped.
  • xpilot test speed. Measures download speed via Cloudflare's __down endpoint (default 10 MB per node). --all-nodes tests every node's proxy speed concurrently; --direct tests direct (no-proxy) speed; --current tests the current node; --size sets the download size. Extracted _start_probe (shared temp-xray-probe logic reused by connectivity and speed) and added check_speed, check_direct_speed, batch_check_speed, and _curl_download in xpilot/health_checker.py.
  • test is now a command group (invoke_without_command keeps xpilot test --all-nodes / --tcp / --current / --group working) to host the speed subcommand. Breaking change: single-node testing moved from a positional argument (xpilot test my_node) to the --node option (xpilot test --node my_node), because a group's positional would collide with the subcommand name.
  • status shows three live metrics by default: the current node's real-traffic latency, through-node download speed, and direct download speed (downloading 5 MB through the running proxy and directly). --no-speed skips the speed test and shows latency only.
  • Latency comparison everywhere. Wherever "through-node latency" is shown, "direct latency" now appears alongside — status shows them side by side; test shows the direct-latency baseline in its connectivity summary for comparison. Added HealthChecker.check_direct_latency (a direct generate_204 request that auto-falls back to the Cloudflare endpoint behind the GFW).
  • New unit tests: subscription by-name refresh vs. default append, source auto-cleanup with cliff protection (tests/test_node_manager.py); real-traffic check failure paths and port probing (tests/test_health_checker.py); auto_switch real-traffic switch decisions and subscription-refresh backoff (tests/test_auto_switch.py); VLESS+Reality parsing and outbound-config generation (tests/test_subscription.py, tests/test_proxy_manager.py).

Changed

  • subscription update now refreshes by name and imports new nodes instead of only appending new ones (matching the "update" semantics). Use node import to append without refreshing existing nodes.
  • Default auto_switch settings in xpilot/config.py add auto_update_subscription (default True) and subscription_refresh_cooldown (default 3600).
  • auto_switch.enabled defaults to True (was False). A fresh install or xpilot init now enables real-traffic auto-switching out of the box — no manual config set needed.

Fixed

  • subscription.fetch now uses a trust_env=False session to force a direct connection, bypassing environment variables and the macOS system proxy. Previously, xpilot start pointed the system proxy at the local xray, so when the proxy itself was broken the subscription request was intercepted too — a "proxy down → can't fetch subscription → can't recover proxy" deadlock.
  • _parse_ss_link rewritten to fix SIP002 (ss://base64(method:password)@host:port#name) parsing. Base64 characters (+ / =) in the userinfo made urlparse mistake the userinfo for hostname/port, so SS nodes failed to parse and were silently skipped during subscription updates, leaving stale nodes unrefreshed. Added _b64decode_loose for websafe-base64 compatibility.

Full Changelog: v0.3.0...v0.4.0