feat: support outbound proxy for Chromium sessions (#131)

* feat: support outbound proxy for Chromium sessions

Routes WhatsApp Web traffic through an HTTP/HTTPS/SOCKS5 proxy when
PROXY_URL is set. Optional PROXY_API_KEY enables auth by passing the
key as the basic-auth username (the format API-key proxies expect).
No behavior change when unset.

Useful for deployments behind egress filters or where outbound
WhatsApp traffic must originate from a specific IP.

* docs: 📝 rewrite proxy auth part

* fix: 🐛 move api key value to passwd

* fix: 💄 change var naming

* fix: 🎨 change var ref in docs and data passing
This commit is contained in:
vbas-644 2026-05-27 12:56:34 +03:00 • committed by GitHub
commit 132bcf6ee3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 4 deletions

View file

@ -55,4 +55,13 @@ TRUST_PROXY=FALSE
## CORS ##
# OPTIONAL, CORS ALLOWED ORIGINS (comma separated), DEFAULT ALLOWS ALL
# ALLOWED_ORIGINS=http://localhost:3000,http://example.com
# ALLOWED_ORIGINS=http://localhost:3000,http://example.com
## Outbound Proxy ##
# OPTIONAL, ROUTES CHROMIUM (WHATSAPP WEB) TRAFFIC THROUGH AN OUTBOUND PROXY.
# ACCEPTED FORMATS: http://host:port, https://host:port, socks5://host:port
# PROXY_URL=http://10.0.0.10:8118
# OPTIONAL, PROXY BASIC-AUTH USERNAME
# PROXY_USERNAME=
# OPTIONAL, PROXY BASIC-AUTH PASSWORD
# PROXY_PASSWORD=

View file

@ -163,6 +163,11 @@ By setting the `ENABLE_WEBHOOK` environment to `FALSE` you can disable webhook d
In order to validate a new WhatsApp Web instance you need to scan the QR code using your mobile phone. Official documentation can be found at (https://faq.whatsapp.com/1079327266110265/?cms_platform=android) page. The service itself delivers the QR code content as a webhook event or you can use the REST endpoints (`/session/qr/:sessionId` or `/session/qr/:sessionId/image` to get the QR code as a png image).
### Outbound Proxy
The Chromium instance that powers each session can be routed through an outbound proxy by setting the `PROXY_URL` environment variable (e.g. `http://10.0.0.10:8118`, `https://...`, or `socks5://...`). When `PROXY_URL` is empty, sessions connect directly with no behavior change.
If the proxy requires authentication, set `PROXY_USERNAME` and `PROXY_PASSWORD`. When both are defined they are forwarded to Chromium as HTTP Basic auth via puppeteer's `proxyAuthentication`. For API-key vendor proxies, use the vendor-specified username (often `api-key`) and the key itself as the password.
### WebSocket mode
The service can dispatch realtime events through websocket connection. By default, the websocket is not activated, so you need manually set the `ENABLE_WEBSOCKET` environment variable to activate it. The server activates a new websocket instance per each active session. The websocket path is `/ws/:sessionId`, where sessionId is your configured session name. The websocket supports ping/pong scheme to keep the socket running.
The below example shows how to receive the events for **test** session.

View file

@ -13,6 +13,16 @@ services:
env_file: .env
volumes:
- ./sessions:/usr/src/app/sessions
# Optional: route Chromium (WhatsApp Web) traffic through an outbound proxy.
# Set PROXY_URL (and optionally PROXY_USERNAME / PROXY_PASSWORD for HTTP
# Basic auth) in .env, or uncomment the block below to point at the
# bundled tinyproxy service.
# environment:
# PROXY_URL: http://tinyproxy:8888
# # PROXY_USERNAME: api-key
# # PROXY_PASSWORD: your-api-key
# depends_on:
# - tinyproxy
# Optional healthcheck
# healthcheck:
# test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
@ -20,3 +30,17 @@ services:
# timeout: 10s
# retries: 3
# start_period: 40s
# Optional: local HTTP proxy for testing the PROXY_URL feature end-to-end.
# Uncomment along with the api `environment` / `depends_on` blocks above.
# tinyproxy:
# image: vimagick/tinyproxy
# container_name: wwebjs-tinyproxy
# ports:
# - "8888:8888"
# environment:
# ALLOWED_NETWORKS: 0.0.0.0/0
# # Uncomment to test the PROXY_USERNAME / PROXY_PASSWORD path.
# # BASIC_AUTH_USER: "api-key"
# # BASIC_AUTH_PASSWORD: "your-api-key"
# restart: unless-stopped

View file

@ -25,6 +25,9 @@ const enableWebSocket = process.env.ENABLE_WEBSOCKET ? (process.env.ENABLE_WEBSO
const autoStartSessions = process.env.AUTO_START_SESSIONS ? (process.env.AUTO_START_SESSIONS).toLowerCase() === 'true' : true
const basePath = process.env.BASE_PATH || '/'
const trustProxy = process.env.TRUST_PROXY ? (process.env.TRUST_PROXY).toLowerCase() === 'true' : false
const proxyUrl = process.env.PROXY_URL || null
const proxyUsername = process.env.PROXY_USERNAME ?? null
const proxyPassword = process.env.PROXY_PASSWORD ?? null
module.exports = {
servicePort,
@ -49,5 +52,8 @@ module.exports = {
enableWebSocket,
autoStartSessions,
basePath,
trustProxy
trustProxy,
proxyUrl,
proxyUsername,
proxyPassword
}

View file

@ -2,7 +2,7 @@ const { Client, LocalAuth } = require('whatsapp-web.js')
const fs = require('fs')
const path = require('path')
const sessions = new Map()
const { baseWebhookURL, sessionFolderPath, maxAttachmentSize, setMessagesAsSeen, webVersion, webVersionCacheType, recoverSessions, chromeBin, headless, releaseBrowserLock } = require('./config')
const { baseWebhookURL, sessionFolderPath, maxAttachmentSize, setMessagesAsSeen, webVersion, webVersionCacheType, recoverSessions, chromeBin, headless, releaseBrowserLock, proxyUrl, proxyUsername, proxyPassword } = require('./config')
const { triggerWebhook, waitForNestedObject, isEventEnabled, sendMessageSeenStatus, sleep, patchWWebLibrary } = require('./utils')
const { logger } = require('./logger')
const { initWebSocketServer, terminateWebSocketServer, triggerWebSocket } = require('./websocket')
@ -138,12 +138,18 @@ const setupSession = async (sessionId) => {
'--use-mock-keychain',
'--disable-setuid-sandbox',
'--no-sandbox',
'--disable-blink-features=AutomationControlled'
'--disable-blink-features=AutomationControlled',
// Route Chromium outbound traffic through PROXY_URL when configured.
...(proxyUrl ? [`--proxy-server=${proxyUrl}`] : [])
]
},
authStrategy: localAuth
}
if (proxyUrl && proxyUsername != null && proxyPassword != null) {
clientOptions.proxyAuthentication = { username: proxyUsername, password: proxyPassword }
}
if (webVersion) {
clientOptions.webVersion = webVersion
switch (webVersionCacheType.toLowerCase()) {