Skip to content

CONNECT tunnel with access: full + tls: skip still causes 405 from upstream (WhatsApp/Baileys) #760

Closed

Description

@novarii

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/node

What 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

  1. Create a sandbox with the policy above
  2. Inside the sandbox, install baileys and attempt a connection through the proxy agent
  3. Observe that the WebSocket opens but the Noise handshake fails with 405
  4. 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?

Activity

  1. self-assigned this
    on Apr 6, 2026
  2. johntmyers commented on Apr 6, 2026

    @johntmyers
    Collaborator

    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.

  3. mjamiv commented on Apr 6, 2026

    @mjamiv
    Contributor

    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: rest and/or tls: terminate on the WebSocket endpoint. These flags trigger L7 inspection in the proxy, which modifies WebSocket frames even in tls: skip mode if other flags are present.

    Fix: We created a separate policy for WebSocket endpoints with NO protocol or tls flags at all — just the raw endpoint + port + access: full:

    slack_websocket:
      name: slack_websocket
      endpoints:
        - host: wss-primary.slack.com
          port: 443
          access: full
        - host: wss-backup.slack.com
          port: 443
          access: full
      binaries:
        - path: /usr/local/bin/node

    No protocol: rest, no tls: terminate, no tls: 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/tls flags from the policy, and (2) ensure your gateway binary includes the #718 fix.

  4. novarii commented on Apr 7, 2026

    @novarii
    Author

    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:

    • Direct (baseline): 8/8 PASS
    • Through proxy with access: full + tls: skip: 8/8 PASS — all frames byte-identical

    The proxy is not corrupting WebSocket frames after HTTP 101. The #718 fix is working correctly.

    We also tested removing tls: skip per @mjamiv's suggestion — WhatsApp still rejects with 405 but from a different location code (lla vs atn), 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions