Every part of the router on one screen.
tui-router is the read-only cockpit for a Linux router. It reads the machine with cheap system probes and draws one card per area — the interfaces and their WAN/LAN roles, the firewall's posture, live traffic per interface, the DHCP server and its leases, the WireGuard peers, the self-hosted tailnet — and refreshes them in place.
It changes nothing itself. Press ENTER on a card and tui-router suspends, hands the terminal to the tool that manages that area — tui-firewall, tui-network, tui-traffic, tui-wireguard, tui-tailscale — and resumes when you leave it, the same handoff the family launcher uses. The overview is here; the change happens in the tool the card opens, behind that tool's own preview and confirm.
Beta. The family is days old and still changing. Package names, flags and keys may move without notice until 1.0. Pin versions, and report what breaks.
| Card | What it reads | Managed by |
|---|---|---|
| Interfaces | each link, up/down, its IPv4, and whether it looks like WAN (a default route) or LAN (an attached subnet) — from ip -j addr and ip -j route |
tui-network |
| Firewall | which backend is active (firewalld, nftables or ufw) and a one-line posture: default policy, rule count, whether NAT masquerades | tui-firewall |
| Traffic | the live throughput per interface, a small /proc/net/dev delta refreshed on a timer |
tui-traffic |
| DHCP | which DHCP server is serving the LAN and what it hands out: dnsmasq or Kea by their package, or systemd-networkd's own server — the .network unit (and its drop-ins) that carries [DHCPServer], the link it matches, the pool its Address= and PoolOffset=/PoolSize= work out to, and the leases networkctl status <link> lists |
tui-network |
| VPN | any WireGuard interface and its peer count (from wg show), and whether a headscale control plane is present |
tui-wireguard |
| Tailnet | this host on a self-hosted tailnet, read from tui-tailscale --check (cached, re-read every minute): whether the node is online and how many peers it sees, whether this host runs the headscale control plane, its node count and the first step it still lacks (the next of tui-tailscale's readiness); "tui-tailscale (not installed)" when the binary is absent |
tui-tailscale |
| Updates | the pending and security update counts, read from tui-update --check (cached, re-read every few minutes); "tui-update not installed" when the binary is absent |
tui-update |
tui-wireguard was called tui-vpn before 0.5.0. On a machine that still has
only the old tui-vpn binary, the VPN card hands off to it and the status line
says it was renamed, so upgrade the package. The headscale control plane the
VPN card detects has its own card, Tailnet, which hands off to
tui-tailscale.
Every read is cheap and read-only. Most are unprivileged; the few that need
root — the nftables ruleset, ufw status, the WireGuard dump — escalate with
sudo -n, which never prompts. A card that cannot escalate reads unknown with
the reason, rather than failing the whole screen.
A fresh Omarchy Server router
boots into safe mode: /etc/omarchy/router/roles.conf exists but assigns no
WAN or LAN role, so every port takes a DHCP lease and nothing is forwarded —
reachable, not open, and not locked out. Until now no tool in the family could
assign those roles. The cockpit closes that gap.
On a router-profile host whose roles are not both assigned, the cockpit shows
a banner and w opens the roles wizard:
- Select. Every NIC is listed with its name, MAC, link state and current
IP. Mark each one WAN, LAN or unassigned — a role is a set, so several WAN
uplinks (failover) and several LAN ports (bridged into
br-lan) are fine, and the same port may carry both.mpins a NIC's role to its MAC instead of its name, so the assignment survives a kernel rename. - Preview the write. The wizard shows a unified diff of
roles.conf, the output ofomarchy-router-nics --preview(run read-only), and the exactinstall -m 644command. Confirming writes the file — through a staged temp file, never a partial write — and nothing else. - Preview the apply. A second, danger-colored confirm shows every command
of the apply sequence and warns that an SSH session may drop. Before the
apply runs, the wizard stages the previous
roles.confasroles.conf.prevand arms a timed revert withsystemd-run: unless connectivity is confirmed within 120 seconds, the previous assignment is restored and re-applied automatically. Thenomarchy-router-nics --applyrewrites the.networkunits and reloads networkd. - Confirm connectivity. If the session held, one more previewed command
(
systemctl stop tui-router-roles-revert.service …) disarms the revert and the new mapping is permanent. If it did not, wait two minutes and the router comes back on the old assignment.
Where systemd-run is not available the wizard degrades honestly: no
automatic revert is armed, and the result screen prints the exact manual
revert commands instead.
Everything the wizard writes is validated first — interface names and MACs
only, in strict shapes — because roles.conf is sourced by shell and must
never carry anything shell could interpret. --demo walks the whole wizard
against the sample router, running nothing.
B reboots and P powers off, each behind a typed confirm: the dialog shows
the exact systemctl command and runs it only after you type the action's own
name (reboot / poweroff). A router's power is the one thing heavier than
any card, so a plain y/n is not enough.
tui-router --demo--demo drives a sample office router — two interfaces, an active firewall,
live traffic, a systemd-networkd DHCP server handing out leases on the LAN, a
WireGuard interface with peers, a tailnet node that also runs a headscale
control plane one step short of ready, a pending-updates count, and an unassigned
roles.conf so the roles wizard can be walked end to end — every card renders
and every flow works on a machine that has none of these backends installed.
Nothing is read and nothing is changed.
git clone https://github.com/tui-tools/tui-router
cd tui-router && make demoOr run it against a sample router with tui-router --demo, which needs nothing
installed.
Not packaged for these yet; the static binary works everywhere in the meantime.
Needs the tui-tools repository, which is a one-time setup.
The one-liner detects the distribution and adds the repository and its signing key:
curl -fsSL https://pkgs.tui.tools/install.sh | shPiping a script into a shell is not this family's style, so here is the same
setup by hand — read it, or read the script first with curl -fsSL https://pkgs.tui.tools/install.sh -o install.sh:
curl -fsSL -o /tmp/tui-tools.asc https://pkgs.tui.tools/pubkey.asc
sudo pacman-key --add /tmp/tui-tools.asc
sudo pacman-key --lsign-key \
"$(gpg --show-keys --with-colons /tmp/tui-tools.asc | awk -F: '/^fpr:/{print $10; exit}')"
printf '[tui-tools]\nServer = https://pkgs.tui.tools/arch/$arch\n' \
| sudo tee -a /etc/pacman.conf
sudo pacman -SyThen, and for every other tool in the family:
sudo pacman -S tui-routerAvailable once tui-router's first release lands in pkgs.tui.tools.
Needs the tui-tools repository, which is a one-time setup.
The one-liner detects the distribution and adds the repository and its signing key:
curl -fsSL https://pkgs.tui.tools/install.sh | shPiping a script into a shell is not this family's style, so here is the same
setup by hand — read it, or read the script first with curl -fsSL https://pkgs.tui.tools/install.sh -o install.sh:
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://pkgs.tui.tools/pubkey.asc \
| sudo gpg --dearmor -o /etc/apt/keyrings/tui-tools.gpg
echo "deb [signed-by=/etc/apt/keyrings/tui-tools.gpg] https://pkgs.tui.tools/deb stable main" \
| sudo tee /etc/apt/sources.list.d/tui-tools.list
sudo apt updateThen, and for every other tool in the family:
sudo apt install tui-routerAvailable once tui-router's first release lands in pkgs.tui.tools.
Needs the tui-tools repository, which is a one-time setup.
The one-liner detects the distribution and adds the repository and its signing key:
curl -fsSL https://pkgs.tui.tools/install.sh | shPiping a script into a shell is not this family's style, so here is the same
setup by hand — read it, or read the script first with curl -fsSL https://pkgs.tui.tools/install.sh -o install.sh:
sudo rpm --import https://pkgs.tui.tools/pubkey.asc
sudo curl -fsSL -o /etc/yum.repos.d/tui-tools.repo https://pkgs.tui.tools/rpm/tui-tools.repo
sudo dnf makecacheThen, and for every other tool in the family:
sudo dnf install tui-routerAvailable once tui-router's first release lands in pkgs.tui.tools.
curl -fsSL https://github.com/tui-tools/tui-router/releases/download/v0.4.0/tui-router_0.4.0_linux_amd64.tar.gz | tar -xz tui-router
sudo install -m0755 tui-router /usr/local/bin/tui-routerAvailable once tui-router is released.
Every release of tui-router ships a checksums.txt. Check an archive against
it before installing:
sha256sum -c checksums.txt --ignore-missingWebsite: https://tui.tools/tools/tui-router/
tui-router [flags]
| Flag | What it does |
|---|---|
--demo |
run against a sample router, reading and changing nothing |
--check |
read every card once, print the result as JSON, and exit (no UI) |
--report |
print the versions and machine facts a bug report needs, then exit |
--theme PATH |
use an Omarchy-style colors.toml |
--sudo PREFIX |
privilege escalation prefix, e.g. sudo -n, or "" to disable |
--version |
print the version and exit |
Keys: ↑/↓ select a card, enter opens the tool that manages it, w opens
the roles wizard (router profile), b opens the backup screen, B/P
reboot/poweroff behind a typed confirm, r re-reads the router now, ? shows
help, q quits.
--check runs the read path only — never a handoff — so it is safe against a
live router. Its exit code reports whether the tool could read, never a verdict
about the machine: a router with no firewall is a successful run whose findings
travel in the JSON.
tui-router export writes one self-describing, integrity-checked artifact that
captures everything a router needs to be itself again:
| Subsystem | What travels |
|---|---|
| roles | /etc/omarchy/router/roles.conf — which ports play WAN and which play LAN |
| networkd | the .network / .link units from /etc/systemd/network |
| sysctl | /etc/sysctl.d/30-omarchy-router.conf, the forwarding knobs |
| resolved | /etc/systemd/resolved.conf.d/30-omarchy-router.conf |
| dhcp-dns | the dnsmasq config the router profile owns |
| wireguard | each interface's config, key material stripped and referenced by path |
| firewall-rules | tui-firewall's saved ruleset, when that tool manages this router |
| nftables | the live ruleset, as the plain form nft -f reloads |
| accounts | the router's own users and groups — names and roles only |
tui-router restore reads that artifact back, previews it, and applies it
after one explicit confirmation. The cockpit's b key runs both flows on
screen, with the same previews and the same confirmations; the subcommands stay
because a backup you can put in a cron job is worth more than one you can only
press a key for.
tui-router export [--out FILE] [--sign KEY] [--demo]
tui-router restore FILE [--verify PUBKEY] [--dry-run] [--keep] [--demo]
| Flag | What it does |
|---|---|
export --out FILE |
write the artifact here (default router-<host>-<stamp>.tuiback) |
export --sign KEY |
add a detached Ed25519 signature over the checksum file |
restore --verify PUBKEY |
require a valid signature from this public key |
restore --dry-run |
verify and preview only; apply nothing |
restore --keep |
keep the restored ruleset without the connectivity confirmation |
--demo |
run the whole loop against the in-memory sample router, no root |
The artifact is a gzip'd tar (.tuiback) with a manifest.json, one part per
subsystem, an always-present MANIFEST.sha256, and — only when you pass a key —
a detached SIGNATURE. Integrity is unconditional: a single altered byte makes
restore refuse. A signature is optional and is checked only when you pass
--verify; no key is ever required, and no secret is ever written to disk or
into the artifact. WireGuard key material is stripped from the config and
referenced by path — you provision it out of band — and accounts carry no
credential hashes in this stage.
restore never applies silently. It shows a per-subsystem diff, then the exact
reload commands it will run, then takes one typed confirmation. What runs, in
order:
- The config files land. Every subsystem the artifact carries is written
to its fixed path — the paths are a closed set in the backend, never derived
from the artifact, so a crafted file cannot steer a write elsewhere.
roles.confis re-parsed and re-rendered through the profile's own validator on the way in: it is sourced by shell, so only validated interface names and MACs can ever reach it. - What was written is reloaded —
networkctl reload,systemctl restart systemd-resolved,systemctl restart dnsmasq(when dnsmasq is installed),sysctl --system, andwg-quick down/upfor each restored WireGuard interface. Every one of these is previewed before the confirmation, and the ones that can drop your session are marked. A file on disk that nothing re-read is not a restored router. - The ruleset applies last, as one atomic
nft -ftransaction with a connectivity-safe rollback: the live ruleset is snapshotted first, and if you do not confirm within 60 seconds that you still have access, it reverts on its own. That is why nftables goes last — a bad ruleset is what locks an operator out, and by then everything else is already in place.
--dry-run stops after the preview.
Scripted restores. The keep confirmation is read from stdin, so
printf 'yes\nkeep\n' | tui-router restore FILE works. A restore nobody is
sitting in front of can skip the 60-second window with --keep, which keeps
the ruleset the moment it applies. It is for scripted restores or a local
console: it bypasses the connectivity guard, so only use it when you can reach
the box another way. Without it the default is unchanged — silence rolls back.
Restoring onto different hardware. If the artifact's roles.conf assigns
roles to ports this machine does not have — a new box whose NICs are named
enp1s0 where the old one had eth0 — the restore says so, names the ports
that are missing and the ones this machine actually has, and requires a second
confirmation typed as different hardware before the ordinary one. Restoring
as-is is legal and sometimes right; it must not be an accident. Pinning roles
by MAC in the roles wizard (m) is what makes an assignment survive a rename
in the first place.
The cards are read-only: every per-area change belongs to the tool a card
hands off to, through that tool's preview-and-confirm. The cockpit itself
carries exactly four mutations — the roles wizard, reboot, poweroff and
restore — and each one shows the literal command line or the file diff before
a confirm, with the wizard's apply and the restore's nftables step additionally
staging their own timed revert first. The one place this binary starts a
process is its backend package — the read-only probes, the handoff exec, and
these previewed commands — which is what the family's exec boundary requires.
The handoff is Bubble Tea's tea.Exec: the cockpit suspends, the child tool
takes over the terminal, and the cockpit resumes and re-reads the machine when
the child exits — because after another tool has run, the screen has to show
the machine, not what it remembered of it.
tui-router probes its backend once at startup and shows the version in the
header. A version nobody has tested is marked (untested) there rather than
hidden; one below the minimum is marked as such and the tool still runs.
| Binary | ip |
| Version read with | ip -V |
| Minimum | 5.0 |
| Tested | none yet |
| Version-gated features | json-output (since 4.13) |
| Versions | What changes |
|---|---|
<4.13 |
ip -j (JSON output) is missing, so the interface and route cards cannot be read from this version |
| Binary | nft |
| Version read with | nft -v |
| Minimum | 0.9 |
| Tested | none yet |
| Binary | wg |
| Version read with | wg --version |
| Minimum | 1.0 |
| Tested | none yet |
The tested versions are generated from compat/results.jsonl, which the tool's
own smoke test appends to when it runs against a real machine in
tui-lab.
Bug reports start with the output of tui-router --report: it names the
versions and the machine, and carries no hostname, user name, home path or
address. See CONTRIBUTING.md.
MIT. See LICENSE.
