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 routeMCP toolRunsNeeds
GET /v1/tfps/statustfps_statustfps_ctl statusthe block map, to report enforcement
GET /v1/tfps/bannedtfps_bannedtfps_ctl bannedthe block map
GET /v1/tfps/labelstfps_labelstfps_ctl logthe database
POST /v1/tfps/bantfps_bantfps_ctl banthe block map and /etc/tfps/config.json
POST /v1/tfps/unbantfps_unbantfps_ctl unbanthe block map
GET /v1/tfps/droppedtfps_droppednothing yeta 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_ctl has to find TFPS’s block map in the kernel by name. That takes CAP_BPF and CAP_SYS_ADMIN. With CAP_BPF alone it fails with no loaded eBPF map called 'blocked'.
  • To ban, tfps_ctl reads /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 lacks CAP_BPF and CAP_SYS_ADMIN.
  • reading /etc/tfps/config.json: Permission denied. Give the file to the sipnab group, as in section 3.
  • /v1/tfps/dropped answers 502. Expected: no TFPS build has dropped yet.
  • installed: false. sipnab cannot find tfps_ctl. Check the path given to --tfps-ctl.