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, set failClosed: true in the networking block or 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. - 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.
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.