Inbound & Outbound Modes¶
tunlx separates how it listens from how it dials out. The two are
configured independently, and either can be flipped from the dashboard
without editing config.json by hand.

flowchart LR
classDef in fill:#22d3ee,stroke:#0e7490,color:#0f172a
classDef out fill:#a855f7,stroke:#7e22ce,color:#fff
classDef core fill:#6366f1,stroke:#4338ca,color:#fff
subgraph INBOUND["Inbound (listeners)"]
I1["public"]:::in
I2["tailscale"]:::in
I3["both"]:::in
end
subgraph OUTBOUND["Outbound (egress)"]
O1["public"]:::out
O2["vpn (WireGuard)"]:::out
O3["tsexit (Tailscale exit node)"]:::out
end
Core["tunlx core<br/>proxies + services"]:::core
INBOUND --> Core --> OUTBOUND
Inbound modes¶
Which networks can reach your dashboard and proxy listeners?
| Mode | Behavior |
|---|---|
public |
Listeners bind on the host's public interface(s). Default. |
tailscale |
Listeners bind only on the embedded Tailscale node. No public exposure. |
both |
Bind public and Tailscale interfaces. Useful for staged rollouts. |
Switching modes rewrites bind addresses, so tunlx triggers an in-place process restart to re-bind cleanly. Active stream sessions reconnect on their own — clients re-dial as part of normal Xtream client behavior.
Outbound modes¶
How does egress leave the host?
| Mode | Behavior |
|---|---|
public |
Use the system routing table. Whatever IP your host has, that's the source. |
vpn |
Egress sockets bind to the WireGuard interface (SO_BINDTODEVICE). See WireGuard / VPN. |
tsexit |
Egress is routed through the configured Tailscale exit node. See Tailscale Access. |
Per-proxy override beats global
Global outbound is the default for every proxy. Individual proxies and
Xtream services can still set their own route (system, vpn,
tsexit, direct) to override. See Feature Tour →
Per-proxy egress.
Fail-closed behavior
By default, if a proxy requests vpn or tsexit egress and that interface is down or unavailable, tunlx will silently fail open and use the public system route. To prevent IP leaks, turn on Block traffic if the VPN / Tailscale path is down in the Networking dialog (or set failClosed: true in the networking block), or set strictEgress: true on individual proxies, to force a 503 error instead.
Configure from the dashboard¶
The dashboard exposes both modes in the same panel:
- Open Networking (Advanced mode).
- Pick an Inbound mode.
- Pick an Outbound mode. If you choose
tsexit, the Exit Node selector appears with the list of advertised Tailnet exit-node peers. - Optionally switch on Block traffic if the VPN / Tailscale path is down so proxies answer 503 instead of falling back to your normal connection (see the note above).
- Save. tunlx applies the change and, if necessary, restarts the process in place.
Configure in config.json¶
If networking is absent, both modes default to public.
Picking exit nodes from the Tailnet¶
When outbound is tsexit, tunlx queries the local Tailscale node for the
list of peers that advertise themselves as exit nodes and exposes them in
the dashboard. Pick by hostname instead of memorizing Tailnet IDs.
Under the hood, the dashboard calls:
| Method | Path | Purpose |
|---|---|---|
GET |
/tailnet/status |
Current Tailscale state (logged-in, IPs, etc.) |
GET |
/tailnet/peers |
Advertised exit nodes available to this node |
POST |
/tailnet/action |
Apply exit-node selection |
POST |
/networking/config |
Persist inbound + outbound choice |
You can script the same flow from cron / CI if you need to flip modes during maintenance windows.
Reachable URLs¶
When inbound is tailscale, the dashboard surfaces the Tailnet-reachable
URL (e.g. http://tunlx.example-tailnet.ts.net:6060) so you don't have
to guess what hostname to give other operators. Logic lives in
inbound_endpoints.go.
The public IP shown in the navbar¶
To show the server's outward-facing address, tunlx asks an outside service for
it: api.ipify.org, then checkip.amazonaws.com, ifconfig.me and ip-api.com
if the earlier ones do not answer. The request goes out the way the dashboard's
own traffic does (through WireGuard when it is up, for the VPN address; directly
for the ISP address). Whatever the number of open dashboards, the answer is kept
for a minute, so that is roughly two lookups a minute while a dashboard is
open (a few more while the services are unreachable), and none otherwise.
Set TUNLX_DISABLE_EXTERNAL_IP_LOOKUP=true to turn the lookups off. The navbar then
shows no address, and the wizards no longer prefill one; nothing else depends
on it.
Avoid double-hopping¶
If you set per-proxy route: vpn and global outbound tsexit, you've
asked for traffic to go through WireGuard and through a Tailscale exit
node. That may be what you want — but check, because the latency and
debuggability cost is real.
A safe rule of thumb:
- One policy per workload. Use either per-proxy egress or global outbound, not both, for the same proxy.