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.
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.
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.
| Flag | Meaning |
|---|---|
--service | Install and enable the systemd service. |
--nic NAME | Force the parent interface (otherwise auto-detected). |
--host ADDR / --port N | UI bind address and port (default 0.0.0.0:8088). |
--no-deps | Skip package installation. |
--waf / --waf-only / --waf-enforce | Install, repair or enforce the WAF. |
systemctl status splitter journalctl -u splitter -f # apply failures land here systemctl restart splitter # after every git pull
: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.
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_statusendpoint, 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.
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
| Path | Contents |
|---|---|
/var/lib/splitter/*.json | Mappings, 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. |
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.
| Page | What you do there | Roles |
|---|---|---|
| Map | Stream, Reverse Proxy and Forward Proxy tabs. Create, edit, preview, enable/disable and delete mappings; export/import; re-apply everything. | all (viewers read-only) |
| Docker | Running containers / Swarm services and the mappings built from them. Same form as Map, with a container grid in the backend section. | all |
| Live Mapping | Animated routing map of every mapping, bind IP, pool and backend health, plus live nginx counters. | all |
| Monitoring | Host CPU / RAM / disk / network, per-interface throughput and nginx stub_status. | all |
| Network | General (host DNS and hosts file, sub-interface policy), Sub-interfaces, Access Lists. | all; editing is admin |
| Snippets | Rate limits, timeouts, log rotation, log formats, config snippets, error pages — reusable items mappings pick from. | admin |
| SSL | Managed certificates: upload, self-sign, Let's Encrypt, delete. | all; changes admin/creator |
| Tools | Ping, port test, port scanner, DNS, traceroute, packet capture, WHOIS, routes, SSL checker. | admin/creator |
| Logs | Per-mapping log tail, time-range search across archives, downloads, rotation defaults. | admin |
| WAF · Firewall | ModSecurity install and mode; per-interface iptables rules. | admin |
| Backup · Users · Activity | Snapshots 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.
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.
Mapping form reference
Top to bottom, every field on the Add / Edit Mapping form. Blank optional fields keep the built-in defaults.
Identity & listener
| Field | Meaning |
|---|---|
| Domain name | The 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. |
| Transport | TCP for connection-based services; UDP for DNS, WireGuard, QUIC, NTP. |
| Protocol | A preset that fills in the usual port (HTTPS 443, MySQL 3306, RDP 3389, DNS 53, …) or Custom. |
| Listen port | The port this mapping accepts connections on at its bind IP. 443 enables the TLS termination options. |
| Parent interface / Sub-interface | Bind the interface's existing IP, or a managed sub-interface with its own IP (see Interfaces). |
| VLAN ID, MAC, IP allocation, Bind IP / prefix | Shown when sub-interface creation is enabled: 802.1Q tag, fixed or random MAC, DHCP lease or static address. |
Backends
| Field | Meaning |
|---|---|
| Backend pool | One 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 balancing | Opens automatically with two or more backends: method, hash key, consistent hashing, random-two. See Load balancing. |
| Active-passive failover | Per-backend tiers; traffic stays on the Primary tier until all of it is down. |
| HTTP health check | Probe 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
| Field | Meaning |
|---|---|
| WebSocket support | Forward Upgrade/Connection headers; also switches the backend connection to HTTP/1.1. |
| HTTP/2 | On the TLS-terminating listener; clients that can't fall back to HTTP/1.1. |
| HTTP/1.1 to backend | proxy_http_version 1.1 for keep-alive / chunked backends. |
| Custom locations | Per-path rows: own backends, method list, snippets and extra directives. See Path & method routing. |
| Advanced config | Raw nginx written verbatim into the server block. A mistake fails nginx -t for this mapping only and is rolled back. |
SSL, access & hostname
| Field | Meaning |
|---|---|
| SSL / TLS | None 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 list | Global default, none, or a named allow/deny list. See Access lists. |
| Only allow this hostname | Drop 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.
Load balancing & failover
A mapping with two or more backends auto-opens the load-balancing panel. Pick a method:
| Method | nginx directive | When to use it |
|---|---|---|
| Round robin | (default) | Each new connection goes to the next backend in turn, honouring weights. |
| Least connections | least_conn | Long-lived or uneven sessions — pick the backend with the fewest active connections. |
| Hash | hash $remote_addr | Sticky routing by key (default client IP); add consistent (ketama) so a backend change remaps as little as possible. |
| Random | random | Simple, stateless distribution. |
| Random two | random two least_conn | Power 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.
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.
| Path | Backends | Methods | Result |
|---|---|---|---|
/static | — | — | Main pool, with this row's extra directives (e.g. expires 7d;). |
/api | 10.0.0.20:8080, 10.0.0.21:8080 | — | Path routing: /api is served by its own upstream. |
/api | 10.0.0.30:8080 | POST, PUT | Method split: only writes go to the write node; GET stays on the row above. |
/api | 10.0.0.31:8080 | DELETE | Third pool for the same path. |
/ | 10.0.0.40:8080 | POST | Split 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.
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 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.
| Kind | Holds | Rendered as | Per mapping |
|---|---|---|---|
| Rate limits | max connections per client IP, download / upload rate | Stream: limit_conn, proxy_download_rate, proxy_upload_rate · Reverse Proxy: limit_conn, limit_rate | one |
| Timeouts | proxy_timeout, proxy_connect_timeout | Stream as-is · Reverse Proxy: proxy_read/send_timeout, proxy_connect_timeout | one |
| Log formats | a log_format body, stream or http context | log_format <mapping>_fmt … | one |
| Log rotation | keep N days, gzip, optional max size | a logrotate stanza (see Logs) | one |
| Error pages | uploaded HTML for a status code or range | proxy_intercept_errors + error_page + an internal location | many |
| Config snippets | raw nginx directives, server / location scope | an include file under conf.d/splitter-snippets/ | many |
How it's used
- Create the item on the Snippets page (most kinds offer presets to start from and show a live preview of the directives they render).
- 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).
- 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.
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.
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.
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.
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 sendingAccept: 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, anerror_pageline 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.
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.
/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.
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.
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
opensslon 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 withinSPLITTER_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.
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.
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.
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:
| Mode | Behaviour |
|---|---|
| Off | No inspection. |
| Detection | Logs what it would have blocked — the default after install, so tuning false positives can't lock you out. |
| Enforce | Blocks 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.
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.
| Role | Can |
|---|---|
| admin | Everything: users, snippets, WAF, firewall, backups, logs, delete / import / re-apply. |
| creator | Add and edit mappings, certificates and forward proxies, use the Tools page, export. No delete, import or re-apply. |
| viewer | Read-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.
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.
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.
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.
| Tool | What it does |
|---|---|
| Ping | ICMP echo to a host, 1–20 packets. |
| Port tester | One TCP connect to host:port with a timeout — open, refused or filtered. |
| Port scanner | nmap: 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 lookup | dig for A, AAAA, MX, NS, TXT, CNAME, PTR, SOA against an optional server. |
| Traceroute | Hop-by-hop path with a max-hops cap. |
| Packet capture | tcpdump on any interface with a BPF filter and packet count (admin). |
| WHOIS | Registration data for a domain or IP. |
| Routing tables | The host's kernel routes. |
| SSL checker | Connect to any host and inspect the presented certificate and chain, or read a managed certificate off disk. |
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.
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.
| Variable | Default | Purpose |
|---|---|---|
SPLITTER_NIC | auto / eth0 | Default parent interface (auto in Docker detects it from the default route). |
SPLITTER_BIND_PREFIX | 24 | CIDR prefix for static IPs. |
SPLITTER_HOST / SPLITTER_PORT | 0.0.0.0 / 8088 | UI bind address and port. |
SPLITTER_DATA_DIR | /var/lib/splitter | Store for mappings, users, settings, snippets, backups. |
SPLITTER_STREAM_DIR | /etc/nginx/stream.d | Where Stream .conf files are written. |
SPLITTER_SSL_DIR | /etc/nginx/ssl | Managed certificates and keys. |
SPLITTER_SNIPPET_DIR | /etc/nginx/conf.d/splitter-snippets | Config-snippet include files. |
SPLITTER_STATUS_PORT | 8090 | Loopback port of the nginx stub_status endpoint. |
SPLITTER_WAF_PORT | 8443 | Port of the WAF-fronted dashboard listener. |
SPLITTER_LETSENCRYPT_RENEW_DAYS | 30 | Auto-renew a Let's Encrypt certificate within this many days of expiry. |
SPLITTER_PROXY_TIMEOUT / SPLITTER_PROXY_CONNECT_TIMEOUT | 10m / 5s | Built-in timeouts when no snippet sets them. |
SPLITTER_RESOLV_CONF / SPLITTER_HOSTS_FILE | /etc/resolv.conf / /etc/hosts | Host files the Network page edits. |
SPLITTER_DHCP_ACQUIRE_CMD | timeout 25 dhclient -1 {iface} | DHCP client command for sub-interfaces. |
SPLITTER_SUDO | "" if root else sudo | Prefix for privileged commands. |
SPLITTER_RELOAD_CMD / SPLITTER_RESTART_CMD | nginx binary | How nginx is reloaded / restarted. |
SPLITTER_IP_NONLOCAL_BIND | 1 | Bind an address that isn't fully up yet. |
SPLITTER_REBOOT_CMD | reboot | Set to true in Docker, where a host reboot can't work. |
SPLITTER_SIMULATE | auto | 1 = 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.
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
| Method | Path | Role | Description |
|---|---|---|---|
| POST | /api/setup · /api/login · /api/logout | — | First admin; start / end a session. GET /api/auth/status shows the current one. |
| GET | /api/config | any | Effective configuration and mode. |
| GET | /api/mappings | any | Every mapping (Stream, Reverse Proxy, Docker-backed). |
| POST | /api/mappings | admin / creator | Create or update and provision (fields mirror the form: domain, listen_port, backends, snippets, locations_json, …). |
| DELETE | /api/mappings/<domain>?port= | admin | Deprovision and remove. /toggle enables / disables, /diagnose inspects. |
| POST | /api/preview | any | Render the config without applying (l7=1 for the Reverse Proxy block). |
| POST | /api/reapply | admin | Re-provision every stored mapping. |
| GET | /api/health · /api/traffic | any | Per-backend up/down rollup; per-mapping connection counts. |
| GET | /api/interfaces · /api/subinterfaces | any / admin | Detected interfaces and throughput; managed sub-interfaces (POST / DELETE admin). |
| GET | /api/settings · /api/network/dns · /api/network/hosts | any / admin | Tool-wide settings (incl. log-rotation defaults), host DNS and hosts file. |
| GET | /api/snippets | any | The full catalogue for the mapping picker, by kind. |
| POST | /api/snippets/{ratelimit,timeouts,logrotate} | admin | Create / update a snippet of that kind; DELETE …/<name> (409 while in use). |
| POST | /api/log-formats · /api/config-snippets · /api/error-pages | admin | The other snippet kinds (GET lists for any role; DELETE guarded the same way). |
| GET | /api/ssl/certs | any | Managed certificates; POST uploads / self-signs / requests Let's Encrypt (admin / creator). |
| GET | /api/access-lists | any | Access lists (POST / DELETE admin; /refresh re-fetches a sourced list). |
| GET | /api/forward-proxies | any | Forward proxies (POST / DELETE admin / creator). |
| GET | /api/docker/status · containers · services | any | Docker / Swarm discovery. |
| GET | /api/logs · /api/logs/<domain>/<port>/<kind> | admin | Overview; tail (?lines=&q=). Add /search?from=&to=&q=, /files, /download?file=. |
| POST | /api/logs/rotate · GET /api/logs/rotation | admin | Run logrotate now (force=1); rotation status and generated config. |
| GET | /api/metrics · /api/nginx/status | any | Host resources; nginx stub_status counters (POST …/provision, admin). |
| POST | /api/tools/{ping,port,portscan,dns,traceroute,tcpdump,whois,routes,sslcheck} | admin / creator | Network diagnostics. |
| GET | /api/waf/status · POST /api/waf/{install,mode,bind,unbind} | admin | Manage the WAF. |
| GET | /api/firewall/overview · POST /api/firewall/panic · … | admin | Per-interface firewall state, rules, panic. |
| GET | /api/backup · /api/backups | admin / creator · admin | Mapping export; snapshots (take, download, restore, schedule). |
| GET | /api/users · /api/activity | admin | Accounts (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.
Troubleshooting
bind() ... (99: Cannot assign requested address)— the bind IP wasn't fully up yet. Confirmsysctl net.ipv4.ip_nonlocal_bindprints1.- A change "didn't take" — after
git pull, runsudo systemctl restart splitter(native) ordocker 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 500 —
journalctl -u splitter -f(ordocker 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.sockis 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(orSPLITTER_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.