Клиентский сканер детектируемости VPN / DPI / Reality / ТСПУ. Одна
статическая byebyevpn.exe под Windows (работает через Wine на Linux
и macOS), без прав администратора, без DLL-зависимостей.
____ ____ __ ______ _ _
| __ ) _ _ ___| __ ) _ _ __\ \ / / _ \| \ | |
| _ \| | | |/ _ \ _ \| | | |/ _ \ \ / /| |_) | \| |
| |_) | |_| | __/ |_) | |_| | __/\ V / | __/| |\ |
|____/ \__, |\___|____/ \__, |\___| \_/ |_| |_| \_|
|___/ |___/
──────────────────────────────────────────────────────
v3.2.0 · your node through a DPI box's eyes
──────────────────────────────────────────────────────
Languages: English · Русский · 简体中文 · فارسی
Discussion / report issues: ntc.party/t/byebyevpn/24325 · GitHub Issues · Telegram
Given an IP or hostname, run the full Russian OCR методика (§5-10) plus modern 2026 tunnel fingerprints against it from an external vantage point. Output: a detection score, the identified stack, and what a TSPU-class classifier would decide. No VPN connection to the target is needed - the scanner looks at the destination as a third-party observer, the way an ISP or DPI middlebox sees it.
Disable any active VPN / Zapret / proxy on the host running the scanner before you start. The scanner now checks this itself (preflight, step 1b) and refuses to give a verdict when its own stack is compromised:
preflight finding effect the route to the target leaves through a tunnel adapter (Wintun, WireGuard, TAP, sing-tun, PPP) blocked: no probe is sent, label UNRELIABLE, exit 5a TCP connect to 192.0.2.1(RFC 5737, routed nowhere) succeeds on the target's routeblocked: a local stack accepts every SYN, every "open port" would be fake zapret (winws), GoodbyeDPI or clumsy running blocked: our packets are rewritten in flight target resolves to a fake-IP or CGNAT address blocked --expect-ip Agiven and lookup services see another addressblocked tunnel adapters up but the target route is direct warning: GeoIP, CT and RTT anchors may go through the tunnel proxy clients running, system proxy or HTTP(S)_PROXYsetwarning lookup services (ipify, icanhazip, ifconfig.me) disagree on our address warning: egress depends on destination
--i-know-what-i-am-doingruns the scan anyway; the verdict is then marked as overridden in the report and in JSON. The external address check is skipped under--no-geoip/--stealth. Thelocalmode (byebyevpn local) is the one place where an active VPN is the point.
| # | Module | What it does |
|---|---|---|
| 1 | DNS resolve | A + AAAA, IPv4 preferred |
| 1b | Preflight | Tunnel on the target route, local ack-all, packet rewriters, proxies, external address; blocks the verdict when the scanner itself is compromised |
| 2 | GeoIP aggregation | 5 HTTPS-only providers in parallel, ASN + flags; reference only, never scored |
| 3a | TCP port scan | Connect-scan 1-65535 (default) or 205 curated ports, 500 threads; no SYN retransmission, so a closed port reads as refused, not timeout |
| 3a | Path check + ack-all control | 10 connects to the first open port (loss, RTT); 3 random dynamic-range ports that must refuse |
| 3b | TCP stack fingerprint | Handshake distribution + SIO_TCP_INFO peer window/MSS + closed-port reply, no admin; reference only, no OS guess |
| 4 | UDP probes | WireGuard / AmneziaWG / QUIC: replies validated against each protocol's response layout |
| 4b | AmneziaWG S1 deep-probe (v2.6.0) | Junk-prefix size sweep on :51820, response experiment only; cannot confirm AWG/S1 |
| 5 | Service fingerprint + CT | SSH, HTTP, TLS + SNI consistency, SOCKS5, CONNECT, Shadowsocks, crt.sh, proxy-header leak |
| 5b | uTLS dual-probe + JA4 + JA4S | Two ClientHellos per TLS port (synthetic Chrome-style hello vs openssl-default), JA4 / JA4S extracted from raw CH/SH bytes, JA4S classified against a backend-stack table |
| 6 | J3 / TSPU active probing | 8 probes per TLS port; reply / closed / reset / held open per probe, reference only |
| 7 | SNITCH + traceroute + SSTP | RTT vs GeoIP (methodika §10.1), ICMP hop-count, Microsoft SSTP |
| 8 | Verdict | Checks with passports, coverage, score 0-100 or INCONCLUSIVE / UNRELIABLE with reasons, blind spots |
v2.6.0 narrowed the UDP set to the modern signature-less tunnels. the legacy OpenVPN / IKEv2 / L2TP / TUIC / plain-QUIC / DNS probes were removed: those protocols carry fixed-port / fixed-header signatures any DPI already catches, so probing for them cost scan time without adding detection value for this niche.
| Port | Protocol | Payload |
|---|---|---|
| 51820 | WireGuard | 148-byte MessageInitiation, randomized body |
| 51820 | AmneziaWG Sx=8 | Delta-probe: vanilla WG rejected, Sx=8 prefix accepted |
| 55555 | AmneziaWG Sx=8 | 8-byte junk prefix + WG init |
| 51820 | AmneziaWG S1 sweep | 12-step junk-prefix size sweep, does not identify AWG or S1 |
| 36712 | Hysteria2 | QUIC v1 Initial, random DCID |
| 443 | Hysteria2 | QUIC v1 Initial on :443 |
51820 or --wg-port |
WireGuard keyed | full MessageInitiation from the owner's peer key; only with --wg-pubkey and --wg-key |
A datagram coming back is not evidence of a protocol: echo services,
reflectors and generic middleboxes all answer. Every reply is matched
against the protocol's real response layout before it can move the score
(src/scan/udp_validate.h), and any verbatim echo of our own payload is
discarded outright:
- WireGuard: accepted only on a valid
MessageResponse(type0x02, three reserved zero bytes, exactly 92 bytes) or a cookie reply (type0x03, 64 bytes) whose receiver index equals the sender index of our probe. Before this check, 92 bytes of unrelated02 00 00 00garbage scoredIMMEDIATE BLOCK(lab stand K). A reply triggers two more probes; two agreeing replies are required. Real WireGuard and AmneziaWG never answer a probe without the server key and a known peer key, so on real servers this stays silent and is reported as not measured. The owner can check a WireGuard node with keys, see below. - AmneziaWG: accepted on a WG
MessageResponsesitting behind a junk prefix, which is what AmneziaWG emits with defaultH1-H4. This is reported as AmneziaWG-consistent, not confirmed: it is unauthenticated, and a customH1-H4set rewrites the type byte and will not match at all. The S1 sweep is a response experiment; it does not recover S1. - QUIC: a valid QUIC packet confirms a QUIC endpoint, nothing more. HTTP/3 is ordinary web traffic and every QUIC stack answers an Initial identically, so this is a note and never moves the score, on any port. The seven preset ports are probed before open TCP ports; ports cut by the 12-port cap are listed as not probed.
None of these authenticates the peer: the scanner holds no key. They rule
out the accidental false positives, which is the most a score is entitled
to rest on. For real AmneziaWG evidence use awg-entropy on captured
traffic (see below).
byebyevpn scan <your-node> --wg-pubkey server.pub --wg-key peer.key [--wg-psk peer.psk] [--wg-port 51820]
byebyevpn udp <your-node> --wg-pubkey <base64> --wg-key peer.key
A WireGuard server answers nobody without keys: mac1 needs the server
public key, and after mac1 the server decrypts the initiator's static key
and silently drops a peer it does not know. So the check needs the server
public key and the private key of a peer configured on that server,
both from your own configs. With them the scanner sends a normal
148-byte initiation (fresh ephemeral, TAI64N timestamp rounded like
wireguard-go) and checks the reply cryptographically: a response whose mac1
verifies can only come from the holder of the server private key. Two
agreeing answers, 1 to 1.5 s apart, give wg-keyed positive (tier A, -15,
counted once together with wg-family). Silence is inconclusive: a wrong
key, a filter and no listener look the same. A preshared key that differs
is reported as such and still counts, since mac1 already proved the server.
- the public key may be given as text or a file; the private key and the preshared key only as files, so they never sit in the process list;
- keys are never printed and never written to JSON (
wg_self_checkholdsrequested,ranandport); - the server moves that peer's endpoint to this machine until the peer's own client sends again: use a spare peer for the check;
- it measures the path from this machine to the node at this moment. It does not cover AmneziaWG with junk or custom headers, which drops a plain initiation (inconclusive, not "hidden").
Eight probe types fired at every TLS-capable port:
- Empty TCP connect (no bytes)
GET /with a real Host headerCONNECT example.com:443- Plausible OpenSSH banner
- 512 random bytes (via
RAND_bytes) - TLS ClientHello with random
.invalidSNI - Absolute-URI proxy-style
GET 0xFF × 128
Each probe ends in one of: reply, closed by peer (FIN), reset (RST), no
reply with the connection held open, or no connect. Earlier builds printed
all non-replies as SILENT (dropped) and called six of them
"silent-on-junk (TLS-only / Reality-hidden)" on a plain nginx-like site.
The counts are only read when a well-formed TLS or HTTP exchange worked on
the same port (control); on a lossy path silence is declared not evidence.
J3 is reference output: it never moves the score and does not identify
Reality, Vision or any other protocol.
TLS certificates, HTTPS parsing and crt.sh lookup limits are documented in
TLS/HTTP observations. Short-lived certificates,
missing Server headers and CT lookup misses do not reduce the score.
| Score | Label | Meaning |
|---|---|---|
| 85-100 | CLEAN |
Few or no weighted indicators matched |
| 70-84 | NOISY |
Some weighted indicators matched |
| 50-69 | SUSPICIOUS |
More weighted indicators matched |
| < 50 | OBVIOUSLY VPN |
Many or heavily weighted indicators matched |
| - | INCONCLUSIVE | The scan did not happen in a way that supports a verdict; reasons are listed |
| - | UNRELIABLE | Preflight failed; the results describe this machine, not the target |
These are legacy heuristic labels, not protocol confirmation or detection
probabilities. CLEAN means that no named signature answered. It does not
mean the node is invisible to DPI: every report lists what these probes
cannot see (Reality with a working target, Shadowsocks AEAD/2022,
WireGuard without the owner's keys, AmneziaWG, Trojan or VLESS behind a
real site).
Only four signals move the score, each with a passport in
docs/SIGNALS.md: wg-family (-15), wg-keyed (-15,
owner self-check, one score together with wg-family), sstp (-18),
socks5 (-20), all tier A. Every check ends as positive, negative,
inconclusive or not applicable; only positives move the score, and a
positive needs two agreeing observations out of at most three. GeoIP tags,
junk probes, TCP stack data, JA4S families and RTT are reference output.
No verdict is given when: preflight failed; 2 of 3 random control ports accepted a connection (ack-all path); the path check lost 50% or more; more than half of the applicable signature checks were inconclusive; the TCP scan was interrupted; or no service answered in an attributable way.
Any tier A match caps the score at 69, so the label is at least
SUSPICIOUS and the exit code at least 2. Earlier builds could print
CLEAN 85 next to IMMEDIATE BLOCK for a single WireGuard-shaped reply.
The error matrix of every signal against known targets is in
docs/GROUNDTRUTH.md and
docs/CALIBRATION.md.
| Tier | Verdict | Meaning |
|---|---|---|
| A≥1 | IMMEDIATE BLOCK |
At least one legacy tier A rule matched |
| B≥2 | BLOCK (cumul.) |
At least two legacy tier B rules matched |
| B=1 | THROTTLE / QoS |
One legacy tier B rule matched |
| 0 | PASS / ALLOW |
No rules in this model matched |
This model does not reproduce a verified operator classifier. These category
names are retained for compatibility and do not establish actual blocking,
throttling or allowance. JSON marks thresholds_validated and blocking_verified
as false. No current signal is tier B, so the two middle rows cannot occur;
see docs/TSPU-MODEL.md for why nothing supports an
accumulative tier on the box.
The tool does not impersonate a browser. Every outbound HTTP request (to IP-intel services, to the target during HTTP-over-TLS audit, to crt.sh) goes out with zero tool-specific headers.
For http_get() - the one used against IP-intel services, crt.sh and
the DoH lookup of the ech command - the request is byte-wise
(captured on loopback):
GET /path HTTP/1.1
Connection: Keep-Alive
Host: <host>
Connection: Keep-Alive is added by WinHTTP itself. The Cloudflare DoH
fallback of ech adds Accept: application/dns-json, nothing else does.
No User-Agent, no Accept-Language, no Accept-Encoding, no
Sec-Fetch-*, no Upgrade-Insecure-Requests. Builds before this fix
also sent Accept-Encoding: gzip, deflate. A compressed reply is now
rejected instead of decoded.
For https_probe() - the target HTTP-over-TLS audit - headers are
also minimal (Host, Accept: */*, Connection: close). dpi --volume
sends the same three headers to the node and to the control host.
The WireGuard self-check (--wg-pubkey, --wg-key) sends a full
148-byte initiation built like a real client's: RAND_bytes ephemeral key
and sender index, TAI64N timestamp with the low 24 bits of nanoseconds
cleared as wireguard-go and the Linux module do, mac2 zero. Up to three,
1.0 to 1.5 s apart.
Earlier versions (v2.5 - v2.5.4) emitted a Chrome-131 header block intended to look "browser-like". That was itself a unique static fingerprint and has been dropped (see issue #5).
For protocol probes (UDP handshakes, TLS ClientHello, ICMP) every
field that a real client would randomize is filled via OpenSSL
RAND_bytes: WireGuard MessageInitiation body, AmneziaWG junk prefix +
WG body, Hysteria2 QUIC DCID, TLS ClientRandom, invalid-SNI prefix.
ICMP traceroute payload is the Windows ping.exe pattern
(abcdefghijklmnopqrstuvwabcdefghi, 32 bytes). Builds before this fix
sent 33 bytes (the C string terminator went out too), which no Windows
tool emits.
The uTLS dual-probe (v2.6.0) sends two different ClientHellos per TLS
port. The "chrome" side is a synthetic hello built by hand
(src/scan/chrome_ch.cpp): the Chrome extension set from before
X25519MLKEM768, GREASE at the spec positions, a GREASE-prefixed x25519
key_share and the padding extension. Its JA4 is
t13d1516h2_8daaf6152771_e5627efa2ab1, the FoxIO example value. It is
not what a current Chrome sends: the extension order is fixed (Chrome
permutes it per connection), there is no X25519MLKEM768 key share and no
ECH GREASE. Treat it as its own fingerprint, not as browser traffic.
The "openssl" side is the default OpenSSL ClientHello, same as the
existing tls_probe. SNI is the target's hostname in both.
ClientHello / ServerHello bytes stay in-process; JA4 / JA4S hashes are
computed locally and never transmitted.
The TCP stack fingerprint (3b) does not change anything on the wire.
It runs 6 sequential SOCK_STREAM connects to an already-known open
port, calls WSAIoctl(SIO_TCP_INFO_v0) on the local handle, and (if
asked) tries one connect to a closed port. No raw socket, no admin,
no extra packet shapes.
Measurement-health traffic added for the false-positive work: preflight
sends one SYN to 192.0.2.1 on a random port (only when the target shares
its route) and, unless --no-geoip, one GET each to api.ipify.org,
icanhazip.com and ifconfig.me/ip through http_get(). After the port
scan: 10 connects to the first open port (path check) and one connect to
each of 3 random ports in 49152-65535 (ack-all control). SOCKS5 greetings
and SSTP setup requests are repeated until two agree (at most three);
a WireGuard or AmneziaWG probe that gets a reply is sent twice more.
Every connect is made with SYN retransmission off (SIO_TCP_INITIAL_RTO).
local sends nothing unless a tunnel adapter is up. Then, only when a
non-tunnel adapter holds a global IPv6 address, one TCP connect each to
[2606:4700:4700::1111]:443 and [2001:4860:4860::8888]:443, closed at
once with no payload; and to each resolver the system would use beside the
tunnel (at most three), two standard queries for example.com A with a
RAND_bytes id. pcap, diff and audit-config read files only.
TLS clients no longer put an IP literal into SNI (RFC 6066 section 3): with
an IP target the OpenSSL probes send no SNI, the synthetic Chrome-style
hello omits server_name (so its JA4 starts t13i), and the SSTP request
uses the host name or no SNI plus a random correlation GUID instead of
{00000000-...}.
Grep the modular source tree for tool-identifying strings. Since
v2.5.8 the source is split across src/**/*.cpp and src/**/*.h
under src/common, src/net, src/scan, src/local, src/app,
plus src/main.cpp. Expected matches: only the --help printf in
src/app/cli.cpp (cap is 3 to leave headroom).
$ grep -rnE 'ByeByeVPN|BYEBYEVPN|BBVPN|BBV|pwnnex' \
src --include='*.cpp' --include='*.h'
src/app/cli.cpp:131: printf("ByeByeVPN - full TSPU/DPI/VPN ...
None of these reach a socket. The CI workflow
(.github/workflows/release.yml) fails the build if more than three
matches appear.
Windows: download byebyevpn-v3.2.0-win64.zip from
Releases, extract, run byebyevpn.exe - either
double-click for the interactive panel, or pass an IP/hostname from
the terminal.
Runtime: Windows 10 1803+ / 11 / Server 2019+. No admin, no DLLs, no
.NET, no VC++ Redistributable. Internet access for GeoIP, CT-log, the
preflight external-address check and the ech DoH lookup; all of them
are optional (--no-geoip, --no-ct, --stealth). The panel needs a
console with VT support (Windows Terminal or conhost on Windows 10+).
Linux / macOS: run through Wine. Everything except local
(host-side adapter enumeration) works identically.
Verify what you downloaded: every release ships a SHA256 in the
release notes, a CycloneDX SBOM (byebyevpn-sbom.json), and -
when the project signing key is configured - matching .minisig
signatures. See BUILD.md for the verify recipe.
byebyevpn # interactive panel
byebyevpn <host> # full scan
byebyevpn scan 1.2.3.4 # same, explicit
byebyevpn <host> --json # full scan, JSON on stdout (v2.6.0)
byebyevpn ports my.server.ru # tcp scan only
byebyevpn udp my.server.ru # udp probes only
byebyevpn tls my.server.ru 443 # tls + sni consistency
byebyevpn j3 my.server.ru 443 # j3 active probing
byebyevpn geoip 8.8.8.8 # geoip aggregation
byebyevpn snitch my.server.ru # rtt vs geo (methodika §10.1)
byebyevpn trace my.server.ru # icmp hop-count
byebyevpn local # scan this machine, IPv6 and DNS leaks included
byebyevpn audit-config cfg.json # identify configured protocols and audit settings
byebyevpn sweep 1.2.3.0/24 # cluster a subnet by TLS fingerprint (v2.8.0)
byebyevpn names sub.mysite.ru # does the NAME give you away? offline, no packets
byebyevpn pcap client.pcapng --node 1.2.3.4 # your own capture as the box reads it
byebyevpn batch nodes.txt --out today # every node in the file, one report each
byebyevpn diff yesterday today # what changed per node, offlineEvery command that sends packets to a target (scan, ports, udp,
tls, j3, grpc, snitch, trace, dpi) runs the preflight first and
exits 5 without sending anything when it fails.
Run byebyevpn with no arguments (or double-click it) for a full-screen
panel:
● byebyevpn v3.2.0 your node through a DPI box's eyes
machine ● direct via Ethernet public targets pass preflight
╭─ menu ──────────────────────╮╭─ SCAN ─────────────────────────────────────────╮
│ SCAN ││ Full scan │
│ ▸ Full scan ││ Preflight, TCP ports, path check, UDP, TLS, │
│ Quick scan (205 ports) ││ junk probes, verdict. ... │
│ DPI path test (SNI) ││ │
│ PROBES ││ sends: Preflight, then every probe ... │
│ TCP ports ││ settings (S to change) │
│ ... ││ ports all 65535 timeout 800 ms ... │
│ ││ last full scan │
│ ││ CLEAN 100/100 tier PASS / ALLOW │
╰─────────────────────────────╯╰────────────────────────────────────────────────╯
↑↓ move ←→ section Enter run S settings L last scan R refresh Q quit
- the status bar shows, without sending anything, whether public traffic from this machine leaves through a tunnel, and whether proxy clients, packet rewriters or a system proxy are active;
- each item says what it does and what it sends, before you run it;
- Settings (
S): port range (all, 205 curated, custom list), timeout, threads, stealth, passive, GeoIP and CT lookups, junk probes per port, saving each run to<target>.md, preflight override; - a run leaves the panel so the full output scrolls normally, then shows a result card; the panel keeps the last verdict and the target history (memory only, nothing is written to disk unless saving is on).
Every action calls the same code as the command line, so the panel sends
exactly the same probes. Without a console (pipe, redirect) the program
falls back to a prompt that takes command lines such as scan 1.2.3.4.
byebyevpn names sub.example.com vless-de1.example.com
byebyevpn names wg01.example.com us8360.nordvpn.com --jsonThis offline check finds protocol, panel and subscription naming conventions.
It accepts ASCII hostnames and Punycode (xn--), validates label boundaries,
and uses a bundled Public Suffix List, including private hosting suffixes.
Generic words match whole components: sub2 matches, subaru does not.
Distinctive names such as amneziawg also match within combined labels.
strong, moderate and weak describe a naming association, not measured
accuracy or proof of a running protocol. No hostname finding changes the
full scan score or TSPU verdict. Certificate names may describe another
service; the report retains each source (target, cert_cn:port, cert_san:port).
Provider-domain and node-pattern rules cover selected Mullvad, IVPN, NordVPN
and Cloudflare WARP names. A provider's website is not necessarily a VPN endpoint.
workers.dev is reported as shared application hosting, not VPN evidence.
Plain names does not resolve names, enumerate subdomains or query CT logs.
names example.com --ct lists every name under your domain that public
certificate transparency logs already hold (via crt.sh), with the same
markers: panel., sub., remnawave. and the rest are exactly what
anyone finds there without touching your node. --ct-file saved.json
reads a crt.sh JSON you saved instead, with no network at all.
--resolve asks DNS from this machine for those names and shows which
share an address; --node IP marks your node (behind a CDN a shared
address means nothing, and the output says so). A failed lookup exits 4
and is never read as "no names"; crt.sh is often overloaded, retry later
or use --ct-file.
Exit codes: 3 strong naming hints, 2 moderate, 0 weak/none/IP input, 64 invalid input. Exit 0 does not certify a clean server. Invalid names have explicit errors in JSON; mixed inputs are all reported and exit 64. See matching rules, sources and limitations.
A completed full scan exits with the verdict tier so wrapper scripts can branch without parsing output (v2.6.0):
0 CLEAN (score >= 85) 2 SUSPICIOUS (50-69)
1 NOISY (70-84) 3 OBVIOUSLY-VPN (< 50)
4 INCONCLUSIVE (no verdict) 5 UNRELIABLE (preflight failed)
64 usage error (no target)
dpi runs the same preflight and exits 5 when it fails. ech exits 4
when the DoH lookup itself failed; "no HTTPS RR" (exit 1) is printed only
when a resolver answered NOERROR without one.
Run these from your ordinary connection against your own node. They measure the path between this machine and that host at this moment; the result does not carry over to another connection, operator or day, and every result says so.
dpi <host> [port]: SNI reset or silent drop on the first flight. PrintsSNI check: positive / negative / inconclusive / not applicable, exit codes as before,--jsonfor scripts.dpi <host> [port] --volume /path --control host[:port]/path: the 16-20 KB freeze field reports describe (F7). The tool downloads/pathfrom your node over TLS (the path must serve 64 KB or more) and the control path from a host you know carries volume, control before and after. Positive means bytes stopped on an open connection for 8 s in two transfers at the same offset while the control carried 64 KB or more; the text says whether the offset lies in the 16-20 KB band. No control, a failing control, stalls at different offsets, or a connection closed by FIN or RST give inconclusive; a resource under 64 KB gives not applicable. Exit 0 carried, 2 stalled, 4 inconclusive or not applicable. The request isGETwith Host, Accept and Connection, the same header set as the HTTPS probe.dpi <node> [port] --sni NAME --real IP|auto: the name your node serves (for Reality, its serverName) goes to the node and to the address the name really lives on (autoresolves it). If it fails to the node and passes to its own address while a benign name to the node works, this path ties the name to its address, the rule field reports blame for Reality with brand targets. If it fails to both, the name itself is blocked here. Two agreeing rounds out of three; exit 0 passes, 2 fails, 4 inconclusive.
Hostnames are resolved via getaddrinfo; IPv4 is always preferred,
and the chosen IP is printed in phase [1/8]. On IPv4-only links (RU /
CIS consumer internet) this avoids the happy-eyeballs AAAA trap where
an unreachable v6 silently burns every timeout.
byebyevpn awg-entropy connection.pcapng --jsonOffline analysis of the outer UDP traffic of an actual connection. Reports per-flow byte/nibble entropy and repeated variable-size packet trains followed by bidirectional traffic. Designed with the examined AWG 2.x, 3.0 and latest 3.1 sources in mind; does not rely on fixed headers, handshake sizes or timers.
AWG_COMPATIBLE_HEURISTIC is a compatibility hint, not protocol proof.
Entropy cannot determine the AWG version. Ordinary encrypted UDP can also
match, and a missing pattern does not exclude AWG. This command does not change
the live scan score. Exit 0 means analysis completed; 64 is an input error.
See source research, thresholds, capture requirements and limitations.
byebyevpn pcap client.pcapng --node 203.0.113.5
byebyevpn pcap client.pcapng --node 203.0.113.5 --jsonCapture your own client on the physical interface (Wireshark, tcpdump,
pktmon + etl2pcap), open a few sites through the tunnel, stop, and
give the file to pcap. It reads what a box on that link reads before any
key, and sends nothing:
- client hellos: SNI, JA4 (TCP, and QUIC from the decrypted Initial),
GREASE, the ECH extension, a post-quantum key share, ALPN. No GREASE
means not Chromium or Safari.
ech-ext yescan be Chromium's GREASE ECH, which still carries the real name; - a TLS handshake inside the tunnel: per server, the sizes of the first flights after the outer handshake. An inner ClientHello, a certificate flight back, then a client record of exactly an inner TLS 1.3 Finished (58, 64, 74 or 80 B). Positive needs two matching flows. Calibrated on real xray: VLESS and Trojan over TLS without Vision match 4 of 4 flows, Vision and plain HTTPS 0 of 4. A size rule only; a negative is not "invisible";
- DNS in the clear: queries on port 53 and the resolvers they went to; DoH to well-known resolver names is marked;
- traffic beside the node (
--node): public addresses other than your node, IPv6 counted apart.
Exit 2 when any of the last three is positive, 0 otherwise, 64 on a file it cannot read. Files up to 64 MiB; TCP is rebuilt for the first 64 KB of each direction.
byebyevpn local lists adapters, routes and VPN software as before and
adds two checks that matter only while a tunnel adapter is up:
- IPv6 beside the tunnel: if a non-tunnel adapter has a global IPv6 address, the tool connects to two public resolvers over IPv6 (a plain TCP connect, closed at once) and looks at which local address the system picked. No global IPv6 outside the tunnel: negative with no packets.
- DNS beside the tunnel: Windows asks every adapter's resolver in
parallel unless a policy turns it off. The tool lists the resolvers the
system would use beside the tunnel and asks each twice for
example.com; an answer there means names leave in the clear. A kill switch that drops them gives silence (inconclusive) or a refused send (negative), never a leak.
Exit 2 when either leaks, 0 otherwise. Windows only.
byebyevpn batch nodes.txt --out 2026-10-01 --fast
byebyevpn diff 2026-09-24 2026-10-01batch runs the full scan on every line of the file (# starts a
comment, duplicates are skipped, at most 256), prints a summary table and
with --out DIR keeps one --json report per node. Scan options apply to
every node; exit is the worst node's verdict code. diff compares two
reports or two such directories, file by file: verdict (label, tier,
checks, scored signals), surface (address, open ports, certificates,
JA4S) and context (score, GeoIP tags). Offline; exit 2 when a verdict
changed, 1 for any other change, 0 when nothing changed.
byebyevpn audit-config server.json
byebyevpn audit-config server.json --jsonReads an Xray, sing-box or WireGuard/AmneziaWG configuration offline. The Xray
report identifies configured protocols, transports and Vision users, and checks
selected compatibility and exposure issues. It supports both the current
method/users fields and legacy network/clients aliases.
Removed XTLS, legacy HTTP/QUIC transports, invalid Vision flows, old VMess and
Shadowsocks settings, REALITY short IDs and proxy authentication are covered.
Both cores: two listeners on one port and socket layer (TCP and UDP may
share a number), a TLS version range with nothing left in it, and a control
API on a public address (Xray api, sing-box Clash or V2Ray API). Xray only:
duplicate inbound tags and duplicate user emails, which stop the core.
sing-box gets the REALITY short ID and handshake-server checks Xray already
had, and REALITY with tls.enabled off is now reported as plaintext: before,
the brand rule fired on a handshake server sing-box never used.
Findings that do not change what an observer sees print in a separate
Hygiene block and never move the tier: settings for another transport
or security mode that the core ignores, two users with one id or password,
debug logging, REALITY show.
Client configs (proxy outbounds) are checked too: VLESS or Trojan without
TLS, no browser fingerprint (Go ClientHello), no server name, certificate
checks off, a malformed REALITY key or shortId.
byebyevpn audit-config server.json client.json checks a client against
its server, Xray and sing-box in any mix: port, protocol, transport,
security, path, user, flow, REALITY serverName and shortId, and whether the
client's publicKey really belongs to the server's privateKey. Each
mismatch is a compatibility error; no key, id or password is printed.
Compatibility errors exit with 65 and don't count as network signatures.
File/parse errors exit with 64; 0-3 retain the legacy exposure tiers.
The file size limit is 16 MiB.
Results come from configuration only. Keys, certificate files, routing and
firewalls are not validated; no remote connection is made. Default ports,
entropy and TLS negotiation differences do not confirm Xray or Vision.
The legacy tspu_tier field is an unvalidated heuristic, not a measured block.
See Xray source research, checks and limits.
--full all ports 1-65535 (default)
--fast 205 curated VPN / proxy / TLS / admin ports
--range 8000-9000 ports port range
--ports 80,443,8443 explicit list
--threads N parallel TCP connects (default 500)
--tcp-to MS TCP connect timeout (default 800)
--udp-to MS UDP recv timeout (default 900)
--no-color disable ANSI colors
-v / --verbose verbose output
--expect-ip A preflight: the external address lookup services must see
--i-know-what-i-am-doing
scan despite a failed preflight; verdict marked overridden
--stealth --no-geoip + --no-ct + --udp-jitter, AND adds
inter-probe timing jitter across J3 / SNI consistency /
uTLS dual-probe / AmneziaWG sweep (v2.7.0)
--no-geoip skip all HTTPS GeoIP lookups
--no-ct skip crt.sh CT-log query
--udp-jitter 50-300ms random delay between UDP probes
--j3-subset N send a random N-probe subset (1..7) of the eight J3
probes per port instead of all eight (v2.7.0)
--passive minimal-probe mode: SKIPS J3, uTLS dual-probe, SNI
consistency loop and AmneziaWG S1 sweep entirely. one
base TLS handshake + GeoIP + CT-log + traceroute +
SNITCH only. fewest scanner-shaped patterns on the
wire (v2.7.0)
All default off. Default scan emits the same bytes v2.6.0 emitted.
Anti-fingerprint context: v2.7.0 randomizes the J3 probe order with a
CSPRNG-backed Fisher-Yates per scan, so the fixed empty -> GET -> CONNECT -> SSH -> rand -> tls-invalid -> abs-URI -> 0xff sequence is
no longer on the wire. The synthetic ClientHello randomness also moved
to RAND_bytes (away from std::mt19937).
--save write the scan to <target>.md in the current directory
--save <path> write the scan to <path> (markdown-wrapped)
ANSI colors are stripped from the file; terminal output is unchanged. The file is wrapped in a markdown code block so it renders cleanly in any md viewer.
See BUILD.md for full instructions, OpenSSL provenance, and SHA256s. Short form:
# msys2 UCRT64
pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-openssl mingw-w64-ucrt-x86_64-make
git clone https://github.com/pwnnex/ByeByeVPN.git && cd ByeByeVPN
make windows-staticRelease zips are produced by
.github/workflows/release.yml from
a pinned msys2 image. SHA256 of the exe and zip are printed in each
release's notes for verification.
- Connect-scan, not SYN-scan. Full TCP handshake seen by the target.
- Cloudflare WARP / CGNAT / corporate proxies / local TUN clients can
ACK every port. Preflight tests the local side with
192.0.2.1; after the scan, 3 random dynamic-range ports must refuse. Two accepted ports (or one plus >60 open ports with RTT spread <80 ms) void the verdict. - The uTLS dual-probe sends a synthetic Chrome-style ClientHello that
is not byte-identical to any current Chrome (see above). The
raw-socket path does not run the TLS 1.3 key schedule, so it has
no peer cert for the Chrome side - cert-steering detection still
relies on the openssl-side handshake. The main
tls_probehello is the OpenSSL default. - QUIC probes (v2.8.0) send a real RFC 9001 protected Initial - the
Initial key schedule, AEAD payload protection and header protection
are byte-exact against the RFC 9001 Appendix A test vectors, and a
TLS ClientHello rides in a CRYPTO frame. a QUIC listener decrypts it
and answers. the ClientHello carries ALPN
h3and the quic_transport_parameters extension (0x39) with initial_source_connection_id. the probe confirms a QUIC endpoint and captures its response shape, but does not drive a full QUIC handshake to completion. - GeoIP providers disagree and share upstream feeds. Their VPN, proxy and Tor tags are printed for reference and never move the score.
- An active scan cannot see what the classifier sees passively (address lists, SNI policy, first-packet heuristics, the volume freeze). See docs/COVERAGE.md.
GPL-3.0-or-later. See LICENSE and NOTICE.
Releases up to v2.5.9 were MIT. From v2.6.0 onward the project is GPL-3.0-or-later. Old MIT releases keep their MIT grant; nothing is revoked retroactively. The relicense was done by the sole copyright holder (every commit up to v2.6.0 was authored by pwnnex).
Получив IP или hostname, программа прогоняет полную методику Роскомнадзора (§5-10) + современные 2026 сигнатуры обфусцированных туннелей против этой цели, работая как внешний наблюдатель. На выходе: score детектируемости, определённый стек, и решение, которое принял бы ТСПУ-классификатор. Подключаться к VPN цели не нужно - сканер смотрит на неё так же, как видит провайдер или DPI-middlebox.
Перед запуском выключи на хосте любой активный VPN / Zapret / GoodbyeDPI / прокси. Теперь сканер проверяет это сам (preflight, шаг 1b) и отказывается выдавать вердикт, если его собственный стек скомпрометирован:
что нашёл preflight что будет маршрут до цели идёт через туннельный адаптер (Wintun, WireGuard, TAP, sing-tun, PPP) стоп: ни одной пробы, метка UNRELIABLE, код 5TCP-connect на 192.0.2.1(RFC 5737, никуда не маршрутизируется) прошёл на маршруте целистоп: локальный стек принимает любой SYN, все «открытые порты» фальшивые запущен zapret (winws), GoodbyeDPI или clumsy стоп: пакеты переписываются на лету цель резолвится в fake-IP или CGNAT стоп задан --expect-ip A, а сервисы видят другой адресстоп туннель поднят, но маршрут до цели прямой предупреждение: GeoIP, CT и RTT-якоря могут идти через туннель запущены прокси-клиенты, задан системный прокси или HTTP(S)_PROXYпредупреждение ipify, icanhazip и ifconfig.me видят разные адреса предупреждение: выход зависит от назначения
--i-know-what-i-am-doingсканирует всё равно, вердикт помечается как overridden в отчёте и в JSON. Проверка внешнего адреса выключается--no-geoip/--stealth. Режимlocal(byebyevpn local) как раз для просмотра своих адаптеров.
| # | Модуль | Что делает |
|---|---|---|
| 1 | DNS resolve | A + AAAA, приоритет IPv4 |
| 1b | Preflight | Туннель на маршруте до цели, локальный ack-all, переписчики пакетов, прокси, внешний адрес; без вердикта, если скомпрометирован сам сканер |
| 2 | GeoIP aggregation | 5 HTTPS-only провайдеров параллельно, ASN + флаги; справочно, в score не входит |
| 3a | TCP port scan | Connect-scan 1-65535 (дефолт) или 205 curated, 500 потоков; без повтора SYN, закрытый порт читается как refused, а не timeout |
| 3a | Проверка канала + контроль ack-all | 10 connect'ов на первый открытый порт (потери, RTT); 3 случайных порта из 49152-65535 обязаны отказать |
| 3b | TCP stack fingerprint | Распределение handshake-времени + SIO_TCP_INFO peer window/MSS + ответ закрытого порта, без админа; справочно, ОС не угадывается |
| 4 | UDP probes | WireGuard / AmneziaWG / QUIC: ответ сверяется с реальным layout'ом ответа протокола |
| 4b | AmneziaWG S1 deep-probe (v2.6.0) | Sweep размера junk-prefix на :51820, не подтверждает AWG или S1 |
| 5 | Service fingerprint + CT | SSH, HTTP, TLS + SNI consistency, SOCKS5, CONNECT, Shadowsocks, crt.sh, proxy-headers |
| 5b | uTLS dual-probe + JA4 + JA4S | По два ClientHello на TLS-порт (синтетический hello в стиле Chrome vs openssl-default), JA4 / JA4S из захваченных байт CH/SH, JA4S классифицируется по таблице стеков |
| 6 | J3 / ТСПУ active probing | 8 probe'ов на каждый TLS-порт; для каждого reply / closed / reset / held open, справочно |
| 7 | SNITCH + traceroute + SSTP | RTT vs GeoIP (§10.1), ICMP hop-count, Microsoft SSTP |
| 8 | Вердикт | Проверки с паспортами, покрытие, score 0-100 или INCONCLUSIVE / UNRELIABLE с причинами, слепые зоны |
v2.6.0 сузил UDP-набор до современных signature-less туннелей. legacy пробы OpenVPN / IKEv2 / L2TP / TUIC / plain-QUIC / DNS убраны: эти протоколы несут fixed-port / fixed-header сигнатуры, которые любой DPI и так ловит, так что probe'инг по ним съедал время скана не давая детект-ценности для этой ниши.
| Порт | Протокол | Payload |
|---|---|---|
| 51820 | WireGuard | 148-байтный MessageInitiation, рандомное тело |
| 51820 | AmneziaWG Sx=8 | Двойная проба: vanilla WG отвергается, Sx=8 принят |
| 55555 | AmneziaWG Sx=8 | 8-байт junk-prefix + WG init |
| 51820 | AmneziaWG S1 sweep | Sweep из 12 размеров junk-prefix, не подтверждает AWG или S1 |
| 36712 | Hysteria2 | QUIC v1 Initial, рандомный DCID |
| 443 | Hysteria2 | QUIC v1 Initial на :443 |
51820 или --wg-port |
WireGuard keyed | полный MessageInitiation от ключа пира владельца; только с --wg-pubkey и --wg-key |
Восемь probe-типов на каждый TLS-порт:
- Пустой TCP (ничего не шлём)
GET /с реальным Host-заголовкомCONNECT example.com:443- Плаузабельный OpenSSH-баннер
- 512 байт
RAND_bytes - TLS ClientHello с рандомным
.invalidSNI - HTTP absolute-URI (proxy-style)
0xFF × 128
Каждая проба заканчивается одним из исходов: ответ, закрыто сервером (FIN),
сброс (RST), нет ответа при открытом соединении, нет соединения. Раньше все
неответы печатались как SILENT (dropped), а шесть таких на обычном
nginx-подобном сайте давали «silent-on-junk (TLS-only / Reality-hidden)».
Счётчики читаются, только если на том же порту прошёл нормальный TLS- или
HTTP-обмен (контроль); на канале с потерями молчание уликой не считается.
J3 справочный: в score не входит и никакой протокол не определяет.
Для WireGuard ответ засчитывается, только если его receiver index равен sender index нашей пробы; ответ вызывает ещё две пробы, нужны два согласных. Настоящие WireGuard и AmneziaWG без ключей не отвечают никогда, это печатается как «не измерено». QUIC-ответ это заметка и в score не входит ни на каком порту.
byebyevpn scan <своя-нода> --wg-pubkey server.pub --wg-key peer.key [--wg-psk peer.psk] [--wg-port 51820]
Сервер WireGuard без ключей не отвечает никому: mac1 считается от
публичного ключа сервера, а после mac1 сервер расшифровывает статический
ключ инициатора и молча отбрасывает неизвестного пира. Поэтому нужны
публичный ключ сервера и приватный ключ пира, который на этом сервере
настроен, оба из твоих конфигов. С ними сканер шлёт обычный 148-байтный
initiation (свежий эфемерный ключ, TAI64N с тем же округлением, что у
wireguard-go) и проверяет ответ криптографически: ответ с верным mac1 может
прислать только владелец приватного ключа сервера. Два согласных ответа с
интервалом 1-1.5 с дают wg-keyed positive (tier A, -15, вместе с
wg-family считается один раз). Тишина это inconclusive: неверный ключ,
фильтр и отсутствие слушателя неотличимы. Несовпадающий preshared key
печатается отдельно и всё равно засчитывается, mac1 уже доказал сервер.
- публичный ключ можно текстом или файлом, приватный и preshared только файлом, чтобы они не попадали в список процессов;
- ключи не печатаются и не пишутся в JSON (
wg_self_check:requested,ran,port); - сервер переносит endpoint этого пира на эту машину, пока его собственный клиент снова не отправит трафик: для проверки заведи отдельного пира;
- измеряется путь от этой машины до ноды в этот момент. AmneziaWG с junk или своими заголовками обычный initiation отбрасывает (inconclusive, а не «скрыт»).
Ограничения анализа сертификатов, HTTPS и crt.sh описаны в
TLS/HTTP observations. Короткий срок сертификата,
отсутствие Server и пустой результат поиска в CT не снижают score.
| Score | Label | Смысл |
|---|---|---|
| 85-100 | CLEAN |
Мало или нет учитываемых признаков |
| 70-84 | NOISY |
Совпала часть учитываемых признаков |
| 50-69 | SUSPICIOUS |
Совпало больше учитываемых признаков |
| < 50 | OBVIOUSLY VPN |
Много признаков или признаки с большим весом |
| - | INCONCLUSIVE | Скан не состоялся так, чтобы на нём строить вердикт; причины перечислены |
| - | UNRELIABLE | Preflight не прошёл; результаты описывают эту машину, а не цель |
Это прежние названия эвристических категорий, не подтверждение протокола и не
вероятность детекта. CLEAN значит «ни одна именованная сигнатура не
ответила», а не «DPI эту ноду не видит»: каждый отчёт перечисляет, чего пробы
не видят (Reality с рабочим target, Shadowsocks AEAD/2022, WireGuard без
ключей владельца, AmneziaWG, Trojan или VLESS за настоящим сайтом).
Score двигают только четыре сигнала, у каждого паспорт в
docs/SIGNALS.md: wg-family (-15), wg-keyed (-15,
self-check владельца, вместе с wg-family считается один раз), sstp
(-18), socks5 (-20), все tier A. Каждая проверка заканчивается как positive,
negative, inconclusive или not applicable; score двигает только positive, и
для него нужны два согласных наблюдения из не более чем трёх. GeoIP-теги,
J3, данные TCP-стека, JA4S и RTT справочные.
Вердикта нет, если: не прошёл preflight; 2 из 3 случайных контрольных портов приняли соединение (ack-all); проверка канала потеряла 50% и больше; больше половины применимых проверок inconclusive; скан прерван; ни один сервис не ответил так, чтобы это можно было атрибутировать. Матрица ошибок по стендам с известной истиной: docs/GROUNDTRUTH.md, docs/CALIBRATION.md.
Любое совпадение группы A ниже ограничивает score значением 69, то есть метка
не лучше SUSPICIOUS, код выхода не меньше 2. Раньше один ответ в форме
WireGuard давал CLEAN 85 рядом с IMMEDIATE BLOCK.
| Tier | Вердикт | Что это значит |
|---|---|---|
| A≥1 | IMMEDIATE BLOCK |
Совпало хотя бы одно правило прежней группы A |
| B≥2 | BLOCK (cumul.) |
Совпало хотя бы два правила прежней группы B |
| B=1 | THROTTLE / QoS |
Совпало одно правило прежней группы B |
| 0 | PASS / ALLOW |
Правила этой модели не сработали |
Модель не воспроизводит проверенный классификатор оператора. Названия сохранены
для совместимости и не доказывают блокировку, замедление или пропуск трафика.
В JSON поля thresholds_validated и blocking_verified равны false. Сейчас
ни один сигнал не относится к группе B, две средние строки не возникают; почему
у коробки нет накопительного tier, см. docs/TSPU-MODEL.md.
Программа не притворяется браузером. Каждый исходящий HTTP-запрос (к IP-intel сервисам, к target при HTTP-over-TLS аудите, к crt.sh) уходит без tool-specific заголовков.
Для http_get() - функция которая ходит в IP-intel, crt.sh и в DoH
команды ech - запрос побайтово (снят на loopback) выглядит так:
GET /path HTTP/1.1
Connection: Keep-Alive
Host: <host>
Connection: Keep-Alive добавляет сам WinHTTP. Резервный DoH Cloudflare
в ech добавляет Accept: application/dns-json, больше ничего. Никаких
User-Agent, Accept-Language, Accept-Encoding, Sec-Fetch-*,
Upgrade-Insecure-Requests. Сборки до этого исправления ещё слали
Accept-Encoding: gzip, deflate. Сжатый ответ теперь отвергается, а не
распаковывается.
Для https_probe() - аудит target'а через HTTP-over-TLS - хедеры
тоже минимальные (Host, Accept: */*, Connection: close).
dpi --volume шлёт те же три заголовка на ноду и на контрольный хост.
Self-check WireGuard (--wg-pubkey, --wg-key) шлёт полный 148-байтный
initiation, собранный как у настоящего клиента: эфемерный ключ и sender
index из RAND_bytes, TAI64N с обнулёнными младшими 24 битами наносекунд,
как у wireguard-go и модуля Linux, mac2 нулевой. До трёх штук с
интервалом 1-1.5 с.
Предыдущие версии (v2.5 - v2.5.4) отправляли блок заголовков "как Chrome 131", чтобы "выглядеть как браузер". Это само по себе было уникальным статическим fingerprint'ом и удалено (см. issue #5).
Для protocol-probe'ов (UDP handshake'и, TLS ClientHello, ICMP) каждое
поле, которое реальный клиент рандомизирует, заполняется через
OpenSSL RAND_bytes: тело WireGuard MessageInitiation, junk-prefix +
WG-тело AmneziaWG, Hysteria2 QUIC DCID, TLS ClientRandom, префикс
invalid-SNI.
ICMP traceroute шлёт payload Windows ping.exe
(abcdefghijklmnopqrstuvwabcdefghi, 32 байта). Сборки до этого
исправления слали 33 байта (в сеть уходил завершающий ноль C-строки),
такого не шлёт ни одна утилита Windows.
uTLS dual-probe (v2.6.0) шлёт два разных ClientHello на TLS-порт.
Сторона "chrome" это синтетический hello, собранный вручную
(src/scan/chrome_ch.cpp): набор расширений Chrome до X25519MLKEM768,
GREASE на позициях из спеки, x25519 key_share с GREASE-префиксом,
padding. Его JA4 t13d1516h2_8daaf6152771_e5627efa2ab1, это пример из
спеки FoxIO. Текущий Chrome шлёт другое: порядок расширений у нас
фиксирован (Chrome перемешивает его на каждое соединение), нет
X25519MLKEM768 и нет ECH GREASE. Это отдельный отпечаток, а не трафик
браузера. Сторона "openssl" это дефолтный ClientHello OpenSSL, как в
tls_probe. Байты CH/SH не покидают процесс, JA4 / JA4S считаются
локально.
Служебный трафик для проверки самого измерения: preflight шлёт один SYN
на 192.0.2.1 на случайный порт (только если цель идёт тем же маршрутом)
и, если не задан --no-geoip, по одному GET на api.ipify.org,
icanhazip.com и ifconfig.me/ip через http_get(). После скана портов:
10 connect'ов на первый открытый порт (проверка канала) и по одному
connect'у на 3 случайных порта из 49152-65535 (контроль ack-all).
SOCKS5-приветствие и SSTP-запрос повторяются до двух согласных ответов
(не больше трёх); проба WireGuard или AmneziaWG, получившая ответ,
отправляется ещё дважды. Все connect'ы идут без повтора SYN
(SIO_TCP_INITIAL_RTO).
local ничего не шлёт, пока не поднят туннельный адаптер. Тогда, только
если у нетуннельного адаптера есть глобальный IPv6, по одному TCP connect'у
на [2606:4700:4700::1111]:443 и [2001:4860:4860::8888]:443, сразу
закрытых, без данных; и каждому резолверу, которого система спросит мимо
туннеля (не больше трёх), два обычных запроса example.com A с id из
RAND_bytes. pcap, diff и audit-config только читают файлы.
TLS-клиенты больше не кладут IP-адрес в SNI (RFC 6066, раздел 3): при
цели-адресе OpenSSL-пробы идут без SNI, синтетический hello в стиле Chrome
не содержит server_name (JA4 начинается с t13i), а SSTP-запрос берёт
имя хоста или идёт без SNI, со случайным GUID вместо {00000000-...}.
Грепнуть модульное дерево исходников на tool-identifying строки.
Ожидается одно совпадение, --help printf в src/app/cli.cpp, лимит 3
оставлен как запас. В сеть оно не уходит:
$ grep -rnE 'ByeByeVPN|BYEBYEVPN|BBVPN|BBV|pwnnex' \
src --include='*.cpp' --include='*.h'
src/app/cli.cpp:131: printf("ByeByeVPN - full TSPU/DPI/VPN ...
CI workflow (.github/workflows/release.yml) проваливает сборку, если
совпадений больше трёх.
Windows: скачать byebyevpn-v3.2.0-win64.zip со страницы
Releases, распаковать, запустить byebyevpn.exe
(двойной клик = интерактивная панель, либо IP/hostname из терминала).
Требования: Windows 10 1803+ / 11 / Server 2019+. Прав администратора
не нужно. DLL не нужно. Интернет нужен для GeoIP, CT-log, проверки
внешнего адреса в preflight и DoH-запроса ech; всё это отключается
(--no-geoip, --no-ct, --stealth). Панели нужна консоль с VT
(Windows Terminal или conhost на Windows 10+).
Linux / macOS: через Wine. Всё кроме local (адаптеры хоста)
работает идентично.
Проверка скачанного: каждый релиз идёт с SHA256 в release notes,
CycloneDX-SBOM (byebyevpn-sbom.json) и - когда выставлен ключ
подписи проекта - с .minisig подписями. Рецепт верификации в
BUILD.md.
byebyevpn # интерактивная панель
byebyevpn <host> # полный скан
byebyevpn scan 1.2.3.4 # то же, явно
byebyevpn <host> --json # полный скан, JSON в stdout (v2.6.0)
byebyevpn ports my.server.ru # только tcp
byebyevpn udp my.server.ru # только udp
byebyevpn tls my.server.ru 443 # TLS + SNI consistency
byebyevpn j3 my.server.ru 443 # J3 active probing
byebyevpn geoip 8.8.8.8 # GeoIP
byebyevpn snitch my.server.ru # RTT vs geo (§10.1)
byebyevpn trace my.server.ru # ICMP hop-count
byebyevpn local # сканировать свою машину, вместе с утечками IPv6 и DNS
byebyevpn audit-config cfg.json # протоколы, Vision и ошибки настроек; без сетевых запросов
byebyevpn sweep 1.2.3.0/24 # кластеризация подсети по TLS-отпечатку (v2.8.0)
byebyevpn names sub.mysite.ru # признаки в имени, офлайн
byebyevpn pcap client.pcapng --node 1.2.3.4 # свой дамп глазами коробки
byebyevpn batch nodes.txt --out today # все ноды из файла, по отчёту на каждую
byebyevpn diff yesterday today # что поменялось по каждой ноде, офлайнaudit-config для обоих ядер ловит два listener'а на одном порту и слое
(TCP и UDP могут делить номер), диапазон версий TLS, в котором ничего не
осталось, и управляющий API на публичном адресе (Xray api, Clash/V2Ray API
sing-box); только для Xray: повтор тега inbound и email пользователя, с
которыми ядро не стартует. sing-box получил проверки short ID и handshake
server, которые были у Xray; REALITY при выключенном tls.enabled теперь
печатается как plaintext. То, что не меняет картину для наблюдателя
(настройки чужого транспорта, которые ядро игнорирует, два пользователя с
одним id, debug-лог, REALITY show), выводится отдельным блоком
Hygiene и на tier не влияет.
Клиентские конфиги (прокси-outbounds) тоже проверяются: VLESS или Trojan
без TLS, нет браузерного отпечатка (уходит ClientHello от Go), нет имени
сервера, отключена проверка сертификата, битый ключ или shortId REALITY.
byebyevpn audit-config server.json client.json сверяет клиента с его
сервером, Xray и sing-box в любом сочетании: порт, протокол, транспорт,
защита, путь, пользователь, flow, serverName и shortId REALITY и то, что
publicKey клиента действительно от privateKey сервера. Каждое расхождение
это ошибка совместимости (выход 65); ключи, id и пароли не печатаются.
names example.com --ct показывает все имена под вашим доменом, которые
уже лежат в публичных логах Certificate Transparency (через crt.sh), с теми
же маркерами: panel., sub., remnawave. и прочее, что любой найдёт там,
не трогая ноду. --ct-file saved.json читает сохранённый JSON crt.sh,
совсем без сети. --resolve резолвит имена с этой машины и показывает,
какие сидят на одном адресе; --node IP отмечает вашу ноду (за CDN общий
адрес ничего не значит, вывод это пишет). Неудачный запрос выходит с 4 и
никогда не читается как «имён нет»; crt.sh часто перегружен, повторите
позже или используйте --ct-file.
Каждая команда, которая шлёт пакеты на цель (scan, ports, udp, tls,
j3, grpc, snitch, trace, dpi), сначала делает preflight и при
провале выходит с кодом 5, ничего не отправив.
byebyevpn без аргументов (или двойной клик) открывает панель на весь
экран: слева меню по разделам, справа описание выбранного пункта, что он
отправит в сеть, текущие настройки и карточка последнего скана.
- строка состояния без единого пакета показывает, уходит ли публичный трафик этой машины через туннель и запущены ли прокси-клиенты, переписчики пакетов или системный прокси;
- клавиши: стрелки вверх/вниз по пунктам, влево/вправо по разделам, Enter
запуск,
Sнастройки,Lпоследний скан,Rобновить статус,Qвыход; - настройки: диапазон портов (все, 205 отобранных, свой список), таймаут,
потоки, stealth, passive, GeoIP и CT, junk-пробы на порт, сохранение
каждого запуска в
<target>.md, обход preflight; - запуск выходит из панели, чтобы вывод прокручивался как обычно, потом показывает карточку результата. История целей хранится только в памяти.
Панель вызывает тот же код, что и командная строка, пробы байт в байт те же.
Без консоли (pipe, перенаправление) работает построчный режим: вводятся
команды вида scan 1.2.3.4.
byebyevpn names sub.example.com vless-de1.example.com
byebyevpn names wg01.example.com us8360.nordvpn.com --jsonОфлайн-проверка ищет названия протоколов, панелей и подписочных сервисов.
Поддерживаются ASCII-имена и Punycode (xn--); границы доменов определяются
по встроенному Public Suffix List с частными зонами хостингов.
sub2 совпадает с sub, а subaru нет. Названия вроде amneziawg также
ищутся внутри составных меток.
Уровни strong, moderate, weak обозначают характер совпадения, а не
измеренную точность. Имя не подтверждает протокол и не меняет score или
вердикт ТСПУ. CN/SAN может принадлежать другому сервису. В отчёте остаются
источники каждого имени: target, cert_cn:порт, cert_san:порт.
Есть отдельные правила зон и имён узлов Mullvad, IVPN, NordVPN и Cloudflare WARP.
Домен провайдера может быть сайтом или API. workers.dev отмечается как общий
хостинг приложений, а не доказательство VPN. Команда не резолвит имена,
не перебирает поддомены и не обращается к CT-логам.
Коды выхода: 3 strong, 2 moderate, 0 weak/нет совпадений/IP, 64 некорректный ввод. Код 0 не означает, что сервер чистый. При смешанном вводе программа показывает все результаты и возвращает 64, если есть ошибка. Правила, источники и ограничения.
Завершённый full scan выходит с кодом по уровню вердикта, чтобы обёртки могли ветвиться без парсинга вывода (v2.6.0):
0 CLEAN (score >= 85) 2 SUSPICIOUS (50-69)
1 NOISY (70-84) 3 OBVIOUSLY-VPN (< 50)
4 INCONCLUSIVE (без вердикта) 5 UNRELIABLE (preflight не прошёл)
64 ошибка использования (нет цели)
dpi делает тот же preflight и выходит с 5, если он не прошёл. ech
выходит с 4, если не удался сам DoH-запрос; «no HTTPS RR» (код 1)
печатается, только если резолвер ответил NOERROR без записи.
Запускаются с обычного подключения против своей ноды. Измеряют путь между этой машиной и этим хостом в этот момент; на другое подключение, другого оператора и другой день результат не переносится, и каждый вывод это пишет.
dpi <host> [port]: сброс или тихий дроп по SNI на первом полёте. ПечатаетSNI check: positive / negative / inconclusive / not applicable, коды выхода прежние,--jsonдля скриптов.dpi <host> [port] --volume /path --control host[:port]/path: заморозка на 16-20 КБ из полевых отчётов (F7). Тулза качает/pathс твоей ноды по TLS (путь должен отдавать от 64 КБ) и контрольный путь с хоста, который заведомо передаёт объём, контроль до и после. Positive значит, что байты перестали идти при открытом соединении на 8 с в двух передачах на одном смещении, а контроль передал 64 КБ и больше; текст пишет, попало ли смещение в полосу 16-20 КБ. Нет контроля, контроль не прошёл, остановки на разных смещениях, соединение закрыто FIN или RST дают inconclusive; ресурс меньше 64 КБ даёт not applicable. Выход 0 передал, 2 встал, 4 inconclusive или not applicable. ЗапросGETс Host, Accept и Connection, тот же набор заголовков, что у HTTPS-пробы.dpi <нода> [port] --sni ИМЯ --real IP|auto: имя, которое обслуживает нода (для Reality её serverName), уходит на ноду и на адрес, где это имя живёт на самом деле (autoрезолвит его). Если до ноды не проходит, а до своего адреса проходит, и безобидное имя до ноды работает, значит путь связывает имя с адресом, то самое правило, на которое жалуются для Reality с брендовым target. Если не проходит никуда, режется само имя. Два согласных раунда из трёх; выход 0 проходит, 2 не проходит, 4 inconclusive.
byebyevpn pcap client.pcapng --node 203.0.113.5Сними дамп своего клиента на физическом интерфейсе (Wireshark,
tcpdump, pktmon + etl2pcap), открой пару сайтов через туннель,
останови и отдай файл pcap. Он читает то, что коробка на этом канале
видит до любого ключа, и ничего не отправляет:
- ClientHello: SNI, JA4 (TCP и QUIC из расшифрованного Initial),
GREASE, расширение ECH, постквантовый key share, ALPN. Нет GREASE значит
не Chromium и не Safari.
ech-ext yesможет быть GREASE ECH от Chromium, в нём настоящее имя всё равно открыто; - TLS-рукопожатие внутри туннеля: по каждому серверу размеры первых полётов после внешнего рукопожатия. Внутренний ClientHello, обратно полёт с сертификатом, потом запись клиента ровно размера внутреннего TLS 1.3 Finished (58, 64, 74 или 80 Б). Positive нужно два совпавших потока. Откалибровано на настоящем xray: VLESS и Trojan поверх TLS без Vision совпадают в 4 потоках из 4, Vision и обычный HTTPS в 0 из 4. Это правило по размерам; negative не значит "невидимо";
- DNS в открытую: запросы на порт 53 и резолверы, куда они ушли; DoH к известным резолверам отмечается;
- трафик мимо ноды (
--node): публичные адреса кроме твоей ноды, IPv6 отдельно.
Выход 2, если что-то из трёх последних positive, иначе 0, 64 если файл не читается. Файлы до 64 МиБ; TCP собирается на первые 64 КБ в каждую сторону.
byebyevpn local как раньше показывает адаптеры, маршруты и VPN-софт и
добавляет две проверки, которые имеют смысл только при поднятом
туннельном адаптере:
- IPv6 мимо туннеля: если у нетуннельного адаптера есть глобальный IPv6, тулза подключается к двум публичным резолверам по IPv6 (обычный TCP connect, сразу закрыт) и смотрит, какой локальный адрес выбрала система. Глобального IPv6 вне туннеля нет: negative без единого пакета.
- DNS мимо туннеля: Windows спрашивает резолверы всех адаптеров
параллельно, если политика это не выключила. Тулза перечисляет
резолверы, которых система спросит мимо туннеля, и спрашивает каждый
дважды про
example.com; ответ оттуда значит, что имена уходят в открытую. Kill switch, который их режет, даёт тишину (inconclusive) или отказ отправки (negative), но не утечку.
Выход 2, если течёт хоть что-то, иначе 0. Только Windows.
byebyevpn batch nodes.txt --out 2026-10-01 --fast
byebyevpn diff 2026-09-24 2026-10-01batch гоняет полный скан по каждой строке файла (# начинает
комментарий, повторы пропускаются, не больше 256), печатает сводную
таблицу и с --out DIR сохраняет --json отчёт на каждую ноду. Опции
скана действуют на все ноды; выход равен худшему коду вердикта. diff
сравнивает два отчёта или две такие папки по именам файлов: вердикт
(label, tier, проверки, сработавшие сигналы), поверхность (адрес,
открытые порты, сертификаты, JA4S) и контекст (score, метки GeoIP).
Офлайн; выход 2, если поменялся вердикт, 1 при любом другом изменении,
0 если ничего не поменялось.
Hostname резолвится через getaddrinfo; IPv4 выбирается всегда, а
выбранный IP печатается в фазе [1/8]. На IPv4-only каналах (РФ / СНГ)
это чинит баг happy-eyeballs, когда недоступный IPv6 тихо съедал
весь timeout.
--full все порты 1-65535 (дефолт)
--fast 205 curated VPN / proxy / TLS / admin
--range 8000-9000 ports диапазон
--ports 80,443,8443 явный список
--threads N параллельных TCP-connect'ов (default 500)
--tcp-to MS TCP connect timeout (default 800)
--udp-to MS UDP recv timeout (default 900)
--no-color без ANSI-цветов
-v / --verbose подробный вывод
--expect-ip A preflight: адрес, который должны видеть сервисы
--i-know-what-i-am-doing
сканировать несмотря на проваленный preflight; вердикт помечается overridden
--stealth --no-geoip + --no-ct + --udp-jitter, и плюс
inter-probe timing jitter по J3 / SNI consistency /
uTLS dual-probe / AmneziaWG sweep (v2.7.0)
--no-geoip не дёргать HTTPS GeoIP-провайдеров
--no-ct не дёргать crt.sh
--udp-jitter 50-300ms случайная задержка между UDP probe'ами
--j3-subset N отправить N (1..7) случайных проб из восьми J3 на порт
вместо всех восьми (v2.7.0)
--passive минимальный профиль: ПРОПУСКАЕТ J3, uTLS dual-probe,
SNI consistency и AmneziaWG sweep целиком. только
один TLS handshake + GeoIP + CT + traceroute + SNITCH.
меньше всего scanner-образных паттернов на проводе
(v2.7.0)
Все по умолчанию OFF. Дефолтный скан шлёт ровно то же что v2.6.0.
Анти-фингерпринт контекст: v2.7.0 рандомизирует порядок J3-проб через
CSPRNG-Fisher-Yates per scan, фиксированной последовательности empty -> GET -> CONNECT -> SSH -> rand -> tls-invalid -> abs-URI -> 0xff на
проводе больше нет. Рандом в синтетическом ClientHello тоже переехал на
RAND_bytes (с std::mt19937).
--save записать скан в <target>.md в текущей папке
--save <path> записать скан в <path> (тоже как markdown)
ANSI-цвета вырезаются при записи в файл; вывод в терминале не меняется. Содержимое обёрнуто в markdown code-block, так что файл нормально открывается в любом md-вьювере.
Смотрите BUILD.md - полные инструкции, provenance OpenSSL, SHA256. Коротко:
# msys2 UCRT64
pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-openssl mingw-w64-ucrt-x86_64-make
git clone https://github.com/pwnnex/ByeByeVPN.git && cd ByeByeVPN
make windows-staticРелизные zip собираются через
.github/workflows/release.yml из
pinned msys2 образа. SHA256 exe и zip печатаются в release notes
для верификации.
- Connect-scan, не SYN-scan. Target видит полный TCP handshake.
- Cloudflare WARP / CGNAT / корпоративный proxy / локальный TUN-клиент
могут ACK'ать любой порт. Preflight проверяет локальную сторону через
192.0.2.1; после скана 3 случайных порта из 49152-65535 обязаны отказать. Два принятых (или один плюс >60 открытых портов с разбросом RTT < 80 мс) аннулируют вердикт. - uTLS dual-probe шлёт синтетический ClientHello в стиле Chrome, он
не совпадает байт в байт ни с одним текущим Chrome (см. выше).
Raw-socket путь не гоняет TLS 1.3 key schedule, так что для
Chrome-стороны нет сертификата peer'а - детект cert-steering
всё ещё опирается на openssl-handshake. Hello основного
tls_probeэто дефолт OpenSSL. - QUIC-пробы (v2.8.0) шлют настоящий защищённый Initial по RFC 9001 -
key schedule, AEAD-защита payload и header protection байт-в-байт
совпадают с тест-векторами RFC 9001 Appendix A, а внутри CRYPTO-фрейма
едет TLS ClientHello. QUIC-сервер расшифровывает его и отвечает. В
ClientHello есть ALPN
h3и расширение quic_transport_parameters (0x39) с initial_source_connection_id. Проба подтверждает QUIC-эндпоинт и снимает форму ответа, но не доводит полный QUIC-handshake до конца. - GeoIP-провайдеры расходятся и берут данные из общих источников. Их теги VPN, proxy и Tor печатаются справочно и в score не входят.
- Активный скан не видит того, что классификатор видит пассивно (списки адресов, SNI-политика, эвристики первых пакетов, заморозка по объёму). См. docs/COVERAGE.md.
GPL-3.0-or-later. См. LICENSE и NOTICE.
Релизы до v2.5.9 включительно были под MIT. С v2.6.0 проект под GPL-3.0-or-later. Старые MIT-релизы сохраняют свою MIT-лицензию, ретроактивно ничего не отзывается. Релиценз сделан единоличным правообладателем (все коммиты до v2.6.0 написаны pwnnex).