Architecture¶
A one-page tour of how tunlx is built. Useful if you're debugging a weird flow, deciding how to slot tunlx into your stack, or sizing a deployment.
System view¶
flowchart TB
classDef edge fill:#0ea5e9,stroke:#0369a1,color:#fff
classDef admin fill:#22d3ee,stroke:#0e7490,color:#0f172a
classDef core fill:#6366f1,stroke:#4338ca,color:#fff
classDef mod fill:#1e293b,stroke:#334155,color:#e2e8f0
classDef egress fill:#0f766e,stroke:#0d9488,color:#fff
classDef src fill:#a855f7,stroke:#7e22ce,color:#fff
classDef store fill:#f59e0b,stroke:#b45309,color:#0f172a
Browser["Operator browser"]:::edge
Client["IPTV / Xtream client"]:::edge
subgraph TUNLX["tunlx process"]
direction TB
Mux["net/http mux<br/>(auth · CSP · rate limit · CORS)"]:::core
Dashboard["Dashboard<br/>+ Web Player"]:::admin
WizardAPI["Setup + Xtream wizard APIs"]:::admin
MetricsAPI["/metrics · /tailnet · /networking"]:::admin
subgraph PROXIES["Proxy layer"]
Standard["Standard proxy<br/>(CustomReverseProxy)"]:::core
Composite["Composite proxy"]:::core
Xtreme["Xtream service"]:::core
end
subgraph SERVICES["Background services"]
EPG["EPG scheduler<br/>(epgAutoRefresh)"]:::mod
VPNSched["VPN scheduler<br/>(Mullvad rotation)"]:::mod
Health["Stream tester"]:::mod
Activity["Activity log<br/>(last 200 events)"]:::mod
MFBridge["MediaFlow bridge"]:::mod
end
Config["config.json<br/>(load · validate · save)"]:::store
end
subgraph EGRESS["Egress modes"]
direction TB
System["system route"]:::egress
WG["WireGuard (wg0 + SO_BINDTODEVICE)"]:::egress
TS["Tailscale exit node"]:::egress
Direct["direct (sourceIP)"]:::egress
end
Upstreams["Upstream IPTV providers"]:::src
MediaFlow["MediaFlow Proxy"]:::src
IPTVOrg["iptv-org catalog"]:::src
Browser --> Mux
Client --> Mux
Mux --> Dashboard
Mux --> WizardAPI
Mux --> MetricsAPI
Mux --> PROXIES
Dashboard -. controls .-> PROXIES
Dashboard -. controls .-> SERVICES
Dashboard -. reads .-> Config
Dashboard -. writes .-> Config
WizardAPI -. writes .-> Config
PROXIES -. reads .-> Config
Composite --> Standard
Standard --> MFBridge
Xtreme --> MFBridge
MFBridge --> MediaFlow
Standard --> EGRESS
Xtreme --> EGRESS
MediaFlow --> EGRESS
EGRESS --> Upstreams
Xtreme --> IPTVOrg
VPNSched -. rotates .-> WG
EPG -. fetches .-> Upstreams
Health -. probes .-> Standard
Health -. probes .-> Xtreme
Standard --> Activity
Composite --> Activity
Xtreme --> Activity
Process model¶
tunlx is a single Go binary, listening on:
- The dashboard port (default
6060) — admin UI, web player, all admin APIs, plus/metrics. - One listener per proxy / composite / Xtream service — each on its
own host/port from
config.json.
There's no separate worker pool. Background jobs (EPG, VPN scheduler, stream tester) are goroutines inside the main process. This is deliberate — one binary, one config file, one log stream.
HTTP plumbing¶
- Router: stdlib
net/httpmux only. Nogorilla/mux,gin,chi. - Auth: dashboard-level.
webCredentials→ bcrypt verify → signed session cookie viagorilla/securecookie+gorilla/sessions. - Middleware chain:
AuthMiddlewareon admin routessecurityHeadersMiddleware(CSP, HSTS, X-Frame-Options)loggingMiddlewareapplyCORSHeaders- Rate limiting:
tollboothwith three buckets: general (60 r/s burst 180), EPG (180 r/s burst 360), login (0.5 r/s burst 2 — deters brute force).
Proxy flavors at a glance¶
| Flavor | Code path | Best for |
|---|---|---|
| Standard | CustomReverseProxy in tunlx.go |
One upstream, optional MediaFlow + EPG |
| Composite | xtreme_group_streams.go + composite mux |
Curated multi-provider catalog |
| Xtream service | xtreme_*.go |
Your own panel, with users and groups |
Config lifecycle¶
flowchart LR
classDef io fill:#0ea5e9,stroke:#0369a1,color:#fff
classDef proc fill:#6366f1,stroke:#4338ca,color:#fff
classDef data fill:#f59e0b,stroke:#b45309,color:#0f172a
Start["./tunlx"]:::io
Load["config.go: load + validate"]:::proc
Default["Generate defaults if missing"]:::proc
Boot["startProxiesFromConfig()"]:::proc
JSON["config.json"]:::data
DashWrite["Dashboard / API writes"]:::proc
Save["Atomic write"]:::proc
Start --> Load --> JSON
Load -. missing .-> Default --> JSON
Load --> Boot
DashWrite --> Save --> JSON
JSON -. read .-> Boot
- All persisted state lives in
config.json. No database. - Writes from the dashboard use atomic replace so a crashed write can't corrupt the file.
- A few "live" overrides (verbose flags) take effect immediately without a restart; inbound-mode changes trigger a clean in-place re-exec.
Where each subsystem lives¶
| Concern | File(s) |
|---|---|
| Main, mux, dashboard | tunlx.go |
| Config schema + load | config.go |
| Standard proxy logic | tunlx.go (CustomReverseProxy) |
| Composite resolution | xtreme_group_streams.go, xtream_augment.go |
| Xtream service | xtreme_*.go |
| Stream protocol detect | stream_protocol.go |
| Stream health tests | xtreme_stream_tester.go |
| iptv-org catalog | iptv_org.go |
| WireGuard / VPN | vpn/, vpn_scheduler.go |
| Tailscale (tsnet) | tailnet/, tailnet_config.go, tailnet_handlers.go |
| Networking modes | networking_handlers.go, inbound_endpoints.go |
| Templates | templates/dashboard.html, templates/player.html |
| Static assets | static/ |
Request flow: live stream¶
sequenceDiagram
participant C as Xtream client
participant P as Standard proxy
participant M as MediaFlow (optional)
participant U as Upstream
C->>P: GET /live/user/pass/123.ts
P->>P: Auth + rate limit + activity policy match
alt mediaFlowEnabled
P->>M: GET /proxy/stream?url=upstream
M->>U: Range-aware GET upstream
U-->>M: Stream
M-->>P: Stream
else
P->>U: GET upstream (route: system / vpn / tsexit / direct)
U-->>P: Stream
end
P-->>C: Stream + bandwidth counted
Build & deploy targets¶
| Target | Detail |
|---|---|
GOARCH |
amd64 (CI build). int is 64-bit, which matters for Xtream stream IDs. |
| Container image | ghcr.io/eyupio/tunlx:latest — Ubuntu 22.04 base, ffmpeg preinstalled. |
| Deploy units | Root docker-compose.yaml (host networking) plus a gluetun-sidecar variant under deployments/docker/. |
| Installer | install.sh — interactive, silent, and dry-run modes. |
Performance notes¶
- Allocation-light hot paths. Stream protocol detection uses
strings.EqualFoldinstead ofToLowerto avoid per-segment allocs. - Bandwidth counters flush at 64KB so low-bitrate streams still appear in the live UI.
- int64 stream IDs. Xtream
stream_id/series_idexceed int32 in the wild. Clamp logic deliberately rejects only NaN / ±Inf / true int64 overflow.