Skip to content

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/http mux only. No gorilla/mux, gin, chi.
  • Auth: dashboard-level. webCredentials → bcrypt verify → signed session cookie via gorilla/securecookie + gorilla/sessions.
  • Middleware chain:
  • AuthMiddleware on admin routes
  • securityHeadersMiddleware (CSP, HSTS, X-Frame-Options)
  • loggingMiddleware
  • applyCORSHeaders
  • Rate limiting: tollbooth with 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 reverse_proxy.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.
  • Saving the configuration restarts only the proxies, composites and Xtream services whose own entry changed (plus any whose last start failed, so a freed port gets another try). The others keep serving without a gap. A change to a setting they all depend on (networking, VPN, Tailscale, forced HTTPS) restarts every one of them, and the log says which setting did.

Where each subsystem lives

Concern File(s)
Main, mux main.go, proxy_lifecycle.go
Dashboard pages, login page_handlers.go, auth_handlers.go, middleware.go
Config schema + load config.go
Standard proxy logic reverse_proxy.go (CustomReverseProxy), proxy_server.go
Composite resolution xtreme_group_streams.go, xtream_augment.go
Xtream service xtreme_*.go
Listener lifecycle listener_status.go, listener_restart.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/*.html, embedded by assets.go
Static assets static/, embedded by assets.go

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.EqualFold instead of ToLower to 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_id exceed int32 in the wild. Clamp logic deliberately rejects only NaN / ±Inf / true int64 overflow.