Repository navigation
CONNECT tunnel with access: full + tls: skip still causes 405 from upstream (WhatsApp/Baileys) #760
Copy link
Copy link
Closed
Description
Summary
WebSocket connections through the sandbox proxy fail with HTTP 405 from WhatsApp's Noise protocol handshake, even with access: full and tls: skip configured. The same connection succeeds when made directly from the VPS host (outside the sandbox).
Environment
- Gateway:
ghcr.io/nvidia/openshell/gateway:0.0.21 - Sandbox binary: built Apr 2, 2026 (post WS-relay fix c6f3087)
- VPS: Hetzner CPX32, Ubuntu 24.04, x86_64
- Client: Baileys 7.0.0-rc.9 (WhatsApp Web client library)
- Runtime: Node.js 22 inside sandbox, also tested with Bun
Policy (relevant section)
whatsapp:
name: whatsapp
endpoints:
- host: "*.whatsapp.net"
port: 443
access: full
tls: skip
- host: "*.whatsapp.com"
port: 443
access: full
tls: skip
binaries:
- path: /usr/local/bin/bun
- path: /usr/local/bin/nodeWhat works
| Test | Location | Result |
|---|---|---|
fetch('https://web.whatsapp.com/') via bun |
Inside sandbox | 200 OK |
Raw CONNECT to web.whatsapp.com:443 |
Inside sandbox | 200 Connection Established |
| TLS cert check after CONNECT | Inside sandbox | Real DigiCert cert (not MITM'd) |
| Baileys full connection (QR generated) | VPS host directly | Works |
| WebSocket open via ws library | Inside sandbox | Opens successfully |
What fails
| Test | Location | Result |
|---|---|---|
| Baileys connection via proxy | Inside sandbox (Node) | WS opens → Noise handshake → 405 |
| Baileys connection via proxy | Inside sandbox (Bun) | WS opens → Noise handshake → 405 |
Detailed failure sequence
[WhatsApp] connection.update connection=connecting
[WhatsApp] WS open # <-- WebSocket establishes
{"msg":"connected to WA"} # <-- Noise hello sent
{"msg":"not logged in, attempting registration..."}
# Then noise-handler.js:141 throws:
Connection Failure, data: { reason: "405", location: "atn" }
Analysis
- The CONNECT tunnel returns 200 and the TLS certificate is WhatsApp's real DigiCert cert (not the proxy's), so TLS termination appears to be correctly skipped.
- The WebSocket opens successfully through the tunnel.
- The failure happens at the application protocol level (WhatsApp's Noise protocol handshake), after the WebSocket is established.
- The exact same Baileys version, Node version, and WhatsApp account work when connecting directly from the VPS host without the proxy.
- This suggests the proxy is modifying something subtle about the TCP stream or WebSocket frames that WhatsApp's server-side validation detects and rejects.
Reproduction
- Create a sandbox with the policy above
- Inside the sandbox, install baileys and attempt a connection through the proxy agent
- Observe that the WebSocket opens but the Noise handshake fails with 405
- Run the same code on the host directly — it succeeds and generates a QR code
Expected behavior
With access: full + tls: skip, the proxy should act as a raw TCP relay after CONNECT, passing bytes unmodified between the client and upstream. The Baileys Noise handshake should succeed identically to a direct connection.
Workaround needed
Is there a policy configuration that produces a completely transparent TCP tunnel with zero modification of the byte stream? Or is there a known interaction between the proxy's frame handling and binary WebSocket protocols?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
johntmyers commentedon Apr 6, 2026
Can you provide a minimal working environment that creates the failure case using a vanilla web socket connection that does not rely on a third party account like Whats App? We can try and investigate but will be more difficult if we cannot re-create the failure case. We don't have Whats App access to try this.
mjamiv commentedon Apr 6, 2026
We hit the exact same class of issue with Slack Socket Mode WebSocket connections and resolved it. Sharing what worked in case it helps.
Our symptoms were identical: WebSocket opened successfully through the CONNECT tunnel, TLS was not MITM'd (real Slack cert), but the application-level protocol (Slack's Socket Mode framing) broke with pong timeouts and eventually HTTP 408 from the upstream server.
Root cause in our case: The network policy had
protocol: restand/ortls: terminateon the WebSocket endpoint. These flags trigger L7 inspection in the proxy, which modifies WebSocket frames even intls: skipmode if other flags are present.Fix: We created a separate policy for WebSocket endpoints with NO
protocolortlsflags at all — just the raw endpoint + port +access: full:No
protocol: rest, notls: terminate, notls: skip— just a bare endpoint. This produces a raw TCP relay after CONNECT.Additionally: PR #718 (WebSocket relay fix, merged 2026-04-02) addressed the underlying proxy code that was modifying WebSocket frames. If your gateway binary predates that merge, upgrading should help.
For WhatsApp's Noise protocol specifically, I'd try: (1) remove all
protocol/tlsflags from the policy, and (2) ensure your gateway binary includes the #718 fix.novarii commentedon Apr 7, 2026
We built a minimal binary WebSocket echo repro (zero external deps, pure Node built-ins) and tested through the sandbox proxy. Results:
Setup: Self-signed TLS echo server on VPS host, client sends 8 binary frames (4B to 256KB, including a Noise-like hello with 4-byte length prefix + 32-byte key). Client runs inside sandbox through proxy, compares SHA-256 of sent vs echoed data byte-for-byte.
Results:
access: full+tls: skip: 8/8 PASS — all frames byte-identicalThe proxy is not corrupting WebSocket frames after HTTP 101. The #718 fix is working correctly.
We also tested removing
tls: skipper @mjamiv's suggestion — WhatsApp still rejects with 405 but from a different location code (llavsatn), confirming a different rejection path.Our conclusion is that the Baileys/WhatsApp failure is not a proxy frame corruption issue — it's most likely TLS fingerprinting (JA3/JA4) on WhatsApp's side. WhatsApp's anti-bot detection is more aggressive than Slack's, which is why @mjamiv's fix worked for Slack but not for us.
We're working around this with a relay process on the host (Baileys runs outside the sandbox, bridges messages to the sandboxed daemon). Closing since this isn't actionable on the OpenShell side. Thanks for looking into it.