A Local VLESS Tunnel for Playwright and Puppeteer Automation
Headless browsers take a proxy at launch and never think about it again, which makes them a good match for a VLESS line running as a local daemon. The browser talks to a port on 127.0.0.1; the daemon carries everything through an encrypted tunnel to the modem on your NeonProxies line; the site sees a carrier IP. No credentials in launch flags, no authentication popups, and swapping lines means swapping a port number. This guide sets up the daemon, wires Playwright and Puppeteer to it, and shows how rotation fits into an automation loop.
The link and what it contains
From the NeonProxies dashboard, open My proxies and copy the VLESS / Xray row on the line. The vless:// URL holds your id before the at sign, then host and port, then the REALITY parameters as query values: sni, pbk, sid, fp and flow. Keep the URL private; it is the credential. VLESS is served on our server-based lines, so tick VLESS / Xray in the purchase or move filters.
The daemon reads every one of those values from the config file, never from the browser, so the browser launch line contains nothing sensitive and can live in source control. When a line is moved or its modem is switched the server issues a new id and the old config stops authenticating; treat the link as something you fetch from the dashboard at deploy time rather than something you commit.
Run the daemon
Install Xray-core on the automation host. A single binary, no dependencies. Write the config below with your values and start it; it will sit there forever until you stop it.
Port 3001 is arbitrary; pick anything free on the host. The inbound binds to 127.0.0.1 so only processes on the same machine can use it. If the browsers run in other containers, bind the inbound to the container network address instead and keep that network private. Leave udp enabled on the inbound so DNS and any QUIC traffic the browser attempts can pass through the tunnel as well.
{
"inbounds": [{"listen": "127.0.0.1", "port": 3001, "protocol": "socks",
"settings": {"udp": true}}],
"outbounds": [{"protocol": "vless",
"settings": {"vnext": [{"address": "HOST", "port": PORT,
"users": [{"id": "UUID", "encryption": "none", "flow": "xtls-rprx-vision"}]}]},
"streamSettings": {"network": "tcp", "security": "reality",
"realitySettings": {"serverName": "SNI", "fingerprint": "chrome",
"publicKey": "PBK", "shortId": "SID"}}}]
}
xray run -c line1.json
Playwright
Playwright also accepts a proxy per context on Chromium, so one browser process can hold several contexts each on a different line: run one xray inbound per line on ports 3001, 3002 and so on, and pass the matching port when creating each context.
Headless and headed modes behave the same way with respect to the proxy, so develop with a visible browser through the same port and switch to headless for the run. If a test suite launches many browsers, keep them on one port per line and let the line's connection limit, not the number of browsers, decide the concurrency.
// Node
const browser = await chromium.launch({
proxy: { server: 'socks5://127.0.0.1:3001' }
});
const context = await browser.newContext();
// Python
browser = p.chromium.launch(proxy={"server": "socks5://127.0.0.1:3001"})
Puppeteer
Puppeteer sets the proxy for the whole browser process. For several lines, launch several browsers, one per port. Chromium resolves DNS through a SOCKS5 proxy when it is given as socks5://, so lookups happen on the carrier side as they should.
const browser = await puppeteer.launch({
args: ['--proxy-server=socks5://127.0.0.1:3001']
});
Rotation inside the loop
The tunnel stays connected across rotations because it ends at our server, not at the carrier. That means the automation loop can rotate between tasks without touching the browser.
A rotation between tasks is also the natural moment to clear state. Close the context, rotate, wait for the new IP, then open a fresh context with a clean cookie jar. Reusing a context across a rotation makes the site see one session move between two carrier IPs, which is a stronger signal than either IP on its own.
- Finish the task and close the page or context.
- Call the line's rotation link with an ordinary HTTP request.
- Poll the status link until the reported IP differs from the previous one.
- Open the next page. It exits on the new address through the same tunnel.
- Do not rotate while a page is mid-navigation; the in-flight request will fail and the page will show an error.
- If you must hold an IP for a login flow, disable interval rotation on that line in the dashboard for the duration.
What breaks and why
- Invalid request user id in the xray log: the line was moved or its modem switched, which issues a new id. Copy the current link and restart the daemon.
- Every handshake failing at once: the host clock has drifted. REALITY validates real certificates.
- Browser shows your own IP: it was launched before the daemon was up, or with the wrong port. Chromium does not retry a proxy that refused the first connection.
- Site rejects the session even though the IP is fine: the tunnel hides your network path, not your browser fingerprint, time zone or WebRTC. Set those to match the line's city.
Frequently asked
Can I pass the vless:// URL straight to the browser?
No. Browsers speak HTTP and SOCKS proxies only. The daemon translates the local SOCKS port into the VLESS tunnel.
Can I run the daemon in Docker next to the browser container?
Yes. Run xray as its own service, bind the inbound to the compose network, and give the browser the service name and port as its proxy.
Is there any difference in exit IP between VLESS and SOCKS5?
None. Both reach the same modem. Choose VLESS for the encrypted path and the credential-free local port.