Skip to content

v3.0.2 (2026-05-25)

Choose a tag to compare

@github-actions github-actions released this 25 May 00:11
· 244 commits to main since this release

Security

  • Credentials redacted in lib/proxy.js 'Invalid proxy URL' warngetProxyArgs echoed the raw user-configured proxyUrl when parseProxyUrl returned null. For a URL like socks5://user:pass@host:port that fails parse (mistyped protocol, port out of range, etc.) this emitted the full credentials to stderr. Regex-strips the user:pass@ segment (handles both scheme-prefixed and bare host:port forms) before logging. Same redaction policy as getProxyInfo() and the socks-relay logs already fixed in 3.0.1. The new port-range validation in this release expanded the trigger surface (one more parse-failure path) which made me find the leak.
  • applyProxyAuth debug log redacted — the Auth set for USER@host:port debug-only log line emitted the raw username. Now [redacted]@host:port. Same leak class as above, third site of the same kind.

Added

  • scripts/test-stealth.js --format=json (already shipped in 3.0.1, listed here only because the harness gained a real consumer via the next item) — getRelayStats() exposed from lib/socks-relay.js, returning [{key, port, activeConnections, errors}] per active relay (key with the username segment stripped for safety, IPv6-aware). Diagnostic surface for answering "is the proxy slow because the upstream is saturated or because the scan is opening too many parallel tunnels?" without enabling forceDebug.
  • delay_uncapped: true site-config flag — lifts the 2s post-networkidle delay cap; honors the configured delay up to half the per-URL timeout. Targets sites with setTimeout-deferred lazy ad/tracker loaders (weather.com / cbssports.com class) where late requests fire well past the standard window. Default behavior unchanged (still 2s) so fast sites stay fast.

Fixed

  • Race: late-completing dig/whois validations were orphaned. Per-URL async nettools handlers were scheduled via fire-and-forget setImmediate(() => netToolsHandler(...)); if the handler's full async chain (dig spawn + match check + addMatchedDomain) resolved AFTER the result snapshot ran, the addMatchedDomain call landed in a Set that was no longer referenced by any in-flight result. Most visible symptom: domains appearing in the end-of-scan "Fresh dig:" list with no corresponding rule in the output. Now tracked via trackNetToolsHandler (closure over per-URL pendingNetTools[]) and drained via drainPendingNetTools() with a 3s hard cap (TIMEOUTS.NETTOOLS_DRAIN_TIMEOUT), called BEFORE formatRules at all three snapshot sites (dry-run, success, partial-success/catch path). All three setImmediate call sites (popup observer, main request handler, secondary request handler) migrated.
  • Race: scan-exit hang up to ~100s when a dig/whois lookup hung. Four setTimeouts in lib/nettools.js (outer exec timer, overall 65s timer, whois progressive retry delay up to ~30s, whois server-switch delay ~8s) were not unref'd, so a genuinely-hung lookup that survived the new 3s drain could hold the Node event loop alive for the remainder. All four now unref'd with defensive typeof timer.unref === 'function' guards; the previously-unref'd inner SIGKILL tail-timer makes 5/5 setTimeout sites in the module now safe for scan-exit. Natural-completion paths still clearTimeout on resolution, so this only affects the hung-process case.
  • parseProxyUrl accepted ports > 65535. Now rejects ports outside 1-65535 at parse time, surfacing misconfiguration immediately instead of passing an invalid value to Chromium and getting an opaque downstream error.
  • @version 1.1.0 JSDoc in lib/proxy.js was stale (const said 1.2.0). Aligned to 1.2.0; the const + export then went away in the export trim — see Improved.
  • Site-config delay field was a no-op. nwss.js per-URL handler hardcoded const delayMs = DEFAULT_DELAY regardless of siteConfig.delay. Now reads siteConfig.delay || DEFAULT_DELAY. Visible only with the new delay_uncapped: true flag (without it, the configured value is still capped at 2s as before).
  • "Something went wrong when opening your profile" popup in --keep-open headful mode. --disable-sync was conditionally dropped when --keep-open was set, which let Chrome's sync subsystem initialise against our temp userDataDir (which has no real profile), error out, and pop a modal that blocked the page until dismissed. Three-flag fix: --disable-sync is now always-on (was the only one of five --keep-open-conditional flags actually causing user-visible breakage), plus --allow-browser-signin=false and AccountConsistencyMirror,AccountConsistencyDice appended to the existing --disable-features= list as defence in depth across Chromium's multiple account-subsystem entry points. The other four conditional-on-keep-open flags (--disable-component-extensions-with-background-pages, --disable-component-update, --disable-background-networking, --disable-extensions) stay conditional so user-loaded extensions and live inspection still work normally.
  • Race: socks-relay.ensureRelay concurrent-init created orphan servers. Two concurrent callers for the same upstream both passed the _relays.get(key) check, both created net.Server listeners, both raced to _relays.set — second overwrote first, first server was orphaned (listening forever, never closed by closeAllRelays). Not triggered by current usage (proxy.js's prepareSocksRelays uses a sequential await loop) but a latent bug for future parallel-init paths. Fix: singleflight via new _pendingRelays Map; second caller for an in-flight upstream rides the existing promise. Cleanup uses .finally() on the returned promise (not try/finally inside the IIFE) so a hypothetical sync-throw in the init body can't leave a permanent rejected entry in _pendingRelays. Mirrors the pendingDigLookups/pendingWhoisLookups pattern in lib/nettools.js.
  • Race: handshake watchdog firing during upstream connect orphaned the upstream socket. HANDSHAKE_TIMEOUT_MS = 10000 vs SocksClient.createConnection timeout = 20000 left a 10-second window where the watchdog could fire mid-await, destroy the client, and set settled = true. When the upstream connect then resolved into a fresh socket, the subsequent cleanup() short-circuited via the settled guard, leaving an open TCP connection to the upstream that was never destroyed — held alive until OS-level timeout or remote close. Fix: disarm the watchdog at the phase = 'connecting' transition (client has completed its part of the handshake; SocksClient's own 20s timeout covers the upstream connect), plus a defence-in-depth if (settled) destroy + return after upstreamSock = info.socket for any other path that could call cleanup before upstreamSock registers.
  • Race: closeAllRelays didn't wait for in-flight ensureRelay inits. A relay whose listen() completed AFTER closeAllRelays snapshotted _relays landed in _relays unowned by the close pass — leaked until next call or process exit. Pre-existing, more visible after _pendingRelays became a separate Map for the singleflight. Fix: await Promise.allSettled(Array.from(_pendingRelays.values())) at the head of closeAllRelays so the snapshot is guaranteed-complete. allSettled (not all) because rejected inits have already cleaned up their _pendingRelays entries via .finally().

Improved

  • socks-relay handshake buffer cap (MAX_HANDSHAKE_BYTES = 4096) on pre-piping growth. Prior code absorbed arbitrary bytes for the full 10s handshake watchdog window, letting a hostile/buggy local process pin memory by drip-feeding garbage. Sends a protocol-appropriate failure reply per phase before closing.
  • socks-relay TCP keep-alive on upstream socket (setKeepAlive(true, 60000)). Catches silently-dead upstreams (NAT timeout, mobile-tower drop, proxy crash without FIN/RST) in ~12 minutes (60s idle + kernel-default 9 × 75s probes) instead of the Linux default ~2 hours. Comment is honest about the kernel-default probe math — 60000 is TCP_KEEPIDLE only, not the full detection time.
  • socks-relay auth-misconfig warnensureRelay warns once per unique upstream when username && !password, since RFC 1929 auth will almost certainly fail. Surfaces the misconfiguration at relay start instead of as opaque per-request failures inside forceDebug-gated logs.
  • socks-relay server.maxConnections = 256 cap per relay. Sheds excess Chromium connections at the TCP-accept layer (where HTTP retry handles them cleanly) instead of letting all N tunnels open to the upstream and have the provider silently drop past-quota ones — which looks to the scan like random missed requests.
  • socks-relay per-relay error counter tracked in relayEntry.errors, bumped on SocksClient.createConnection failures, surfaced via getRelayStats() as the errors field. Lets a post-scan reader see "X of N upstream connects failed" without re-running with forceDebug.
  • socks-relay graceful drain on closeAllRelaysDRAIN_TIMEOUT_MS = 2000 window via Promise.race(closePromise, drainTimeout) for in-flight tunnels to flush their last response bytes into Chromium / Puppeteer. Stragglers past 2s get force-destroyed (server.close callback then fires immediately). SIGINT mid-scan no longer amputates in-flight responses, but a hung tunnel can't block exit beyond 2s. Drain timer unref'd so it doesn't hold the event loop open when the close-promise wins the race.
  • lib/proxy.js exports trimmed 12 → 8 — removed getModuleInfo, PROXY_MODULE_VERSION, SUPPORTED_PROTOCOLS, getConfiguredProxy (zero external callers in each case, grep-verified). Mirrors the same trim already done in lib/cloudflare.js. SUPPORTED_PROTOCOLS and getConfiguredProxy stay as module-local since they're used internally.
  • lib/proxy.js code cleanup — two require('./socks-relay') calls consolidated into one destructured import (with closeAllRelays renamed inline), net module require hoisted from testProxy() body to top of file, applyProxyAuth JSDoc enumerates the 5 distinct false return scenarios (caller treating false as "auth failed" would incorrectly retry on the SOCKS5 → relay handles it case).

CI

  • GitHub Release names now include date suffix (v3.0.2 (YYYY-MM-DD)), matching the convention used by the backfilled v2.0.10 through v2.0.66 releases. Auto-applied via the already-computed steps.version.outputs.date in softprops/action-gh-release.