Skip to content

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.

The global Networking dialog

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:

  1. Open Networking (Advanced mode).
  2. Pick an Inbound mode.
  3. Pick an Outbound mode. If you choose tsexit, the Exit Node selector appears with the list of advertised Tailnet exit-node peers.
  4. Save. tunlx applies the change and, if necessary, restarts the process in place.

Configure in config.json

{
  "networking": {
    "inbound": "tailscale",
    "outbound": "vpn"
  }
}

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.