Docs Let sipnab see and control TFPS
Let sipnab see and control TFPS
Have sipnab report what TFPS blocks and ban or unban on request, over REST and MCP, with TFPS on the same machine or reached over SSH.
On this page
TFPS blocks attacking SIP sources in the kernel. sipnab can ask it what it is blocking and why, and can block or unblock an address on your behalf, through its REST API and its MCP tools. sipnab never blocks anything on its own: every ban through sipnab is one you ask for.
This guide starts where Add TFPS to an OpenSIPS voice stack ends, with TFPS installed and enforcing. It covers sipnab on the same machine as TFPS, and sipnab on a different machine.
What sipnab asks TFPS
sipnab runs tfps_ctl, TFPS’s own command, and reads its answers:
| sipnab route | MCP tool | Runs | Needs |
|---|---|---|---|
GET /v1/tfps/status | tfps_status | tfps_ctl status | the block map, to report enforcement |
GET /v1/tfps/banned | tfps_banned | tfps_ctl banned | the block map |
GET /v1/tfps/labels | tfps_labels | tfps_ctl log | the database |
POST /v1/tfps/ban | tfps_ban | tfps_ctl ban | the block map and /etc/tfps/config.json |
POST /v1/tfps/unban | tfps_unban | tfps_ctl unban | the block map |
GET /v1/tfps/dropped | tfps_dropped | nothing yet | a TFPS release that reports its kernel drops; none does yet, so this answers 502 |
The REST API reference gives every request and response.
Tested on
Every command on this page ran as written, in order, on 2026-09-25 and
2026-09-26, on x86_64 virtual machines. The same-machine setup ran on Ubuntu
24.04.5. The different-machine setup ran with sipnab on Debian 13, reaching
TFPS on the Ubuntu one over SSH. Both used sipnab’s .deb.
1. Build TFPS from master
sipnab reads tfps_ctl’s answers as JSON, asking with --json. That option is
on TFPS’s master branch, merged 2026-09-18 as
984577dc, and is in no
release yet: v0.2.1 answers unknown option. Until a release carries it, build
master. The installer builds from a checkout and upgrades the release in place,
keeping /etc/tfps/config.json and what TFPS has learned:
# Run all of these, in order.
sudo apt-get install -y git libbpf-dev
sudo git clone https://github.com/sippulse/tfps.git /usr/local/src/tfps
sudo git -C /usr/local/src/tfps checkout 984577dc
cd /usr/local/src/tfps
sudo TMPDIR=/var/tmp sh packaging/install.sh
sudo tfps_ctl status --json
The build downloads a temporary Rust toolchain, compiles, installs and removes the toolchain again. It needs about 1.5 GB of free memory.
TMPDIR=/var/tmp matters on Debian 13, where /tmp is a RAM disk sized to half
the memory: on a 2 GB machine the toolchain does not fit there and the build
fails with No space left on device.
The last command prints one line of JSON, starting
{"enforcement":"active",.... This build still calls itself 0.2.1 in that
line, because master has not changed its version number since the release.
2. Install sipnab
# Run all of these, in order.
V=$(curl -fsSL https://api.github.com/repos/NormB/sipnab/releases/latest \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"].lstrip("v"))')
curl -fsSLO https://github.com/NormB/sipnab/releases/download/v$V/sipnab_${V}_amd64.deb
curl -fsSL https://github.com/NormB/sipnab/releases/download/v$V/SHA256SUMS.txt \
| grep " sipnab_${V}_amd64.deb$" | sha256sum -c -
sudo apt-get install -y ./sipnab_${V}_amd64.deb
The package runs sipnab as the sipnab user, capturing on every interface.
3. sipnab on the same machine as TFPS
Turn on the REST API. Keep the key out of the command line, where every user on the machine could read it, by putting it in a file only root can read. systemd reads it before it starts sipnab:
# Run all of these, in order.
sudo sh -c 'umask 077; printf "SIPNAB_API_KEY=%s\n" "$(openssl rand -hex 32)" > /etc/sipnab/api.env'
sudo mkdir -p /etc/systemd/system/sipnab.service.d
sudo tee /etc/systemd/system/sipnab.service.d/tfps.conf >/dev/null <<'EOF'
[Service]
EnvironmentFile=/etc/sipnab/api.env
ExecStart=
ExecStart=/usr/bin/sipnab -N -d any --no-cli-print --syslog --metrics 127.0.0.1:9090 \
--api 127.0.0.1:8080 --tfps-ctl /usr/local/bin/tfps_ctl
EOF
The API listens on 127.0.0.1 only. Put a TLS proxy in front of it before you
open it to the network.
Following Send sipnab’s vCons to a vCon server as well? Both
guides replace sipnab’s ExecStart in a drop-in, and systemd uses the last
ExecStart= it reads, taking drop-ins in file-name order. So one silently
replaces the other. Put the flags from both into one drop-in instead.
Give sipnab what tfps_ctl needs. sipnab runs without root. The tfps_ctl
it starts inherits sipnab’s privileges, and TFPS’s commands need more than a
capture does:
- To read or change blocks,
tfps_ctlhas to find TFPS’s block map in the kernel by name. That takesCAP_BPFandCAP_SYS_ADMIN. WithCAP_BPFalone it fails withno loaded eBPF map called 'blocked'. - To ban,
tfps_ctlreads/etc/tfps/config.json, to refuse addresses you told TFPS to trust. The file is readable by root only.
# Run all of these, in order.
sudo tee /etc/systemd/system/sipnab.service.d/tfps-caps.conf >/dev/null <<'EOF'
[Service]
AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN CAP_BPF CAP_SYS_ADMIN
CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_BPF CAP_SYS_ADMIN
EOF
sudo chgrp sipnab /etc/tfps/config.json
sudo chmod 640 /etc/tfps/config.json
sudo systemctl daemon-reload
sudo systemctl restart sipnab
Know what you are granting. CAP_SYS_ADMIN is a broad capability, and sipnab
parses traffic from the Internet. If you only want to read what TFPS decided,
skip the capabilities: labels works without them, and status still answers,
reporting enforcement as inactive because it cannot see the map. The
different-machine setup below grants sipnab
nothing, and works on one machine too.
The installer never rewrites /etc/tfps/config.json, so the group you gave it
survives TFPS upgrades. Anything secret in that file, such as an APIBAN key, is
now readable by sipnab.
Try it.
# Run all of these, in order.
KEY=$(sudo sed -n 's/^SIPNAB_API_KEY=//p' /etc/sipnab/api.env)
curl -s -w '\n' -H "Authorization: Bearer $KEY" 127.0.0.1:8080/v1/tfps/status
curl -s -w '\n' -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"ip":"198.51.100.20","ttl_secs":600}' 127.0.0.1:8080/v1/tfps/ban
curl -s -w '\n' -H "Authorization: Bearer $KEY" 127.0.0.1:8080/v1/tfps/banned
curl -s -w '\n' -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"ip":"198.51.100.20"}' 127.0.0.1:8080/v1/tfps/unban
The status reports "enforcement":"active", the ban answers "applied":true,
banned lists 198.51.100.20 with its expiry, and the unban answers
"applied":true. TFPS refuses to ban its own machine: the answer is
"applied":false with "refused":"local".
4. sipnab on a different machine
sipnab starts tfps_ctl as a local program, and does not care whether that
program is TFPS’s or a script. On the sipnab machine, a two-line script that
runs tfps_ctl on the TFPS machine over SSH takes its place. On the TFPS
machine, a dedicated account’s key can run tfps_ctl and nothing else.
This way sipnab needs no extra privileges at all. The one privileged thing, a
sudo rule for tfps_ctl, stays on the TFPS machine.
On the sipnab machine, make a key for the connection:
# Run all of these, in order.
sudo install -d -o sipnab -g sipnab -m 0700 /etc/sipnab/tfps
sudo -u sipnab ssh-keygen -q -t ed25519 -N '' -C sipnab-tfps -f /etc/sipnab/tfps/key
sudo cat /etc/sipnab/tfps/key.pub
On the TFPS machine, create the account, a forced command that passes the
arguments to tfps_ctl and nothing else, and a sudo rule for tfps_ctl
alone. Paste the public key from the last step where it says PUBLIC-KEY:
# Run all of these, in order.
sudo useradd --system --create-home --home-dir /var/lib/sipnabctl --shell /bin/sh sipnabctl
sudo install -d -m 0755 /usr/local/lib/tfps-remote
sudo tee /usr/local/lib/tfps-remote/serve >/dev/null <<'EOF'
#!/bin/sh
# Forced command for sipnab's key: run tfps_ctl with the arguments sipnab
# sent, and nothing else. set -f stops the shell expanding * or ? in them;
# the arguments are split on spaces and never evaluated.
set -f
exec sudo -n /usr/local/bin/tfps_ctl $SSH_ORIGINAL_COMMAND
EOF
sudo chmod 0755 /usr/local/lib/tfps-remote/serve
echo 'sipnabctl ALL=(root) NOPASSWD: /usr/local/bin/tfps_ctl' | sudo tee /etc/sudoers.d/sipnabctl >/dev/null
sudo chmod 0440 /etc/sudoers.d/sipnabctl
sudo visudo -cf /etc/sudoers.d/sipnabctl
sudo install -d -o sipnabctl -g sipnabctl -m 0700 /var/lib/sipnabctl/.ssh
echo 'restrict,command="/usr/local/lib/tfps-remote/serve" PUBLIC-KEY' \
| sudo tee /var/lib/sipnabctl/.ssh/authorized_keys >/dev/null
sudo chown sipnabctl: /var/lib/sipnabctl/.ssh/authorized_keys
sudo chmod 0600 /var/lib/sipnabctl/.ssh/authorized_keys
restrict turns off port forwarding, agent forwarding and terminals for the
key, and command= means the key runs serve whatever the other side asks for.
An attempt to slip in a second command, such as status;id, reaches tfps_ctl
as one unknown argument, and tfps_ctl refuses it.
Back on the sipnab machine, write the script that stands in for tfps_ctl,
record the TFPS machine’s host key, and try it. Replace 192.0.2.20 with the
TFPS machine’s address:
# Run all of these, in order.
sudo tee /etc/sipnab/tfps/tfps_ctl >/dev/null <<'EOF'
#!/bin/sh
exec ssh -o BatchMode=yes -o UserKnownHostsFile=/etc/sipnab/tfps/known_hosts \
-i /etc/sipnab/tfps/key sipnabctl@192.0.2.20 -- "$@"
EOF
sudo chmod 0755 /etc/sipnab/tfps/tfps_ctl
ssh-keyscan -t ed25519 192.0.2.20 | sudo -u sipnab tee /etc/sipnab/tfps/known_hosts >/dev/null
sudo -u sipnab /etc/sipnab/tfps/tfps_ctl status --json
The last line prints TFPS’s status as JSON. Check the host key that
ssh-keyscan recorded against the TFPS machine’s own
(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub there) before you rely on it.
Then point sipnab at the script. Use the API key file from section 3, and not the capability drop-in:
# Run all of these, in order.
sudo sh -c 'umask 077; printf "SIPNAB_API_KEY=%s\n" "$(openssl rand -hex 32)" > /etc/sipnab/api.env'
sudo mkdir -p /etc/systemd/system/sipnab.service.d
sudo tee /etc/systemd/system/sipnab.service.d/tfps.conf >/dev/null <<'EOF'
[Service]
EnvironmentFile=/etc/sipnab/api.env
ReadWritePaths=/etc/sipnab/tfps
ExecStart=
ExecStart=/usr/bin/sipnab -N -d any --no-cli-print --syslog --metrics 127.0.0.1:9090 \
--api 127.0.0.1:8080 --tfps-ctl /etc/sipnab/tfps/tfps_ctl
EOF
sudo systemctl daemon-reload
sudo systemctl restart sipnab
The curl commands from section 3 work unchanged.
When something does not work
unknown option: --json. The TFPS in use is a release. Build master, as in step 1.no loaded eBPF map called 'blocked' — is tfps running, and are you root?Either TFPS is not running, or sipnab lacksCAP_BPFandCAP_SYS_ADMIN.reading /etc/tfps/config.json: Permission denied. Give the file to thesipnabgroup, as in section 3./v1/tfps/droppedanswers502. Expected: no TFPS build hasdroppedyet.installed: false. sipnab cannot findtfps_ctl. Check the path given to--tfps-ctl.