A secure tunnel that lets browsers and real devices in the TestingBot
cloud reach websites that only your machine or network can see — a development server on
localhost, a staging environment behind a firewall, an internal application.
Tests run against localhost:4445 as if the grid were local; the tunnel forwards Selenium
traffic to TestingBot and routes the browser's web requests back through your network.
Running tunnel 4.x or older? This page documents 5.0, which needs Java 17 and changes a few defaults. The 4.x source and its documentation stay on the
v4.xbranch. If you are upgrading, read Upgrading from 4.x first — it is short, and two of the changes are ones you will notice immediately.
| Tunnel version | Minimum JDK | Notes |
|---|---|---|
| 5.0 and later | 17 | Built on Jetty 12. Tested on 17, 21 and 25. |
| 4.x | 11 | Jetty 11, which is end-of-life and no longer receives security fixes. |
| 3.x and earlier | 8 | Unsupported. |
5.0 adds 28 options and removes none, so existing command lines keep working. Six things do change, and the first three are the ones people notice:
| 4.x | 5.0 | To keep the 4.x behaviour | |
|---|---|---|---|
| Java | 11 | 17 | — (hard requirement) |
| What the listeners bind | every interface | 127.0.0.1 |
--bind-address 0.0.0.0 |
| Per-request logging | one INFO line per request |
only failures and 5xx | --log-http url |
| Selenium relay logging | always logged | honours --log-http |
--log-http forwarder:url |
| Docker image | — | sets TESTINGBOT_BIND_ADDRESS=0.0.0.0 |
— (published ports keep working) |
| Embedding the jar | Jetty 11 + Servlet API | Jetty 12 core handlers | — (source change) |
Java 17. The jar's classes cannot be loaded by an older JVM. The tunnel checks the version
itself and says so plainly, rather than failing as A JNI error has occurred.
Listeners bind loopback. In 4.x the Selenium relay (4445), the local proxy (8087), the
insight endpoints (8003) and --web (8080) accepted connections from any machine that could
route to yours. None of them authenticates: the relay attaches your TestingBot key and secret to
everything it forwards, and the proxy will connect anywhere your machine can, including its own
loopback. They now bind 127.0.0.1.
If your tests run on the same machine as the tunnel — the normal case — nothing changes. If they
run elsewhere, add --bind-address 0.0.0.0 and restrict the port with a firewall. The Docker
image sets that for you, because a loopback bind inside a container makes published ports
unreachable; narrow it there on the host side of the publish instead
(-p 127.0.0.1:4445:4445).
Quieter logs. 4.x logged a line for every proxied request. 5.0 logs failures and 5xx only.
--log-http url restores a line per request, and --log-http takes a level per module —
--log-http proxy:url,forwarder:none.
java -jar testingbot-tunnel.jar API_KEY API_SECRETCredentials can also come from TESTINGBOT_KEY / TESTINGBOT_SECRET, or from ~/.testingbot
containing key:secret. Get yours from
your account page.
When you see You may start your tests, point your Selenium client at
http://localhost:4445/wd/hub.
docker run -e TESTINGBOT_KEY=... -e TESTINGBOT_SECRET=... testingbot/tunnel:5.0The image has a HEALTHCHECK that reports healthy once the tunnel is actually forwarding.
java -jar testingbot-tunnel.jar --doctorVerifies DNS, connectivity to TestingBot, whether the ports it needs are free, and — when Kerberos is configured — the whole Negotiate chain.
Every option can be supplied three ways. Precedence is command line, then --config
file, then environment. Each long option has a TESTINGBOT_* alias derived from its name,
so --se-port reads TESTINGBOT_SE_PORT. That is what lets a container be configured without
assembling a command line.
# these three are equivalent
java -jar testingbot-tunnel.jar --se-port 4446
echo "se-port = 4446" > tunnel.conf && java -jar testingbot-tunnel.jar --config tunnel.conf
TESTINGBOT_SE_PORT=4446 java -jar testingbot-tunnel.jarAnything on the command line is visible to other users of the machine in ps. Every long option
has a TESTINGBOT_* alias derived from its name, so the ones carrying a secret can be set in the
environment instead:
| Option | Environment variable | Format |
|---|---|---|
--auth |
TESTINGBOT_AUTH |
host:port:user:password, comma-separated for several |
--proxy-userpwd |
TESTINGBOT_PROXY_USERPWD |
user:password |
--proxy-testingbot-userpwd |
TESTINGBOT_PROXY_TESTINGBOT_USERPWD |
user:password |
--metrics-auth |
TESTINGBOT_METRICS_AUTH |
user:password |
The API key and secret can come from TESTINGBOT_KEY / TESTINGBOT_SECRET or from
~/.testingbot instead of being passed as arguments. The flag always wins over the variable.
The positional
API_KEY API_SECRETform shown above is still supported, but it does put the secret inpsfor as long as the tunnel runs. On a shared machine, prefer the environment variables or~/.testingbot.
--help lists everything. The options people reach for most:
| Option | Purpose |
|---|---|
--se-port |
Local Selenium port (default 4445) |
--localproxy |
Local HTTP proxy port (default 8087) |
--tunnel-identifier |
Name this tunnel, so several can run at once |
--allow-hosts |
Reach only these hosts; everything else gets 403 |
--fast-fail-regexps |
Refuse matching hosts. Prefix ! for an exception: .*,!ok\.com blocks everything except ok.com and its subdomains |
--bind-address |
Which interface the local listeners use: 127.0.0.1 (default) or 0.0.0.0 |
--proxy |
Upstream proxy for egress — browser traffic and, unless --proxy-testingbot is set, the tunnel's own connection |
--doctor |
Run diagnostics and exit |
A tunnel can reach whatever the machine running it can reach. --fast-fail-regexps names what to
refuse, which only helps for destinations somebody thought of in advance. --allow-hosts is the
other way round — name what the tunnel is for, and everything else is refused:
java -jar testingbot-tunnel.jar --allow-hosts 'staging.example.com,*.internal.example'*.internal.example covers subdomains but not internal.example itself, so widening to the apex
has to be written down. Omit the option and any host is reachable, as before.
Both, and --localhost-policy deny, are enforced on every way out: plain HTTP, CONNECT, and
WebSocket upgrades.
On a network whose only route out is a corporate proxy, --proxy covers everything the tunnel
does, including its control connection:
java -jar testingbot-tunnel.jar --proxy proxy.corp:8080 --proxy-userpwd user:passwordFor proxies that require Kerberos rather than Basic:
java -jar testingbot-tunnel.jar --proxy proxy.corp:8080 --proxy-auth-scheme negotiateCredentials come from your existing Kerberos ticket cache, or from --krb5-keytab with
--krb5-principal where nobody has run kinit. --doctor reports exactly which step fails,
which matters because every Negotiate misconfiguration otherwise looks like the same 407.
--krb5-hosts sends the same credentials to sites rather than to the proxy, for intranets that
authenticate with Kerberos. It is a list, never a wildcard: a service ticket names the user, so
every host that may receive one has to be written down.
Where egress is filtered by destination, the proxy allowed out to the internet is often not the
one that reaches internal test targets. --proxy-testingbot gives TestingBot's own traffic — the
API and the tunnel's connection — a different proxy from the one test traffic uses:
java -jar testingbot-tunnel.jar --proxy 10.0.0.9:8080 \
--proxy-testingbot corp-egress:3128 --proxy-testingbot-userpwd user:passwordCredentials are deliberately not shared between the two: --proxy-userpwd belongs to one proxy
operator and is not sent to another. The same rule applies to a proxy chosen by --pac-local —
it receives no credentials, and the tunnel logs that it withheld them, so the resulting 407
explains itself.
If a proxy intercepts TLS and re-signs it with an internal authority, the JVM will not trust it and the tunnel cannot even register itself. Point it at the authority:
java -jar testingbot-tunnel.jar --proxy proxy.corp:8080 --cacert-file /etc/ssl/corp-ca.pemIt is added to the platform's trust store rather than replacing it, so the public roots keep
working. --doctor prints the subjects it loaded.
--pac-local points the tunnel at a proxy auto-config file and lets it decide, per destination,
whether to go direct or through a proxy:
java -jar testingbot-tunnel.jar --pac-local /etc/corp.pac
java -jar testingbot-tunnel.jar --pac-local https://corp.example/proxy.pacThe file decides where traffic goes and, on the CONNECT path, which proxy receives
--proxy-userpwd. Fetched over plain http:// it is whatever the network says it is, so a
plain http:// URL is refused unless you pin the document:
java -jar testingbot-tunnel.jar --pac-local http://wpad.corp/proxy.pac \
--pac-local-sha256 3b1f...64hexUse https:// or a local file where you can. Redirects are refused rather than followed, so
what is fetched is what you named.
The file is evaluated by a restricted interpreter built for this purpose — no JavaScript
engine is embedded. That keeps the dependency surface small for a process that already sits in
the network path, at the cost of supporting only the subset PAC files actually use: functions,
variables, conditionals, loops, the usual operators, and the standard helpers (isPlainHostName,
dnsDomainIs, shExpMatch, isInNet, dnsResolve, myIpAddress, weekdayRange, dateRange,
timeRange and friends). Anything outside it — object literals, regular expressions, new — is
reported with its line number rather than guessed at, because a misread PAC file silently
sends traffic to the wrong place.
Check a file before relying on it:
java -jar testingbot-tunnel.jar --pac-local corp.pac --pac-test https://internal.corp/page--dns resolves through servers you name rather than the platform resolver, taking a list: the
first is primary and the rest are tried in order when it does not answer. --dns-round-robin
spreads queries across them instead, and --dns-timeout bounds each one.
--pac-localis not the same as--pac.--pactells the remote browser which PAC URL to use;--pac-localdecides where this tunnel sends its own traffic.
--header and --response-header edit headers on plain HTTP traffic, repeatable:
name: value set, replacing whatever the peer sent
name; set to an empty value
-name remove
-name* remove by prefix
java -jar testingbot-tunnel.jar --response-header '-Content-Security-Policy'HTTPS arrives as a CONNECT and is relayed as opaque bytes, so these apply to plain HTTP only.
On the metrics port (default 8003, --metrics-port):
| Path | Purpose |
|---|---|
/ |
JSON status: version, uptime, requests, bytes transferred |
/metrics |
Prometheus exposition; honours --metrics-auth |
/healthz |
Liveness — 200 whenever the process is answering, including mid-reconnect |
/readyz |
Readiness — 200 when the tunnel is forwarding, 503 otherwise |
--ready queries /readyz and exits 0 or 1, for use in health checks.
The health endpoints are deliberately not behind --metrics-auth: container probes have no good
way to carry credentials, and they disclose nothing beyond up/down.
--log-http controls per-request logging: none, url, headers, or errors (the default,
which logs only failed or 5xx responses). Every request carries a correlation id, logged in
brackets and passed to the target in --request-id-header. Header values that carry credentials
are redacted.
Browser traffic and the Selenium relay can be turned up independently, which saves drowning in one while debugging the other:
java -jar testingbot-tunnel.jar --log-http proxy:none,forwarder:headersforwarder:body adds the relay's request body. That body is WebDriver capabilities, which
routinely carry your access key, so it is redacted before it is written: values are matched by
key name, and anything that cannot be parsed — an unknown content type, or a body too large to
parse whole — is described rather than printed. proxy:body is refused, because browser traffic
has to stream and cannot be buffered for logging.
--log-format json writes one JSON object per record, so a collector need not guess where a
multi-line message or a stack trace ends.
mvn package # produces target/TestingBotTunnel-5.0-shaded.jar
mvn verify # plus the full test suite
e2e/run-e2e.sh # end-to-end against real browsers (needs credentials)dist/build-runtime.sh produces a self-contained runtime image via jlink for machines with no
JDK installed.
Questions and problems: testingbot.com or
support@testingbot.com. Including the output of --doctor
usually saves a round trip.