Skip to content

Releases: pwnnex/ByeByeVPN

v2.8.2 - dpi SNI-RST probe + code-audit fixes

12 Jun 13:25

Choose a tag to compare

TSPU/DPI/VPN detectability scanner, v2.8.2 (new probe + audit fixes).

new: dpi - active SNI-RST path probe

byebyevpn dpi <host> [port] measures interference between YOU and the host (your ISP / TSPU), not the host's own detectability. it opens two TLS connections to the same target IP - one with the target SNI, one with a benign baseline SNI. a reset on the target SNI while the benign one completes = active SNI-based RST injection on your path. if SNI-RST is found, a best-effort follow-up splits the ClientHello across TCP segments inside the hostname (Zapret / GoodbyeDPI style) to test whether fragmentation defeats it. guards against a misleading "clean" result when a fake-IP VPN is active (resolved 198.18.x / CGNAT / class-E means tunneled - it tells you to disable the VPN for a real test).

fix: code-audit findings (memory safety + leaks)

a multi-agent, adversarially-verified code audit surfaced and fixed:

  • QUIC length underflow (quic.cpp): ct_len = length_val - pn_len on an unchecked wire varint underflowed size_t into a multi-gigabyte allocation / OOB; now rejected + regression test (latent: only the test round-trip reaches it today).
  • unchecked SSL_new / SSL_CTX_new in five TLS probes (sstp, grpc, https_probe, tls, transport_probe): leaked the socket + UB on nullptr under allocation failure; all now null-check and clean up.
  • DPI SNI-fragmentation off-by-one: split rounds up so a 1-char SNI still leaves a byte in segment 1.

77 unit tests / 3301 assertions green under -Werror; cppcheck clean.

verify

byebyevpn.exe                sha256: 691e5d5325bd7039b41ca67ad66582ee0751a189ce54d885c218ef4ebed45b65
byebyevpn-v2.8.2-win64.zip   sha256: 5549a55e402675d1edcbcec516e17520129ddd7a96d1a1c35717e77257cfb6da

runtime: Windows 10 1803+ / 11 / Server 2019+. no admin, no DLLs, no .NET. Linux/macOS via Wine.

v2.8.1 - drop bogus traceroute TSPU-hop scoring (correctness)

11 Jun 12:54

Choose a tag to compare

TSPU/DPI/VPN detectability scanner, v2.8.1 (correctness patch).

fix: traceroute "TSPU mgmt-subnet" removed from the verdict

earlier versions penalised the score (and raised a TSPU-verdict B-tier hit) when a traceroute hop landed in 10.X.Y.[131-235]/[241-245]/254, calling it a "TSPU management subnet on the path". that was wrong (thanks to the reporter on ntc.party):

  • TSPU is a bump-in-the-wire DPI box, transparent at L3: it does not decrement TTL and never appears as its own traceroute hop, so it cannot be detected by trace at all.
  • 10.x hops are ordinary operator link addressing and mean nothing about TSPU.

so the score penalty and the TSPU mgmt-subnet in traceroute verdict rule are both removed. private-10.x hops are now an honest informational note only (no score impact, no TSPU-tier impact). the count is still in --json for reference.

everything else is unchanged from v2.8.0. 76 unit tests / 3299 assertions green under -Werror.

verify

byebyevpn.exe                sha256: d30317028cc67faef76ef26cdb5a3fe128c45fd0d1d84eed10985ada0fca94b6
byebyevpn-v2.8.1-win64.zip   sha256: 54843c5c0cd80f728317bdb4ea19cad944f71e64bee4c5497073c7fff3f52d7f

runtime: Windows 10 1803+ / 11 / Server 2019+. no admin, no DLLs, no .NET. Linux/macOS via Wine.

v2.8.0 - config audit, real QUIC, subnet sweep, gRPC/WS/NaiveProxy detects, FP fixes

11 Jun 07:41

Choose a tag to compare

TSPU/DPI/VPN detectability scanner, v2.8.0.

new

  • audit-config : predict a config's TSPU detectability before deploy. parses Xray/sing-box JSON + WireGuard/AmneziaWG .conf, reuses the verdict engine, --json output, exit code by verdict tier.
  • real QUIC v1 Initial (RFC 9001) : Initial key schedule + AEAD + header protection, byte-exact vs RFC 9001 Appendix A; QUIC ClientHello with quic_transport_parameters; Version-Negotiation probe + response parser; the Hysteria2/QUIC probe now sweeps a curated multi-port set. (pre-alpha function)
  • sweep <cidr> : light-probe a subnet and cluster hosts by JA4S + cert.
  • grpc : HTTP/2 + gRPC transport probe (VLESS/VMess-gRPC). (pre-alpha function)
  • NaiveProxy / forward_proxy (407 over TLS) and VLESS/VMess-WebSocket (101 upgrade) detects. (pre-alpha function)

fixes / calibration (verified no false positives on real RU sites)

  • CONNECT answered with 3xx/4xx/5xx is no longer flagged "open HTTP proxy" (a real open proxy returns 2xx).
  • cert brand-impersonation is only asserted when ASN/GeoIP data is present.
  • a TLS alert to a malformed ClientHello is not "raw non-HTTP proxy framing".
  • canned-fallback requires the same reply to BOTH a valid HTTP request and garbage.
  • SNITCH latency is now vantage-independent (#10) : far-east / non-EU observers no longer get a false geo-conflict; the absolute country RTT bands apply only when the observer looks Moscow/EU-ish.

internals

  • json: objects stored as parallel key/value vectors (a std::pair of an incomplete type is ill-formed; clang / CI-ASan rejected it).
  • 76 unit tests / 3299 assertions green under -Werror; ASan+UBSan green; tool-string wire audit still 1/3.

verify

byebyevpn.exe                sha256: 81b9c82d8757c72ee8d8acc9cfa16765221ee76a19d4ba005827510c937c8563
byebyevpn-v2.8.0-win64.zip   sha256: b2ba940b23dc2e47ff9272dd80d3924f17746df5ea941334f9e21810e120008e

runtime: Windows 10 1803+ / 11 / Server 2019+. no admin, no DLLs, no .NET. Linux/macOS via Wine.

v2.7.0 - anti-fingerprint pack: probe shuffle, --stealth jitter, --j3-subset, --passive

15 May 10:34

Choose a tag to compare

verification

4d1ab9d99851279daa855247cbc4fc6d5b9bf0e0a2c3ff303dabfed9dbd03f27 *byebyevpn.exe
f17e3130165c729948857db941ab86d3f4dc694b8ac0dc9a64e02fde05472930 *byebyevpn-v2.7.0-win64.zip
3f4632157d31e22eaf6ee4722fc75942658a20d0daa68a51484d4a43f60aa50f *byebyevpn-sbom.json

Zip contains the exe + LICENSE + NOTICE + README + CHANGELOG + the CycloneDX SBOM. See BUILD.md for the reproducibility recipe.


Changelog

v2.7.0 - 2026-05-15

an anti-fingerprint release. the v2.6.0 detection features ship the
same as before; what changes is what the scanner LOOKS LIKE on the
wire. probe order is randomized, inter-probe timing jitter lands under
--stealth, two new scope-cut flags let the operator narrow the probe
surface, and the Chrome 131 ClientHello randomness now comes from a
CSPRNG instead of std::mt19937. nothing on the wire changes by
default — every behavioural change is opt-in. detection capability is
unchanged.

why this matters

the scanner emits no tool-identifying strings on the wire (CI gates on
the grep audit; v2.6.0 already passes at 1/3). what it DID emit was
behavioural patterns:

  • eight J3 probes per TLS port in a fixed order
  • chrome-flavored TLS handshake immediately followed by an
    openssl-flavored one, to the same port
  • ten sequential TLS handshakes with rotating SNIs in the SNI
    consistency loop
  • a twelve-datagram AmneziaWG S1 sweep on a single UDP port

each of those is a recognisable scanner shape. v2.7.0 takes them apart.

new: probe-order randomization (src/scan/j3.cpp)

the eight J3 probes are now shuffled per scan with a CSPRNG-backed
Fisher-Yates (crypto_shuffle in src/common/util.h). the per-port
sequence is different every run, so a defender cannot fingerprint a
fixed empty -> GET -> CONNECT -> SSH -> rand -> tls-invalid -> abs-URI -> 0xff order anymore. the J3 implementation was moved to an enum-tagged
dispatcher, which made the shuffle a one-liner.

new: --stealth becomes real

before v2.7.0 --stealth only set the existing --no-geoip +
--no-ct + --udp-jitter triplet. it now also applies inter-probe
timing jitter across every multi-probe phase:

  • 250-1500 ms between J3 probes
  • 200-1200 ms between SNI consistency handshakes
  • 300-1500 ms between the chrome and openssl uTLS dual-probe halves
  • 150-900 ms between AmneziaWG S1 sweep datagrams

a new helper stealth_sleep_ms(min, max) in src/common/util.cpp
picks the delay from RAND_bytes and is a NO-OP when --stealth is
off. scans take longer in stealth mode (an order of seconds per port
extra), but the burst patterns smear into background-noise timing.

new: --j3-subset N

run a random N of the eight J3 probes per port instead of all eight,
where 1 <= N <= 7. combined with the shuffle, the per-port pattern is
much smaller and the probability of two scans sending the same order +
subset is 1 / (P(8,N)) (~few thousand for N=4). drops the canned-J3
signature flat.

new: --passive mode

minimal-probe profile. SKIPS the J3 phase entirely, the uTLS dual-probe,
the SNI consistency loop and the AmneziaWG S1 sweep. one base TLS
handshake + GeoIP + CT lookup + traceroute + SNITCH only. the scan
output marks each skipped phase explicitly. fewest scanner-shaped
patterns the tool can emit while still scoring.

chore: Chrome 131 ClientHello uses CSPRNG

the random ClientRandom, legacy session_id and x25519 key_share private
in src/scan/chrome_ch.cpp now come from RAND_bytes instead of
std::random_device + std::mt19937. functionally a no-op (the raw
chrome path does not run the TLS 1.3 key schedule, so the predictability
of mt19937 never had a security consequence), but the consistency story
is cleaner: every random byte the tool emits is CSPRNG-sourced.

what stays the same

  • detection capability — every v2.6.0 signal works exactly as before
  • the default-off posture — without any new flag, scan behaviour is
    bit-for-bit what v2.6.0 emitted
  • --json, exit codes, SBOM, minisign signing flow

deferred to v2.7.x

  • byte-accurate Chrome 131 with encrypted_client_hello (0xfe0d) and
    X25519MLKEM768 keyshare. the JA4 already matches a real recent
    Chrome (t13d1516h2) and our hello is accepted in practice; ECH +
    PQ is a fidelity polish for very strict uTLS-enforcing servers
  • JA4-Q (QUIC fingerprint) for Hysteria2
  • multi-flavour dual-probe (Firefox, Safari) in addition to Chrome
  • TLS 1.3 key schedule on the raw chrome path to recover the encrypted
    certificate

v2.6.0 - byte-accurate Chrome 131 uTLS, GPL-3.0, --json, signed builds

15 May 08:11

Choose a tag to compare

verification

ccec6d0510899c6c5b86e9ef91841dbd09e8dd76f28d3437f5436a38c3623b84 *byebyevpn.exe
88fac43b3c2fa204ba78afb4faef55be6de6a594835cebac69825e8bed99ac64 *byebyevpn-v2.6.0-win64.zip
0d83e640710799468e3964a9e6530ce35751eb32d014e2ba4b5e9fb8d605a273 *byebyevpn-sbom.json

the zip contains the exe + LICENSE + NOTICE + README + CHANGELOG + the CycloneDX SBOM. see BUILD.md for the full reproducibility recipe.


Changelog

v2.6.0 - 2026-05-14

a focus release. the scope is narrowed to the modern signature-less
tunnel set, the project picks up a real test + static-analysis pipeline,
and three new detection / output features land. the license also
changes here.

license: MIT -> GPL-3.0-or-later

releases up to and including v2.5.9 were MIT. from v2.6.0 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). see the new
NOTICE file for the full licensing history. every source file carries
an SPDX-License-Identifier: GPL-3.0-or-later header.

scope cut: UDP probes

the UDP probe set is now WireGuard / AmneziaWG / Hysteria2 only. the
legacy probes (OpenVPN HARD_RESET, IKEv2 ISAKMP, L2TP SCCRQ, plain DNS,
vanilla QUIC, TUIC) were removed. those target protocols with fixed-port
/ fixed-header signatures that any DPI already catches; probing for them
added scan time without adding detection value for this tool's niche.
the verdict engine, the stack-identification ladder and the TSPU rule
table were trimmed to match.

scope cut: GeoIP providers

the four HTTP-only GeoIP providers (api.2ip.me, ip-api.com,
ip-api.com/ru, api.sypexgeo.net) were removed. a plaintext HTTP GeoIP
query exposes the looked-up IP to every on-path observer between the
scanner and the provider, which on a censored network is exactly the
leak this tool is meant to help avoid. the five remaining providers
(ipapi.is, iplocate.io, freeipapi.com, ipwho.is, ipinfo.io) are all
HTTPS, so the lookup payload stays encrypted in transit.

new: JA4S backend-stack classifier (src/scan/ja4s_db.{h,cpp})

the uTLS dual-probe already computed JA4S (a ServerHello fingerprint).
v2.6.0 adds a classifier on top: an exact seed table names a specific
stack when the ext-hash is known (Cloudflare edge is seeded from an
actual v2.5.9 observation), and a structural decoder emits a coarse
TLS-version / extension-count family when it is not. the seed table is
deliberately small and honest, meant to grow from community-submitted
scans, not from guesses.

new: AmneziaWG S1 deep-probe (src/scan/amnezia_probe.{h,cpp})

a junk-prefix size sweep on the default WireGuard port. AmneziaWG shifts
the real WG initiation packet behind S1 random junk bytes; a vanilla
WG listener drops that, only a listener with the matching S1 accepts
it. the sweep tries a dozen S1 sizes and reports which one answered,
which IS the server's configured S1 obfuscation parameter. if a
non-zero S1 answers while vanilla WG is rejected, the verdict engine
flags it as a recovered obfuscation parameter. no other tool recovers
S1 remotely.

new: --json output + meaningful exit codes

--json emits a flat, stable JSON document on stdout (tool / version /
target / score / label / stack / tspu / signals / geo / open_tcp / udp /
tls_ports / snitch / trace / tcp_fp / amnezia_sweep). in --json mode
the human-readable scan output is moved to stderr so stdout is
pipe-clean.

a completed full scan now exits with a verdict-tier code so wrapper
scripts can branch without parsing output:

0  CLEAN (score >= 85)        2  SUSPICIOUS (50-69)
1  NOISY (70-84)              3  OBVIOUSLY-VPN (< 50)
64 usage error (no target)

new: byte-accurate Chrome 131 ClientHello (src/scan/chrome_ch.{h,cpp})

the v2.5.9 uTLS dual-probe approximated the Chrome side by tweaking an
OpenSSL SSL_CTX (cipher list, group order, sigalgs order). that is not
a real Chrome hello: the extension order is OpenSSL's, there is no GREASE
injection, no padding extension, no GREASE key_share. a uTLS-enforcing
Reality server tells the two apart and the v2.5.9 probe could not.

v2.6.0 hand-builds the actual wire bytes Chrome sends: GREASE values at
the spec positions (cipher list, supported_groups, key_share,
supported_versions, two bookend extensions), the Chrome extension set in
order, a GREASE-prefixed x25519 key_share, and the padding extension
sized the way BoringSSL sizes it. the random, the legacy session id, the
key_share public value and the GREASE selections are randomized per call.
the hello is sent over a raw socket and the server's plaintext
ServerHello is read back directly.

the "chrome accepted, openssl rejected" split is now a real signal: a
server that answers a byte-accurate Chrome hello but drops the
openssl-default one is enforcing a browser fingerprint profile. that is
the main 2026 Reality detection technique. one extension is intentionally
omitted (encrypted_client_hello / 0xfe0d): a GREASE ECH carries an
HPKE-shaped body Chrome fills from its config cache, so a static one
would be less accurate than none. the resulting JA4 is t13d1516h2, a
real recent-Chrome fingerprint, never the openssl-default JA4.

infrastructure: tests, fuzzing, sanitizers, signed releases, hardened CI

  • tests/ with doctest (single-header, MIT, not linked into the shipped
    binary). 34 test cases covering the platform-agnostic logic: string
    helpers, the JA4 byte parsers + builders, the JA4S classifier, the
    Chrome ClientHello builder (including its GREASE / key_share
    invariants), the TSPU recognisers, the port-list builder, the brand
    helpers. make test.
  • fuzz/fuzz_ja4.cpp: a libFuzzer harness for the JA4 byte parsers,
    which consume attacker-controlled handshake bytes. built under
    ASan + UBSan. make fuzz.
  • make test-asan: the full unit-test suite under AddressSanitizer +
    UndefinedBehaviorSanitizer with -fno-sanitize-recover, so any UB is
    a hard failure. runs as its own Linux CI job; the Windows release
    gates on it.
  • the platform-agnostic logic modules (util, tspu, ja4,
    chrome_ch, ja4s_db, brand, ports, config) are now
    cross-platform: the Windows-only wide-char helpers are #ifdef _WIN32-guarded and _stricmp is abstracted, so the test + analysis
    build runs on Linux.
  • the CI workflow runs a Linux lint-and-test job (cppcheck whole tree,
    unit tests with -Werror, clang-tidy with --warnings-as-errors on
    the agnostic modules, a time-boxed libFuzzer smoke pass), a Linux
    sanitizers job (test-asan), and the Windows static-exe release job
    which gates on both passing.
  • every release now ships byebyevpn-sbom.json, a CycloneDX 1.5
    software bill of materials (exe sha, pinned OpenSSL package + sha,
    compiler, source revision), and, when a MINISIGN_SECRET_KEY repo
    secret is configured, minisign .minisig signatures on the exe, the
    zip and the SBOM. the reproducible-archive verification from earlier
    releases stays. see BUILD.md.
  • pre-existing -Wunused warnings were cleaned out so -Werror is
    green (NOMINMAX redefinition guard, dead saw_other / phys_up /
    three orchestrator counters).

no on-the-wire fingerprint change

the new probes do not add tool-identifying bytes. the Chrome ClientHello
builder emits exactly the bytes Chrome emits. the AmneziaWG sweep sends
only well-formed WG-shaped datagrams with randomized bodies. the JA4S
classifier and the JSON serializer are pure in-process logic. the
brand-string audit grep still passes at 1 / 3 (only the --help
printf).

upgrade

--json and the new exit codes are additive. the removed UDP probes
and GeoIP providers mean a v2.6.0 scan of a host running OpenVPN / IKE /
L2TP on default ports will no longer name those protocols by their UDP
handshake; the TCP-side signals (open default port, etc.) still fire.


v2.5.9 - uTLS dual-probe + JA4/JA4S + TCP behavior fingerprint

08 May 09:22

Choose a tag to compare

three new detection vectors. no payload changes to the wire shapes introduced in v2.5.8 and earlier; only fingerprint-side analysis on top of existing handshake bytes.

before scanning, disable any active VPN / Zapret / GoodbyeDPI / proxy on the host. otherwise SNITCH RTT is wrong, ClientHello bytes can be rewritten by Zapret-class fragmentors mid-stream breaking JA4 computation, and GeoIP queries leave through the wrong exit IP. see the new "Prerequisites" section in README.

new detection vectors

1. uTLS dual-probe (src/scan/utls.{h,cpp})

per TLS port the orchestrator runs TWO TLS handshakes:

  • chrome-flavored SSL_CTX: cipher list ordered Chrome-style (TLS 1.3 AES/CHACHA20 first, then ECDHE-ECDSA/RSA), supported groups X25519:P-256:P-384:P-521, sigalgs ECDSA+SHA256:RSA-PSS+SHA256:..., ALPN h2,http/1.1. NOT byte-identical to a real Chrome ClientHello (extension order is OpenSSL's, no GREASE injection from us, no PSK resumption) but enough to differ from default OpenSSL JA3 by cipher hash and group order.
  • openssl-default SSL_CTX: the same shape the rest of the tool uses.

both probes capture raw ClientHello and ServerHello bytes via SSL_set_msg_callback and feed them to parse_client_hello / parse_server_hello to produce JA4 (client) and JA4S (server) per the FoxIO spec (https://github.com/FoxIO-LLC/ja4).

verdict signals:

  • cert_differs (different cert per flavor) → flag_major(-22)
  • only_chrome_ok / only_openssl_ok (one handshake rejected) → flag_major(-18)
  • ja4s_differs (same cert, different ServerHello shape) → flag_minor(-9)

2. JA4 / JA4S (src/scan/ja4.{h,cpp})

full FoxIO-spec implementation:

  • ClientHello and ServerHello byte parsers (RFC 8446 layout)
  • GREASE filter: (v & 0x0f0f) == 0x0a0a
  • supported_versions extension parsing for real TLS version (handles TLS 1.3 ServerHello with legacy_version=0x0303)
  • JA4_a header: t<ver><sni-flag><cipher_count><ext_count><alpn[:2]>
  • JA4_b: sha256(sorted_ciphers_csv)[:12]
  • JA4_c: sha256(sorted_exts_no_sni_no_alpn [+ "_" + sigalgs_in_order])[:12]
  • JA4S analogue for ServerHello
  • JA4H builder for HTTP requests (cookie + referer flags + lang)

3. TCP behavior fingerprint (src/scan/tcpfp.{h,cpp})

per host, no admin, no raw socket, no Npcap / WinDivert dependency:

  • 6 sequential tcp_connect calls to the lowest open port → handshake distribution (median, min, max, stddev, bimodal flag)
  • WSAIoctl(SIO_TCP_INFO_v0) on a fresh handle → peer's advertised SndWnd and negotiated Mss
  • one connect to a closed-port hint (65000 / 1) → classifies behavior as rst-fast / rst-slow / drop

verdict signals:

  • bimodal handshake stddev ≥ 25ms → flag_minor(-6) "userspace TCP stack in path (TUN / gvisor / sing-box-tun)"
  • closed-port drop policy → informational note "operator-grade firewall (TSPU ACL drop) or strict cloud SG"

on-the-wire posture (issues #4 / #5 carried forward)

the new probes do NOT add tool-identifying bytes:

  • chrome-flavored SSL_CTX differs from openssl-default in parameter ordering only. no User-Agent, no custom extension, no static SNI, no fixed session ID. SNI value is the target's hostname (same as tls_probe).
  • ClientHello bytes captured by SSL_set_msg_callback stay in-process. JA4 / JA4S computed locally; nothing exported.
  • TCP fingerprint module never sends a custom packet. just regular connect() calls and one WSAIoctl on a local socket handle.

CI (.github/workflows/release.yml) brand-string scan still passes with 1 / 3 matches (the --help printf in src/app/cli.cpp). build is byte-static: objdump -p shows zero leaked mingw/openssl runtime DLLs.

artifact integrity

sha256(byebyevpn.exe)              = b8301c8eae33174ad9fef0a12e1d98aa5ac138968bd5d417b3134582dd844f27
sha256(byebyevpn-v2.5.9-win64.zip) = c9e6a563fbf61b1121a4cecf261f630f9ef07a38e0020d04b6a8b7d50f409daa

build provenance: GitHub Actions windows-latest + msys2 UCRT64 + the pinned mingw-w64-ucrt-x86_64-openssl-3.6.2-2 static archives shipped in build-win/. workflow verifies both the in-repo archives and a fresh fetch from repo.msys2.org are byte-identical before linking.

upgrade

drop-in: same CLI, same flags, same exit codes, same --save markdown header. existing CI / wrappers continue to work. v2.5.8 -> v2.5.9 runs against the same target produce the same verdict block plus three new lines per TLS port (uTLS dual-probe, JA4 openssl, JA4 chrome) and one new section per scan (TCP stack fingerprint).

tested on: Russia 🇷🇺

v2.5.8 - modular tree refactor + shared SSL_CTX

07 May 21:31

Choose a tag to compare

internal-only release. no behaviour, no scoring, no on-the-wire payload change. one big refactor + a few perf wins.

refactor: monolith -> modular tree

src/byebyevpn.cpp (5799 lines, one translation unit) is split into 28 .cpp + 27 .h files under src/:

src/
  main.cpp
  common/      config, console, util, tspu, winhdr
  net/         dns, tcp, udp, http, icmp
  geoip/       geoip (9 providers)
  scan/        ports, tcp_scan, udp_probes, fingerprint, tls, tls_ctx,
               https_probe, sni, brand, j3, snitch, ct
  local/       adapters, routes, processes, configs
  app/         report, target, orchestrator, cli

every struct, every probe, every output string stays byte-identical to v2.5.7. score, label, signal lists, TSPU-tier table, hardening text all stable.

motivation:

  • 5800-line single TU rebuilt in full on every change. the split lets make -j8 rebuild only what changed.
  • lookups by feature (cert impersonation, J3 analysis, snitch latency) used to need a global scroll. each module is now self-contained.

perf: shared SSL_CTX

every TLS-bearing probe (tls_probe, https_probe, sni_consistency, each foreign-SNI iteration inside it) used to allocate + free its own SSL_CTX. that's ~11 ctx alloc/free cycles per host on the SNI consistency phase alone.

new scan/tls_ctx.{h,cpp} exposes a single shared_tls_client_ctx() with std::call_once lazy init (verify=none, min=TLS 1.2, set once). measured ~30-40ms off the SNI consistency phase on warm cache.

perf: misc tightening

  • icontains() no longer allocates two lower-cased copies per call; scans with std::tolower per char.
  • https_probe lower-cases the response body once, not per header lookup.
  • printable_prefix reserve()s min(s.size(), lim) upfront.

no behaviour change

  • TLS handshake bytes identical (same TLS_client_method, set_min_proto_version, SNI / ALPN / curve set).
  • UDP probes identical payloads, same RAND_bytes randomization, same jitter knob.
  • TCP scan / fingerprint / J3 / SNITCH / CT lookup: identical packets on the wire.
  • Verdict engine: same flag thresholds, same DPI matrix output.

upgrade is drop-in: same CLI, same flags, same exit codes, same on-screen output, same --save markdown header.

artifact integrity

sha256(byebyevpn.exe)              = 745e4074d8e1554543d4449d44716df1ef6ef2e8b64cc7d794035d5685a2b616
sha256(byebyevpn-v2.5.8-win64.zip) = 876db289a9ddcdf2783068b09ddfb52cbdce276204040bfa14faaa8df74b84ab

build provenance: GitHub Actions windows-latest + msys2 UCRT64 + the pinned mingw-w64-ucrt-x86_64-openssl-3.6.2-2 static archives shipped in build-win/. workflow verifies both the in-repo archives and a fresh fetch from repo.msys2.org are byte-identical before linking.

tested on: Russia 🇷🇺

ByeByeVPN v2.5.7

27 Apr 09:14

Choose a tag to compare

v2.5.7

small feature drop on top of v2.5.6. one user-requested feature
(#7), no fingerprint-class
changes.

full details in
CHANGELOG.md.

--save scan output to file (#7)

new flag for keeping a copy of the scan output:

byebyevpn --save example.com         # writes example.com.md
byebyevpn --save scan.md example.com # writes scan.md (explicit path)

implementation: tee-style. every printf/puts call goes both to
stdout (with ansi colors as before) AND to the save file (with ansi
escapes stripped). terminal output is byte-identical to the no-save
case. file is wrapped in a markdown code block so it renders cleanly
in any md viewer.

filename rules:

  • --save with no argument -> <target>.md in the cwd
  • --save <path> -> explicit path (must not start with -)
  • target sanitization: : / \ * ? \" < > | -> _
  • --save for local / interactive without a target -> falls back
    to byebyevpn-scan.md

sha256

byebyevpn.exe                  b7024fb8a1d06bbbc0821364a39a0a95cb5c0a70bad00d35217fcb4c4142e85b
byebyevpn-v2.5.7-win64.zip     5f7eddaab97bd2cb08a715f373bb0fc3292c1858603b121c4b614859b893882a
static openssl provenance: same as v2.5.5/v2.5.6 -
\`mingw-w64-ucrt-x86_64-openssl-3.6.2-2-any.pkg.tar.zst\` sha
\`4b919cc9ed46b55c465a39204bf5034f2d8be931840c6a62ae71b1554bbea9a5\`
(see [BUILD.md](https://github.com/pwnnex/ByeByeVPN/blob/main/BUILD.md)).

note

no protocol changes, no scoring changes, no openssl bump, no
on-the-wire fingerprint changes. save-file md header is deliberately
brand-free (`# Scan report` / `Scanner version: vX.Y.Z`) so
the public audit grep over the source stays at the same count as
v2.5.6:

\$ grep -nE 'ByeByeVPN|BYEBYEVPN|BBVPN|BBV|pwnnex' src/byebyevpn.cpp
1:     // ByeByeVPN — full VPN / proxy / Reality detectability analyzer
5325:  printf(\"ByeByeVPN — full TSPU/DPI/VPN detectability scanner\n\n\");

both are non-network. ci enforces `<= 3` matches.

ByeByeVPN v2.5.6

21 Apr 19:25

Choose a tag to compare

v2.5.6

small feature drop on top of v2.5.5. inspired by
DanielLavrushin/tspu-docs
methodology (operator-level tspu documentation) and one user request
(#6).

full details in
CHANGELOG.md.

q-skip for tcp scan phase (#6)

press q / Q / Esc during phase [3/8] and the remaining ports
are skipped, pipeline moves to the next phase. useful on --full
1-65535 when you already see the relevant ports are below some
threshold.

bgp-blackhole detector (tspu type B)

tspu type B blocks hosts via bgp-pushed ip-lists on the operator
balancer, not via DPI (tspu-docs ch. 7.3.2). visible as "all ports
timeout, zero RST". tcp_connect() now distinguishes
timeout/refused/other, and the verdict engine raises tier A
+40 when ≥99% of ≥1000 scanned ports timed out with zero RST.

tspu mgmt-subnet hops in traceroute

tspu sites use a stable 10.<region>.<site>.Z layout where
Z ∈ [131..235, 241..245, 254] for filters/balancers/ipmi/spfs
(tspu-docs ch. 10). trace_hops() now counts private hops matching
this layout, verdict flags them tier B (+5 per hop).

http redirect-page blacklist (tspu type A)

tspu type A redirects HTTP/:80 via 302 to operator warning page
(rkn.gov.ru, warning.rt.ru, blocked.rt.ru, 185.76.180.75,
etc. - tspu-docs ch. 5.1.5). fp_http_plain() now parses the
Location: header, checks against a hardcoded blacklist, verdict
flags matches tier A (+30).

sha256

byebyevpn.exe                  29b06e0b7a0dd90167e9265f209cdf350df521806761ccd4cffa0b9960200b60
byebyevpn-v2.5.6-win64.zip     2d84798240a0f5b4b312db7ebd069995d956ec5793c26fa77142b94032f141dd

static openssl provenance: same as v2.5.5 -
mingw-w64-ucrt-x86_64-openssl-3.6.2-2-any.pkg.tar.zst sha
4b919cc9ed46b55c465a39204bf5034f2d8be931840c6a62ae71b1554bbea9a5
(see BUILD.md).

note

nothing fingerprint-class changed. outbound requests still minimal
(zero extra headers on the wire). all new detectors consume only
target-side bytes (already attacker-controlled, already bounded).
security audit after the change - clean.

ByeByeVPN v2.5.5

21 Apr 02:53

Choose a tag to compare

v2.5.5

post-ntc.party audit release.
closes #5 and
#4. full details in
CHANGELOG.md.

headers (#5)

the chrome-131 header blob was itself a static fingerprint. http_get()
now sends zero extra headers on the wire: bare GET /path HTTP/1.1 +
Host:. no ua, no accept, no accept-encoding, no sec-fetch, no
upgrade-insecure. winhttp-specific: session agent empty +
WINHTTP_OPTION_USER_AGENT="" force-override + WINHTTP_NO_ADDITIONAL_HEADERS
in SendRequest. auto-gzip via WINHTTP_OPTION_DECOMPRESSION stays
(server may compress on its own; we don't advertise).
https_probe() + fp_http_plain() are minimal too (Host, Accept: */*,
Connection: close).

2ip.io (#5)

anti-bot html fallback removed. geo_2ip_ru() hits
api.2ip.me/geo.json directly now - same url from Dreaght's curl example.

build (#4)

  • openssl 3.6.1-33.6.2-2 (current upstream)
  • build-win/libssl.a + libcrypto.a tracked in git, bit-identical to
    the public msys2 pkg
  • BUILD.md sha256 match the public package, verifiable via
    pacman -S mingw-w64-ucrt-x86_64-openssl or direct
    curl https://repo.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-openssl-3.6.2-2-any.pkg.tar.zst

ci

.github/workflows/release.yml builds on tag push from a pinned msys2
ucrt64 image. verifies build-win/*.a are bit-identical to the pacman
pkg, objdump -p checks no mingw/openssl dll deps, greps source for
tool-fingerprint strings (fails the build on any extra match), writes
exe + zip sha256 to the release body. starting with this release all
future builds come out of ci.

audit fixes

  • fp_socks5 read past reply[1] when a server returned exactly 1 byte
  • 9 cli error-exit paths skipped WSACleanup (single done: label now)
  • --threads/--tcp-to/--udp-to clamped to 1+ (negative wrapped
    SO_*TIMEO to ~49 days)
  • scan_tcp open.size() read outside the mutex - snapshot under lock
  • !err.empty() == false rewritten as plain err.empty()

docs

sha256

byebyevpn.exe                  61bd4562baf727ec02cf673471883be85ddc9adfe22fa71cd8fb38b2ef3fc709
byebyevpn-v2.5.5-win64.zip     b1a44f7f4ea13655b745dc99b37e937ade3e34d6fd8c53b44295c23e4e2b4557

static openssl provenance:
mingw-w64-ucrt-x86_64-openssl-3.6.2-2-any.pkg.tar.zst sha256
4b919cc9ed46b55c465a39204bf5034f2d8be931840c6a62ae71b1554bbea9a5
(see BUILD.md).

notes

  • v2.5.4 binary was +200 kb vs public rebuild - locally built without
    ci. not fixable retroactively. v2.5.5+ comes strictly from the ci
    workflow.
  • tls ja3 still ≠ chrome. needs a utls port in c++. documented in
    SECURITY.md
    as a known open threat.