Documentation

Everything Splitter does, in one place

A practical reference for running Splitter in production — install and upgrade, every page of the dashboard, every field of the mapping form, snippets and routing, logs and monitoring, configuration and the REST API. If something in the dashboard has an ⓘ tooltip, it's explained here in more depth.

Getting started

Quick start

Two ways to run Splitter: a single Docker image (fastest) or a native install with a systemd service. Both end in the same place — a browser dashboard on port 8088.

Docker (recommended)

One self-contained image: nginx with the stream module, ModSecurity + OWASP CRS, certbot, logrotate, the diagnostic CLIs (nmap, tcpdump, dig, …) and the app. It runs in the host network namespace so the interfaces and IPs it creates are real and reachable on the LAN, and it auto-detects your uplink NIC from the default route.

# clone and bring it up
git clone https://github.com/splitter-uz/splitter.git && cd splitter
docker compose up -d --build

Open http://<server-ip>:8088 and create the admin account on first load. Named volumes hold your data, so docker compose up -d --build is safe to re-run after every git pull.

Docker Desktop (macOS/Windows) isn't supported. There is no real LAN NIC inside that VM. Splitter needs a genuine Linux host — bare metal, a VM or a cloud instance all work.

Native (systemd)

git clone https://github.com/splitter-uz/splitter.git && cd splitter
sudo ./setup.sh --service

setup.sh is idempotent. It installs the dependencies (nginx + stream module, Python/venv, iproute2, a DHCP client, openssl, iptables, certbot, logrotate and the Tools-page CLIs), wires the top-level stream {} include, enables net.ipv4.ip_nonlocal_bind, creates the persistent store at /var/lib/splitter, and with --service installs and starts a systemd unit.

FlagMeaning
--serviceInstall and enable the systemd service.
--nic NAMEForce the parent interface (otherwise auto-detected).
--host ADDR / --port NUI bind address and port (default 0.0.0.0:8088).
--no-depsSkip package installation.
--waf / --waf-only / --waf-enforceInstall, repair or enforce the WAF.
systemctl status splitter
journalctl -u splitter -f        # apply failures land here
systemctl restart splitter       # after every git pull
The dashboard performs privileged host actions. It is protected by login, but keep :8088 on a trusted network — or bind it to 127.0.0.1 and reach it over an SSH tunnel or the built-in WAF on :8443 — and use a strong admin password.
Getting started

Upgrading

Pull the new code and rebuild (Docker) or re-run the installer (native). Both paths install any new dependencies; existing mappings keep working unchanged.

git pull
docker compose up -d --build         # Docker
sudo ./setup.sh --service            # native
  • On start, Splitter provisions the nginx stub_status endpoint, re-exports error pages and config-snippet include files, and writes the log-rotation config — nothing to do by hand.
  • Mappings saved before the Snippets model carry inline rate-limit, timeout, log-format and error-page values. They keep applying until the mapping is saved again; the form shows a notice so you can create matching snippets first.
  • Take a full-system snapshot before a major upgrade — it is one click and includes every certificate and key.
Getting started

Architecture

nginx is the data plane; Splitter is the control plane. Each mapping can bind a physical interface's existing address or get its own sub-interface — a real macvlan/ipvlan device with a unique IP and MAC, optionally inside an 802.1Q VLAN. Splitter runs directly on the host, so every interface it creates shows up in ip link show and answers ARP like any other NIC.

Provisioning pipeline

On every Save / Apply Splitter runs, roughly, the commands you would otherwise type by hand:

ip link add link eth0 name eth0.50 type vlan id 50               # if a VLAN ID is given
ip link add link eth0.50 name mv-site4-0 type macvlan mode bridge   # ipvlan on VMware
ip addr add 192.168.50.15/24 dev mv-site4-0                         # or: dhclient
sysctl -w net.ipv4.ip_nonlocal_bind=1                               # bind not-yet-up IPs
nginx -t && nginx -s reload                                      # stop+start if reload can't bind a new IP

nginx -t gates every reload. A failing block, snippet or log format is rolled back before it touches the live proxy, and a missing per-mapping log directory is healed automatically so one unrelated change can't block everyone else's.

Two rendering paths

A Stream mapping renders to /etc/nginx/stream.d/<domain>.<port>.conf — an upstream {} pool inside nginx's stream {} block, pure Layer 4. A Reverse Proxy mapping renders to /etc/nginx/conf.d/ instead — an HTTP server {} block that terminates TLS, optionally runs each request through ModSecurity/CRS, and proxies at Layer 7. Both share the same backend pool, load-balancing, failover and snippet logic.

Generated config (Stream)

upstream upstream_924cb235c2 {
    least_conn;
    server 192.168.50.10:443;
    server 192.168.50.11:443;
}
server {
    listen 192.168.50.15:443;
    include /etc/nginx/acl.d/_default.conf;   # only when an access list is selected
    limit_conn upstream_924cb235c2_conn 10;   # from a rate-limit snippet
    proxy_pass upstream_924cb235c2;
    proxy_timeout 10m;
    proxy_connect_timeout 5s;
}

A Docker-backed backend renders the same way, except the server line's address is resolved from the container's current IP at generation time — the stored backend is the container name, not a fixed address (see Docker & Swarm).

Background schedulers

The app keeps a handful of daemon threads: backend health probing and active-passive failover, the Docker reconciler and events watcher, Let's Encrypt renewal, scheduled backups, access-list refresh, and hourly log rotation. None of them need cron.

On-host state

PathContents
/var/lib/splitter/*.jsonMappings, users, sub-interfaces, settings, snippets, firewall, activity log.
/var/lib/splitter/backups/Full-system snapshots.
/var/lib/splitter/error_pages{,_nginx}/Uploaded error-page templates and their static exports for nginx.
/etc/nginx/stream.d/ · /etc/nginx/conf.d/Generated Stream and Reverse Proxy / WAF blocks, the loopback stub_status server, snippet include files.
/etc/nginx/ssl/ · /etc/nginx/acl.d/Managed certificates (0600 keys) and rendered access lists.
/var/log/splitter/<domain>.<port>/Per-mapping access / error logs and their rotated archives.
Getting started

Dashboard tour

Every page in the sidebar, what it is for, and who can see it. Pages marked admin are hidden from creators and viewers.

PageWhat you do thereRoles
MapStream, Reverse Proxy and Forward Proxy tabs. Create, edit, preview, enable/disable and delete mappings; export/import; re-apply everything.all (viewers read-only)
DockerRunning containers / Swarm services and the mappings built from them. Same form as Map, with a container grid in the backend section.all
Live MappingAnimated routing map of every mapping, bind IP, pool and backend health, plus live nginx counters.all
MonitoringHost CPU / RAM / disk / network, per-interface throughput and nginx stub_status.all
NetworkGeneral (host DNS and hosts file, sub-interface policy), Sub-interfaces, Access Lists.all; editing is admin
SnippetsRate limits, timeouts, log rotation, log formats, config snippets, error pages — reusable items mappings pick from.admin
SSLManaged certificates: upload, self-sign, Let's Encrypt, delete.all; changes admin/creator
ToolsPing, port test, port scanner, DNS, traceroute, packet capture, WHOIS, routes, SSL checker.admin/creator
LogsPer-mapping log tail, time-range search across archives, downloads, rotation defaults.admin
WAF · FirewallModSecurity install and mode; per-interface iptables rules.admin
Backup · Users · ActivitySnapshots and restore; accounts and roles; the audit log.admin

Every option on the mapping form has an icon — hover or tap it for a one-paragraph explanation of what the setting does and when to use it.

Mappings

Stream vs. Reverse Proxy

The Map page has two tabs and the mapping form changes shape with them — a field that only takes effect on one side is not shown on the other, because nginx never reads it there.

Stream (L4)

Raw TCP/UDP passthrough or TLS termination: databases, SSH, RDP, DNS, WireGuard, QUIC, anything with a port. Backend pool, load balancing, failover, health checks, rate-limit / timeout / log-format snippets, SSL and access lists all apply.

Reverse Proxy (L7)

Everything Stream has, plus WebSocket, HTTP/2, Force-HTTPS + HSTS, per-path location rows with path & method routing, config snippets, custom error pages served by nginx, raw advanced config, and one-click ModSecurity/CRS filtering.

Creating a Reverse Proxy mapping

Pick the Reverse Proxy tab and fill in the form exactly like a Stream mapping: domain, bind target, backend pool. On save, Splitter creates the mapping and binds it behind the WAF in the same step. Only HTTPS/TLS-terminating mappings are eligible — UDP and TLS-passthrough stay Stream-only, since there is no HTTP request to inspect.

Docker-backed mappings live on their own page

A mapping whose pool includes at least one Docker container or Swarm service is Docker-managed and is listed on the Docker page rather than in Map's tables. It has the same Stream / Reverse Proxy split. See Docker & Swarm.

Mappings

Mapping form reference

Top to bottom, every field on the Add / Edit Mapping form. Blank optional fields keep the built-in defaults.

Identity & listener

FieldMeaning
Domain nameThe hostname clients connect to. With TLS passthrough it is matched against the SNI the client sends; on a terminated / reverse-proxy mapping it becomes the nginx server_name. One domain can be mapped on several listen ports.
TransportTCP for connection-based services; UDP for DNS, WireGuard, QUIC, NTP.
ProtocolA preset that fills in the usual port (HTTPS 443, MySQL 3306, RDP 3389, DNS 53, …) or Custom.
Listen portThe port this mapping accepts connections on at its bind IP. 443 enables the TLS termination options.
Parent interface / Sub-interfaceBind the interface's existing IP, or a managed sub-interface with its own IP (see Interfaces).
VLAN ID, MAC, IP allocation, Bind IP / prefixShown when sub-interface creation is enabled: 802.1Q tag, fixed or random MAC, DHCP lease or static address.

Backends

FieldMeaning
Backend poolOne or more host:port rows, or containers ticked from the Docker grid. Each row carries a weight, a down flag and a failover tier.
Load balancingOpens automatically with two or more backends: method, hash key, consistent hashing, random-two. See Load balancing.
Active-passive failoverPer-backend tiers; traffic stays on the Primary tier until all of it is down.
HTTP health checkProbe a path or full URL with an expected status instead of a bare TCP connect.

Snippets

One block for every reusable setting: one rate limit, one timeouts set, one log format, one log-rotation policy, any error pages and config snippets. Nothing selected keeps the defaults. Details in Snippets.

Reverse Proxy (L7) options

FieldMeaning
WebSocket supportForward Upgrade/Connection headers; also switches the backend connection to HTTP/1.1.
HTTP/2On the TLS-terminating listener; clients that can't fall back to HTTP/1.1.
HTTP/1.1 to backendproxy_http_version 1.1 for keep-alive / chunked backends.
Custom locationsPer-path rows: own backends, method list, snippets and extra directives. See Path & method routing.
Advanced configRaw nginx written verbatim into the server block. A mistake fails nginx -t for this mapping only and is rolled back.

SSL, access & hostname

FieldMeaning
SSL / TLSNone passes the encrypted stream through untouched (the backend holds the certificate). Managed cert terminates TLS here with a certificate from the SSL page; then choose whether to re-encrypt to the backend, force HTTPS and send HSTS.
Access listGlobal default, none, or a named allow/deny list. See Access lists.
Only allow this hostnameDrop TLS connections whose SNI is not this domain (passthrough only).

Preview renders the exact config without applying it — the stream block on the Stream form, the full HTTP server block (routing, snippets, error pages) on the Reverse Proxy form. Save / Apply provisions live and streams back every step.

Mappings

Load balancing & failover

A mapping with two or more backends auto-opens the load-balancing panel. Pick a method:

Methodnginx directiveWhen to use it
Round robin(default)Each new connection goes to the next backend in turn, honouring weights.
Least connectionsleast_connLong-lived or uneven sessions — pick the backend with the fewest active connections.
Hashhash $remote_addrSticky routing by key (default client IP); add consistent (ketama) so a backend change remaps as little as possible.
RandomrandomSimple, stateless distribution.
Random tworandom two least_connPower of two choices — pick two at random, use the less loaded one.

Active-passive failover

Each backend gets a priority tier (Primary, Backup, …). Traffic stays on the best tier with at least one healthy backend; Splitter promotes the next tier the moment every backend in the current one fails a probe, and fails back once a better tier recovers — with flap protection so a flaky backend doesn't bounce traffic back and forth. Two backends on the same tier are active-active within it.

Health checks

By default a backend is probed with a plain TCP connect. Turn on HTTP health check to request a path (probed on each backend) or a full URL, with an expected status (blank accepts any 2xx/3xx). Probes are cached and deduplicated across concurrent polls, so the dashboard can refresh often without hammering your backends. The health column, the live map and failover all read the same result.

Mappings

Path & method routing

On a Reverse Proxy mapping, Custom locations turn one domain into a small router. Each row has a path, optional backends for this path, optional methods, its own snippet picker and optional extra directives.

PathBackendsMethodsResult
/staticMain pool, with this row's extra directives (e.g. expires 7d;).
/api10.0.0.20:8080, 10.0.0.21:8080Path routing: /api is served by its own upstream.
/api10.0.0.30:8080POST, PUTMethod split: only writes go to the write node; GET stays on the row above.
/api10.0.0.31:8080DELETEThird pool for the same path.
/10.0.0.40:8080POSTSplit the whole site by method — the default location targets the map.

Rendering: every pool becomes a dedicated upstream; rows with methods feed one map $request_method per path whose default is the path's method-less pool (else the main pool), and the location proxies to that variable:

map $request_method $upstream_88de6e3b02_http_p1_target {
    default  upstream_88de6e3b02_http_p1r1;
    POST     upstream_88de6e3b02_http_p1r2;
    PUT      upstream_88de6e3b02_http_p1r2;
    DELETE   upstream_88de6e3b02_http_p1r3;
}
location /api {
    proxy_read_timeout 60s;
    proxy_pass http://$upstream_88de6e3b02_http_p1_target;
}

Rules: methods need backends (they only choose a pool); a method may appear in one row per path; at most one method-less row per path; backend hosts must resolve. Hostnames like db-write.local:5432 are fine. Location pools are plain server entries — health checks and failover apply to the main pool.

Mappings

Interfaces & sub-interfaces

The Network page detects physical and VLAN interfaces (ip -j addr) and lets you edit the host's DNS servers and /etc/hosts. Its Sub-interfaces tab holds a global toggle:

  • Off (default) — mappings bind the parent interface's existing IP. Several mappings share the IP on different ports; a shared IP:port clash is refused.
  • On — create managed macvlan / ipvlan sub-interfaces with their own MAC and IP (static, or a DHCP lease), optionally inside an 802.1Q VLAN. A mapping then simply selects one; several mappings can share a sub-interface on different ports.

Sub-interfaces are real devices: they appear in ip link, answer ARP and are reachable on the LAN. Some networks (phone hotspots, certain switches) block extra MACs — that is physics, not Splitter.

Snippets

Snippets overview

The Snippets page (admin) holds every reusable setting as a named item, one tab per kind. A mapping stores only references like ratelimit:api; the config generator resolves them at render time.

KindHoldsRendered asPer mapping
Rate limitsmax connections per client IP, download / upload rateStream: limit_conn, proxy_download_rate, proxy_upload_rate · Reverse Proxy: limit_conn, limit_rateone
Timeoutsproxy_timeout, proxy_connect_timeoutStream as-is · Reverse Proxy: proxy_read/send_timeout, proxy_connect_timeoutone
Log formatsa log_format body, stream or http contextlog_format <mapping>_fmt …one
Log rotationkeep N days, gzip, optional max sizea logrotate stanza (see Logs)one
Error pagesuploaded HTML for a status code or rangeproxy_intercept_errors + error_page + an internal locationmany
Config snippetsraw nginx directives, server / location scopean include file under conf.d/splitter-snippets/many

How it's used

  1. Create the item on the Snippets page (most kinds offer presets to start from and show a live preview of the directives they render).
  2. On any mapping — Stream, Reverse Proxy or Docker, it is the same form — pick it under Snippets. Each custom location row has its own picker (config snippets, rate limit, timeouts for that path only).
  3. Save / Apply. Nothing selected keeps the built-in defaults. Error pages and config snippets take effect on Reverse Proxy / WAF mappings; the rest apply to both kinds.

Editing a snippet re-applies every mapping that uses it — a change nginx rejects is rolled back and reported. A snippet in use (directly or through a location) cannot be deleted; the list shows which mappings use each one.

Snippets

Rate limits & timeouts

Rate limits

A rate-limit snippet caps simultaneous connections per client IP and the bytes per second of each connection (download, and upload for Stream). Values use nginx sizes such as 1m or 512k; blank means no limit for that value.

# stream
limit_conn_zone $binary_remote_addr zone=upstream_924cb_conn:10m;
limit_conn upstream_924cb_conn 10;
proxy_download_rate 1m;
proxy_upload_rate 512k;

# reverse proxy
limit_conn upstream_924cb_http_conn 10;
limit_rate 1m;

On a custom location the same snippet limits that path only — for example two connections per IP on /upload while the rest of the site stays unlimited.

Timeouts

A timeouts snippet sets how long nginx waits for a backend to accept a connection (proxy_connect_timeout) and how long an idle connection may stay open (proxy_timeout). nginx time values: 10s, 5m, 1h. Defaults are 10 minutes and 5 seconds. Use a long idle timeout for databases and SSH, a short one for busy HTTP APIs.

Snippets

Log formats

A log-format snippet is the body of an nginx log_format. Pick a context (stream for Layer-4 mappings, http for Reverse Proxy ones — a mapping only uses a snippet of its own context), an escaping mode (default, json or none) and write the line. Each line of the editor becomes one quoted string; nginx joins them, so line breaks are only for readability. Variable chips insert at the cursor and the preview shows the exact directive.

log_format upstream_924cb_fmt escape=json '{"ts":"$time_iso8601","client":"$remote_addr",'
                                          '"status":$status,"sent":$bytes_sent,'
                                          '"upstream":"$upstream_addr"}';

Five presets ship: the built-in stream line, stream JSON, HTTP combined with timings, HTTP JSON and HTTP minimal. JSON lines feed Loki, Elasticsearch or jq directly. Logs are readable on the Logs page.

Snippets

Config snippets

A config snippet is a named block of raw nginx directives with a scope — server, location or any. It is saved as an include file (/etc/nginx/conf.d/splitter-snippets/<name>.inc) and a mapping pulls it in with include: pick it in the mapping's Snippets block (server scope) or on a custom location row (location scope). Because it is a real include, editing the snippet updates every mapping that uses it — if one of those is live, the change is validated with nginx -t and reloaded, and rolled back on failure.

Presets: security headers, gzip, large uploads, streaming / SSE without buffering, static-asset caching, open CORS, block dot-files, real client IP behind Cloudflare.

# conf.d/splitter-snippets/security_headers.inc
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

Unbalanced braces are refused on save. Directives that are invalid where they are included fail nginx -t for that mapping only. Config snippets apply to Reverse Proxy / WAF mappings; plain Layer-4 stream blocks have no raw-config hook.

Snippets

Error pages

Upload an HTML / Jinja2 template keyed to an exact status code (404) or an inclusive range (400-499); an exact code always wins over a range that covers it. Uploaded pages are used in two places:

  • The dashboard's own errors. A bad URL, a rate limit or an unhandled exception renders your page instead of a bare server error. Built-in defaults cover 404, 500 and everything else. An /api/* request, or one sending Accept: application/json, always gets plain JSON.
  • Mappings. Tick a page in a Reverse Proxy mapping's Snippets block and nginx serves it for those status codes — backend responses with that code, and nginx's own 502 / 504 when the backend is down — via proxy_intercept_errors on, an error_page line and an internal-only location that serves a static export of the template. Only codes 300–599 can be served by nginx.

Templates receive status, title, message, path, method, timestamp and request_id (the static export for nginx uses generic values for the request-specific ones). Every dashboard response also carries an X-Request-Id header for correlating logs.

Backends

Docker & Swarm

Splitter runs in the host network namespace, so it can't rely on Docker's embedded DNS for service names. Instead it talks to the Docker Engine API over its unix socket to discover running containers (name, network IPs, exposed ports) — or, on a Swarm manager, services (routing-mesh published port, replica count) — and turns a chosen one into a concrete backend the host can reach directly. The host already has routes to the docker bridges, so a container's 172.x address is reachable without publishing ports.

Picking containers as backends

"+ New Stream" / "+ New Proxy" on the Docker page opens the normal mapping form. Inside the Backends section, Add from Docker reveals a card grid (checkbox, image, ports / replicas); ticking a container adds it as a backend row, right alongside manually typed host:port rows in the same pool.

Staying in sync

  • Self-healing addresses. A background reconciler periodically re-resolves every Docker-backed backend; when a container is recreated with a new IP, the mapping is re-rendered and nginx reloaded automatically.
  • Real-time reaction. A Docker events watcher streams the Engine API's event feed and fires an immediate reconcile when a container starts, stops, dies or its health check changes (and on Swarm service updates) — a dead backend drops out of the pool, and a recovered one rejoins, within a fraction of a second. The periodic poll stays on as a safety net.
Enabling it in Docker Compose: mount the socket read-only — /var/run/docker.sock:/var/run/docker.sock:ro — which the project's own docker-compose.yml already does. Remove the line if you don't want Splitter reading the Docker API at all.
Backends

Forward proxy

A separate resource from reverse-proxy mappings: a standalone outbound HTTP forward proxy, bound to its own IP:port, that your own clients route through. Each one has either an allow-all policy or a domain-pattern allowlist restricting which upstream hosts it will relay to, plus an optional access list for who may use it. Manage them from the Map page's Forward Proxy tab.

Security & SSL

SSL & Let's Encrypt

Certificates are managed on the SSL page and referenced by mappings — a mapping picks a managed certificate or passes TLS through untouched.

  • Upload an existing certificate and key.
  • Self-signed — generate a SAN certificate with openssl on the spot.
  • Let's Encrypt — request one via certbot's HTTP-01 challenge (standalone authenticator, bound briefly to the mapping's IP on port 80). A background scheduler renews it within SPLITTER_LETSENCRYPT_RENEW_DAYS (30 by default) of expiry.
  • Reuse one managed certificate across several mappings — a certificate still in use cannot be deleted.

When a mapping terminates TLS you can additionally re-encrypt to the backend (proxy_ssl on), force HTTPS (a port-80 redirect, Reverse Proxy only) and send HSTS, optionally including subdomains. Keys land at 0600; Let's Encrypt's own copy under /etc/letsencrypt/live/ is copied into the managed directory under the same naming every other source uses. The Tools page's SSL checker inspects any host's presented chain or a managed certificate on disk.

Security & SSL

Access lists

Named IP/CIDR allow lists rendered to /etc/nginx/acl.d/<name>.conf (a series of allow lines plus a final deny all;). A mapping's server {} block pulls in the one it selected via include — identical for Stream and Reverse Proxy.

# /etc/nginx/acl.d/tasx.conf — managed by Splitter
allow 82.148.0.0/21;
allow 217.30.160.0/20;
# … hundreds more
allow 127.0.0.1/32;
deny all;

Create your own by pasting CIDRs, point one at a Source URL to auto-refresh on an interval, or set a global default that every "use default" mapping follows with no re-apply. The built-in tasx list (tas-ix / Uzbekistan) ships with auto-refresh enabled. Manage them on Network → Access Lists.

Security & SSL

Firewall

Security-group-style iptables rules, one ordered chain pair per interface (SFW-IN-<iface> / SFW-OUT-<iface>): protocol, port or range, source CIDR, accept / drop / reject, priority, plus a default policy.

# established + the dashboard, always first
iptables -A SFW-IN-eth0 -m conntrack \
    --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A SFW-IN-eth0 -p tcp --dport 8088 -j ACCEPT

# your rules, by priority
iptables -A SFW-IN-eth0 -p tcp --dport 22 \
    -s 203.0.113.0/24 -j ACCEPT

# fallback = interface default policy
iptables -A SFW-IN-eth0 -j DROP

Two safety switches, both off by default — a global master switch and a per-interface enforce toggle — so installing the feature changes nothing until you opt in. Every managed chain accepts established connections and the dashboard's own port first, and a one-click Panic button tears every managed chain down if a rule locks something out.

Security & SSL

WAF

ModSecurity + the OWASP Core Rule Set, fronting the dashboard itself and any Reverse Proxy mapping. nginx terminates TLS on :8443, filters every request (SQLi, XSS, RCE, …) with per-IP login rate limiting, then proxies onward. The module and rule set are baked into the image, so the WAF survives redeploys.

Install / repair it from the WAF page (admin), then switch modes:

ModeBehaviour
OffNo inspection.
DetectionLogs what it would have blocked — the default after install, so tuning false positives can't lock you out.
EnforceBlocks matching requests.

Creating a Reverse Proxy mapping binds it behind the WAF automatically. The WAF page's Protected apps list shows every bound mapping, with a one-click unbind back to a plain Layer-4 stream.

Security & SSL

Users, roles & audit

First run creates an admin; after that the whole UI requires login. Passwords are stored only as salted PBKDF2 hashes, sessions are cookie-only, and the last admin cannot be deleted.

RoleCan
adminEverything: users, snippets, WAF, firewall, backups, logs, delete / import / re-apply.
creatorAdd and edit mappings, certificates and forward proxies, use the Tools page, export. No delete, import or re-apply.
viewerRead-only. Sees every page's data; every create / edit / delete control is hidden and the API answers 403.

The Activity page keeps an append-only audit log of logins, mapping and snippet changes, user management, WAF / firewall actions and re-applies — who did what, and when.

Operations

Logs & rotation

Every mapping writes its own access and error log under /var/log/splitter/<domain>.<port>/. The Logs page (admin) lists every mapping with file sizes, archive counts and the rotation policy in force.

Live tail

Open a mapping to tail its access or error log (last 200–5000 lines) with substring search, auto-refresh and a download of the full file.

Searching by time — "what happened two days ago at 15:40?"

The Time range row takes a from / to date-time (in your browser's time zone) or a quick pick (last 1h / 24h / 7d). The search reads the live file and every rotated archive, gzip-compressed ones included, and returns the lines whose own timestamp falls in the window — CLF, ISO 8601 and nginx error-log formats are all recognised. Each result is prefixed with the archive date it came from, and the search box still filters within the window. Files & rotation lists every archive with size and date, each downloadable.

Rotation & retention

Rotation runs through the real logrotate: daily, a -YYYYMMDD suffix, gzip, deleted after the retention period, and nginx -s reopen afterwards. Splitter regenerates the config and runs it hourly from a background thread (logrotate itself keeps to once a day), so no cron is needed; Rotate now forces a run. The defaults — 7 days, gzip, optional size trigger — are on the Logs page; a Log rotation snippet on a mapping overrides them for that mapping (for example keep 5 days for a noisy site, 90 for an audited one).

# generated stanza for one mapping
/var/log/splitter/shop.example.com.443/shop.example.com.443-access.log … {
    daily
    dateext
    rotate 5
    maxage 5
    compress
    delaycompress
    postrotate
        nginx -s reopen
    endscript
}

The most recent archive stays uncompressed until the next rotation (delaycompress, so nginx never writes into a file being gzipped); from the second day on archives are .gz.

Operations

Monitoring & live map

The Monitoring page reads host CPU, RAM, disk and per-interface network throughput straight from /proc — no agent — and shows live nginx counters from the built-in stub_status module: active connections, requests and connections per second, requests per connection, and the reading / writing / waiting split. Splitter provisions the status endpoint itself, as a loopback-only server block on 127.0.0.1:8090; if it is missing, the card offers to enable it.

stub_status counts nginx's HTTP layer, i.e. Reverse Proxy / WAF mappings. Plain Layer-4 stream mappings show their per-mapping connection counts in the Traffic column of the mappings table instead.

The Live Mapping page draws an animated canvas of every mapping, its bind IP and sub-interface, the proxy host and each backend pool, flagging a downed backend in red the moment a probe fails; the same nginx counters sit in its header.

Operations

Tools

Network diagnostics run on the host, from the browser (admin / creator; packet capture is admin-only). Inputs are validated and every command runs as an argv list — never a shell string.

ToolWhat it does
PingICMP echo to a host, 1–20 packets.
Port testerOne TCP connect to host:port with a timeout — open, refused or filtered.
Port scannernmap: TCP connect, SYN, UDP or host-discovery scans of a host, CIDR block or range; top 100 / top 1000 / custom / all ports; timing T2–T5; optional service detection (-sV) and skip-ping (-Pn). A pure-Python connect scan takes over if nmap is missing.
DNS lookupdig for A, AAAA, MX, NS, TXT, CNAME, PTR, SOA against an optional server.
TracerouteHop-by-hop path with a max-hops cap.
Packet capturetcpdump on any interface with a BPF filter and packet count (admin).
WHOISRegistration data for a domain or IP.
Routing tablesThe host's kernel routes.
SSL checkerConnect to any host and inspect the presented certificate and chain, or read a managed certificate off disk.
Only scan hosts and networks you are authorised to test. Large ranges or the all-ports option can take minutes; scans are capped at five minutes.
Operations

Backup & restore

Export just the mappings as JSON (admin / creator), or take a full-system snapshot — mappings, users, certificates and keys, sub-interfaces, snippets, settings and the audit log — as a timestamped zip. Schedule automatic snapshots on an interval with a retention count, and restore or roll back to any stored point with one click. After restoring mappings, Re-apply all provisions them onto the host again. Treat a downloaded backup as a secret: it contains private keys and password hashes.

Reference

Configuration

Everything is environment-overridable, so the same code runs in simulation on a laptop and live on the host. Set these in docker-compose.yml or the systemd unit.

VariableDefaultPurpose
SPLITTER_NICauto / eth0Default parent interface (auto in Docker detects it from the default route).
SPLITTER_BIND_PREFIX24CIDR prefix for static IPs.
SPLITTER_HOST / SPLITTER_PORT0.0.0.0 / 8088UI bind address and port.
SPLITTER_DATA_DIR/var/lib/splitterStore for mappings, users, settings, snippets, backups.
SPLITTER_STREAM_DIR/etc/nginx/stream.dWhere Stream .conf files are written.
SPLITTER_SSL_DIR/etc/nginx/sslManaged certificates and keys.
SPLITTER_SNIPPET_DIR/etc/nginx/conf.d/splitter-snippetsConfig-snippet include files.
SPLITTER_STATUS_PORT8090Loopback port of the nginx stub_status endpoint.
SPLITTER_WAF_PORT8443Port of the WAF-fronted dashboard listener.
SPLITTER_LETSENCRYPT_RENEW_DAYS30Auto-renew a Let's Encrypt certificate within this many days of expiry.
SPLITTER_PROXY_TIMEOUT / SPLITTER_PROXY_CONNECT_TIMEOUT10m / 5sBuilt-in timeouts when no snippet sets them.
SPLITTER_RESOLV_CONF / SPLITTER_HOSTS_FILE/etc/resolv.conf / /etc/hostsHost files the Network page edits.
SPLITTER_DHCP_ACQUIRE_CMDtimeout 25 dhclient -1 {iface}DHCP client command for sub-interfaces.
SPLITTER_SUDO"" if root else sudoPrefix for privileged commands.
SPLITTER_RELOAD_CMD / SPLITTER_RESTART_CMDnginx binaryHow nginx is reloaded / restarted.
SPLITTER_IP_NONLOCAL_BIND1Bind an address that isn't fully up yet.
SPLITTER_REBOOT_CMDrebootSet to true in Docker, where a host reboot can't work.
SPLITTER_SIMULATEauto1 = dry run: prints the commands it would run and touches nothing. Auto-on when not on a Linux nginx host.

Running as a non-root user is possible with the scoped sudoers file shipped under deploy/; the README covers it.

Reference

REST API

Everything the dashboard does goes through /api/* with the same session and roles (viewer gets 403 on any write). Requests are form-encoded; responses are JSON with ok, and a provisioning response includes a steps[] array showing exactly what ran, in order.

# log in, then list mappings with the session cookie
curl -c jar -d username=admin -d password='…' http://host:8088/api/login
curl -b jar http://host:8088/api/mappings
MethodPathRoleDescription
POST/api/setup · /api/login · /api/logoutFirst admin; start / end a session. GET /api/auth/status shows the current one.
GET/api/configanyEffective configuration and mode.
GET/api/mappingsanyEvery mapping (Stream, Reverse Proxy, Docker-backed).
POST/api/mappingsadmin / creatorCreate or update and provision (fields mirror the form: domain, listen_port, backends, snippets, locations_json, …).
DELETE/api/mappings/<domain>?port=adminDeprovision and remove. /toggle enables / disables, /diagnose inspects.
POST/api/previewanyRender the config without applying (l7=1 for the Reverse Proxy block).
POST/api/reapplyadminRe-provision every stored mapping.
GET/api/health · /api/trafficanyPer-backend up/down rollup; per-mapping connection counts.
GET/api/interfaces · /api/subinterfacesany / adminDetected interfaces and throughput; managed sub-interfaces (POST / DELETE admin).
GET/api/settings · /api/network/dns · /api/network/hostsany / adminTool-wide settings (incl. log-rotation defaults), host DNS and hosts file.
GET/api/snippetsanyThe full catalogue for the mapping picker, by kind.
POST/api/snippets/{ratelimit,timeouts,logrotate}adminCreate / update a snippet of that kind; DELETE …/<name> (409 while in use).
POST/api/log-formats · /api/config-snippets · /api/error-pagesadminThe other snippet kinds (GET lists for any role; DELETE guarded the same way).
GET/api/ssl/certsanyManaged certificates; POST uploads / self-signs / requests Let's Encrypt (admin / creator).
GET/api/access-listsanyAccess lists (POST / DELETE admin; /refresh re-fetches a sourced list).
GET/api/forward-proxiesanyForward proxies (POST / DELETE admin / creator).
GET/api/docker/status · containers · servicesanyDocker / Swarm discovery.
GET/api/logs · /api/logs/<domain>/<port>/<kind>adminOverview; tail (?lines=&q=). Add /search?from=&to=&q=, /files, /download?file=.
POST/api/logs/rotate · GET /api/logs/rotationadminRun logrotate now (force=1); rotation status and generated config.
GET/api/metrics · /api/nginx/statusanyHost resources; nginx stub_status counters (POST …/provision, admin).
POST/api/tools/{ping,port,portscan,dns,traceroute,tcpdump,whois,routes,sslcheck}admin / creatorNetwork diagnostics.
GET/api/waf/status · POST /api/waf/{install,mode,bind,unbind}adminManage the WAF.
GET/api/firewall/overview · POST /api/firewall/panic · …adminPer-interface firewall state, rules, panic.
GET/api/backup · /api/backupsadmin / creator · adminMapping export; snapshots (take, download, restore, schedule).
GET/api/users · /api/activityadminAccounts (POST / DELETE) and the audit log. POST /api/account/password for your own password.

The complete, authoritative list is the README's endpoint reference and backend/app.py in the repository.

Reference

Troubleshooting

  • bind() ... (99: Cannot assign requested address) — the bind IP wasn't fully up yet. Confirm sysctl net.ipv4.ip_nonlocal_bind prints 1.
  • A change "didn't take" — after git pull, run sudo systemctl restart splitter (native) or docker compose up -d --build (Docker) to load the new code. In the browser, hard refresh (Ctrl+Shift+R) to drop the cached script.
  • An apply returns 500journalctl -u splitter -f (or docker compose logs -f) logs the failing step; the dashboard's Provisioning Steps panel shows the same trace.
  • Backend host can't be resolved — nginx resolves upstream hostnames at load time. Use an IP, a DNS name the host can resolve, or pick the container from the Docker page so its name is resolved for you.
  • Docker page is empty or hidden — Splitter couldn't reach the Docker Engine socket. Confirm /var/run/docker.sock is mounted (Compose) or readable by the process (native).
  • nginx says a snippet directive is not allowed here — the config snippet's directives don't belong in that context (server vs. location). Move the include to the other picker; the mapping was rolled back to its previous config.
  • Monitoring shows "stub_status unavailable" — click Enable stub_status; Splitter writes the loopback server block and reloads nginx. Check that port 8090 (or SPLITTER_STATUS_PORT) is free.
  • A time-range search finds nothing — the window is outside the retention period, or the log format has no recognisable timestamp. Check the Files & rotation list for the archives that still exist.