Skip to content

fix(proxy): send SOCKS4 and caller-supplied URL fetches through the session proxy - #1629

Merged
rmyndharis merged 2 commits into
mainfrom
fix/baileys-socks-and-url-proxy
Sep 16, 2026
Merged

rmyndharis merged 2 commits into
mainfrom
fix/baileys-socks-and-url-proxy

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Two egress gaps on a proxied session. A Baileys session with a SOCKS4 proxy still fetched from the gateway's own address, because the HTTP client had no SOCKS4 transport: inbound media was skipped entirely and arrived as the omitted marker, the WhatsApp Web version was not looked up, and the history-sync payloads, app-state blobs and a product card's image URL went direct. On every scheme, a media URL passed to a send route or to POST /api/sessions/{sessionId}/media/convert/voice|video, and the link preview of a text send, were fetched by the gateway outside the session's proxy, because that fetch belongs to the request rather than to the session.

  • One undici connector over the socks package (added as a direct dependency; it was already in the tree under socks-proxy-agent) covers SOCKS4 and SOCKS5, so createProxyDispatcher now returns a dispatcher for all four schemes. The SOCKS4 null path and undici's experimental SOCKS5 agent, with the percent-decoding workaround its credential handling needed, are gone, along with the skip branches they forced on the inbound media download and the version resolver.
  • DNS locality per scheme: SOCKS4 has no hostname form, so an unvetted destination is resolved locally to IPv4 before the request goes out and an all-IPv6 destination is refused with a named error; SOCKS5 keeps the hostname on the wire so the proxy's own resolver decides. The comment in socksDestinations records the choice and notes that socks-proxy-agent, which the session's WebSocket rides, resolves socks5:// locally too.
  • Credentials on a socks4:// proxy are reported at session start, the way the whatsapp-web.js engine already reports SOCKS credentials Chromium cannot use: SOCKS4 has no authentication step, so the user name travels as the connect request's user id and the password is dropped.
  • withSafeFetch takes the session's proxy and uses it on every branch that opens a socket: the pinned-hostname path, an IP literal with nothing to pin, each hop of a followed redirect chain, and the guard-off path. An unusable proxy value throws rather than falling back to a direct fetch; the caller sees the send or conversion fail, and a link preview is simply omitted.
  • loadRemoteMediaBuffer and generateSafeLinkPreview take the session proxy as a required argument through the adapters, so a send by URL on either engine, a url conversion and every text-bearing send's preview (a plain send, a reply, an edit) all leave through it. The whatsapp-web.js path shares the same helper, so its five media call sites are covered too.
  • Through SOCKS the whole vetted list is what the proxy is asked to connect to, dialled in resolver order, so the guard's destination check still binds and the address-family failover the direct path gets from happy-eyeballs survives: a dual-stack host resolves AAAA first on an IPv6-capable gateway, which neither a SOCKS4 proxy nor an IPv4-only SOCKS5 proxy can reach. Through an HTTP or HTTPS proxy the destination is named in the CONNECT line and resolved by the proxy, so pinning is not expressible there; the scheme and blocked-address checks still run before any socket is opened. The unproxied path is unchanged: the same pinned Agent, and no dispatcher for an IP literal.
  • Which proxy a route uses: the one the session's live engine was started with, recorded in EngineRegistry beside the engine, because PATCH /proxy edits the row without restarting the engine and a fetch attributed to a running session must leave from the same address as the rest of its traffic. With no live engine there is no such egress, so the stored row is used, which keeps the fetch off the gateway's address for a session an operator has proxied. That row read happens before the conversion's URL error mapping, so a database failure stays a 500 instead of becoming a 400 carrying the driver's message.

Behaviour change for operators: a caller-supplied URL now leaves through the session proxy. SESSION_PROXY_URL_FETCH=false restores the direct fetch for a proxy that only routes to WhatsApp and cannot reach arbitrary media hosts; the key is validated at boot like its siblings, forwarded by both compose files, and documented in .env.example, docs/06 and the upgrade-hazards table.

Verified with real in-process SOCKS4 and SOCKS5 servers that record what the client asked for and relay every tunnel to a local origin: an http destination end to end on both schemes, an https destination by the TLS ClientHello arriving on the tunnelled socket, the resolved IPv4 address (not merely its shape, which the SOCKS4a form would also satisfy) on the wire for SOCKS4 against the hostname for SOCKS5, the IPv4 entry of a dual-stack vetted list on SOCKS4, the fallback to the next vetted address against a SOCKS5 proxy that answers "host unreachable" for IPv6, the vetted address overriding the URL's own, SOCKS5 username/password authentication with percent-encoded credentials, and the SOCKS4 user id. The guard is exercised on all four branches plus the unproxied one, which must never touch the proxy. Every new behaviour was mutation-checked by reverting its line and confirming the guarding spec fails. The CI gate set was run locally.

The one package-lock.json line outside the socks entry ("dev": true on @pkgjs/parseargs) is pre-existing drift that npm install --package-lock-only reconciles from the base commit with no package.json change; @pkgjs/parseargs is an optional dependency of jackspeak with a working fallback, so npm ci --omit=dev is unaffected.

Fixes #1626

…ession proxy

A Baileys session with a SOCKS4 proxy still fetched from the gateway's
own address: the HTTP client had no SOCKS4 transport, so inbound media
was skipped and arrived as the omitted marker, the WhatsApp Web version
was not looked up, and the initial-sync payloads and a product card's
image URL went direct. One undici connector over the socks package now
covers socks4 and socks5, so a dispatcher exists for all four schemes
and the skip branches those two fetches needed are gone. SOCKS4 has no
hostname form, so the destination is resolved locally to IPv4; socks5
keeps the name on the wire for the proxy's resolver. Credentials on a
socks4 URL cannot authenticate, which is now reported at session start.

A media URL passed to a send route or to a media/convert route, and the
link preview of a text send, were fetched outside the session's proxy
on every scheme, because that fetch belongs to the request rather than
to the session. The guarded fetch now takes the session proxy and uses
it on every branch that opens a socket, including an IP literal with
nothing to pin and the guard-off path; an unusable proxy value fails
the request instead of falling back to a direct fetch. Through SOCKS
the vetted addresses are what the proxy is asked to connect to, dialled
in resolver order so a proxy that routes only one address family still
reaches a dual-stack host, and a socks4 proxy is given an IPv4 one;
through an HTTP proxy the destination is resolved by the proxy, so
pinning is not expressible there. The proxy is the one the session's
live engine was started with, since PATCH /proxy does not restart it,
falling back to the stored row when nothing is running.
SESSION_PROXY_URL_FETCH=false restores the direct fetch for a
WhatsApp-only proxy.

Fixes #1626
@rmyndharis
rmyndharis merged commit 2e89a4a into main Sep 16, 2026
16 checks passed
@rmyndharis
rmyndharis deleted the fix/baileys-socks-and-url-proxy branch September 16, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Some Baileys fetches still leave the gateway directly on a proxied session

1 participant