Add TFPS - NormB/sipnab GitHub Wiki
TFPS watches the SIP traffic arriving at a machine. It blocks the sources that are attacking it:
- scanners such as SIPVicious,
- password guessers,
- callers trying to run up international fraud.
It blocks them in the kernel with an XDP program, before the packets reach your SIP server.
- It does not sit in the call path.
- It needs nothing from your SIP server, OpenSIPS or Kamailio. Every step below is the same for both.
- It reads a copy of the traffic, the way a capture tool does.
Install it on the machine that receives SIP from the Internet. That is usually the machine running your SIP server.
- This guide does not use sipnab.
- When TFPS is working, Let sipnab see and control TFPS adds sipnab.
Two terms used below:
- XDP (eXpress Data Path) is a hook in the Linux network driver. There, a
small program can drop a packet before the rest of the kernel sees it.
- TFPS's program drops packets to or from your SIP ports whose source it has blocked.
- It passes everything else, so a blocked address can still reach SSH.
- BTF (BPF Type Format) is a description of the running kernel's data
structures, at
/sys/kernel/btf/vmlinux.- TFPS compiles its XDP program against it during the install.
- So the kernel needs BTF built in.
Tested on
Every block on this page, and every command in the table of the "Operate it" step, ran as written, in order, on 2026-09-28.
- The machines were clean x86_64 virtual machines with 2 cores and 3 GB of
memory:
- Debian 13 (kernel 6.12.63),
- Ubuntu 24.04.5 (kernel 6.8.0).
- A second machine played the attacker.
- TFPS came from a build of master at
984577dc. - On Debian 13, causing each fault under
When something does not work produced what it
describes.
- The exception is a kernel without BTF, which none of the machines had. The installer's source prints that message.
The examples use 192.0.2.20 for the TFPS machine. They use 198.51.100.60 for
a second machine that plays the attacker. Replace both with yours.
Check the kernel
TFPS needs Linux 5.15 or newer, built with BTF:
# Run all of these, in order.
uname -r
ls -l /sys/kernel/btf/vmlinux
The file must exist. Debian 11, Ubuntu 22.04 and later distribution kernels have it.
Install TFPS
Build TFPS from its master branch, at
984577dc.
- The latest release, v0.2.1, has no
--jsonoption. Let sipnab see and control TFPS needs that option. - Master has it, merged on 2026-09-18. No release carries it yet.
- When one does, install that release instead.
The installer compiles the XDP program on this machine.
- It installs
clangandbpftoolfor that. - It does not install the BPF headers the program includes.
- So the first line installs them, with
git:
# 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
Without libbpf-dev the install stops at fatal error: 'bpf/bpf_helpers.h' file not found.
The build does these things:
- downloads a temporary Rust toolchain,
- compiles,
- installs,
- removes the toolchain again.
It needs about 1.5 GB of free memory.
TMPDIR=/var/tmpmatters on Debian 13. There,/tmpis a RAM disk sized to half the memory.- On a 2 GB machine the toolchain does not fit there. The build fails with
No space left on device.
It ends with tfps is running. What it installed:
| Path | What |
|---|---|
/usr/local/bin/tfps |
the daemon |
/usr/local/bin/tfps_ctl |
the command you use to ask it questions and lift blocks |
/usr/local/lib/tfps/tfps_xdp.o |
the XDP program, compiled for this kernel |
/etc/systemd/system/tfps.service |
the unit; the installer replaces it on upgrade |
/etc/tfps/config.json |
settings, written once and never overwritten |
/var/lib/tfps/tfps.db |
what TFPS has learned, and its audit log |
Check that it is enforcing
# Run all of these, in order.
systemctl is-active tfps
sudo journalctl -u tfps -n 30
sudo tfps_ctl status
The journal's startup report names:
- the interface TFPS chose (the one carrying the default route),
- the mode,
PREVENTION, - where the XDP program went, for example
XDP native (DRV) on eth0.
It also lists the addresses it never blocks. Out of the box that is only this machine's own addresses.
Tell it which sources to trust
Some sources should never lose access:
- a carrier that delivers calls to you,
- a monitoring system,
- your own management network.
List them in ignoreip in /etc/tfps/config.json, as single addresses or
ranges. This adds a management range and one carrier to the settings the
installer wrote:
# Run all of these, in order.
sudo tee /etc/tfps/config.json >/dev/null <<'EOF'
{
"ports": [5060],
"intl_prefixes": ["+", "00", "011", "9011"],
"learn_days": 30,
"block_ttl": 3600,
"stats_every": 60,
"ignoreip": ["192.168.10.0/24", "203.0.113.7"]
}
EOF
sudo systemctl restart tfps
sleep 2
sudo journalctl -u tfps -n 30 | grep -A4 ignoreip | tail -5
ports are the SIP ports TFPS watches and blocks on.
- List
5061as well if you take SIP over TLS. - You may have OpenSIPS and Kamailio on one machine, with Kamailio on 5062 as
OpenSIPS and Kamailio on one machine
sets it up. Then list both:
"ports": [5060, 5062]. - TFPS detects attacks only in SIP it can read, which means UDP.
- It cannot read encrypted SIP.
- It does not reassemble SIP over TCP.
- A source it has blocked, though, loses every listed port, TCP and TLS included.
- TFPS's README lists these limits under "What it does not do".
block_ttl is how long a block lasts, in seconds.
Do not list the address of a firewall that forwards SIP to this machine. If the firewall rewrites the source address, fix the forward instead.
- With a plain port forward, TFPS sees each attacker's own address.
- If the firewall replaces it with its own, every attacker looks like the firewall. Trusting the firewall then trusts them all.
Watch it block an attacker
From a second machine, send one SIP request the way a scanner does. Use
SIPVicious's friendly-scanner user agent. Then send a second one. This uses
only Python's standard library:
python3 - <<'EOF'
import socket, time, uuid
TARGET = ("192.0.2.20", 5060)
def options(n):
cid = uuid.uuid4().hex
return (f"OPTIONS sip:100@{TARGET[0]} SIP/2.0\r\n"
f"Via: SIP/2.0/UDP 198.51.100.60:5098;branch=z9hG4bK{cid}\r\n"
"Max-Forwards: 70\r\n"
f"From: <sip:[email protected]>;tag={n}\r\n"
f"To: <sip:100@{TARGET[0]}>\r\n"
f"Call-ID: {cid}\r\nCSeq: 1 OPTIONS\r\n"
"User-Agent: friendly-scanner\r\nContent-Length: 0\r\n\r\n").encode()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(options(1), TARGET)
time.sleep(2)
s.sendto(options(2), TARGET)
EOF
Then, on the TFPS machine:
# Run all of these, in order.
sudo tfps_ctl banned --why
sudo tfps_ctl stats | head -6
sudo journalctl -u tfps -n 5 | grep BLOCKED
The first request got the sender blocked for an hour. The kernel dropped the second before anything else saw it:
SOURCE EXPIRES IN REASON
198.51.100.60 59m user-agent (friendly)
seen on SIP ports : 2
dropped by XDP : 1 (50.0% — gone before sngrep)
The test has to come from a second machine. TFPS never blocks the machine it runs on.
Operate it
tfps_ctl reads and changes what the running daemon holds. Reading or changing
blocks needs root.
| Command | What it does |
|---|---|
sudo tfps_ctl status |
what is running, what TFPS blocks, how fresh the learned state is |
sudo tfps_ctl stats |
every counter: kernel drops, the traffic mix, what got blocked |
sudo tfps_ctl banned --why |
every blocked source, the time left and the reason |
sudo tfps_ctl unban 198.51.100.60 |
lift a block; --all lifts every one |
sudo tfps_ctl ban 192.0.2.10 --ttl 600 |
block a source by hand for 600 seconds; --ttl 0 is forever |
sudo tfps_ctl log --limit 20 |
the audit log of blocks, newest first |
Blocks live in the kernel.
- They last while
tfpsruns. tfps_ctl banandunbanwork only while it is running.
Observe before you enforce. For the first days on a busy machine, you can have TFPS decide without blocking.
- The journal records every decision as
WOULD BLOCKinstead. - Add a drop-in rather than editing the unit. An upgrade replaces the unit.
# Run all of these, in order.
sudo mkdir -p /etc/systemd/system/tfps.service.d
sudo tee /etc/systemd/system/tfps.service.d/observe.conf >/dev/null <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/local/bin/tfps --no-enforce
EOF
sudo systemctl daemon-reload
sudo systemctl restart tfps
In this mode TFPS attaches no XDP program.
- So
tfps_ctl bannedanswersno loaded eBPF map called `blocked` — is tfps running, and are you root?. There is nothing to list. - The journal holds the decisions, as
WOULD BLOCK peer=... (observe only).
To start blocking, remove the drop-in and restart:
# Run all of these, in order.
sudo rm /etc/systemd/system/tfps.service.d/observe.conf
sudo systemctl daemon-reload
sudo systemctl restart tfps
sudo tfps_ctl status
Upgrade by running the installer again with the newer tarball.
- It replaces the binaries, the XDP program and the unit.
- It keeps
/etc/tfps/config.jsonand/var/lib/tfps.
Uninstall. Stopping the service removes the XDP program, so no block outlives it:
# Run all of these, in order.
sudo systemctl disable --now tfps
sudo rm -f /etc/systemd/system/tfps.service /usr/local/bin/tfps /usr/local/bin/tfps_ctl
sudo rm -rf /usr/local/lib/tfps
sudo systemctl daemon-reload
/var/lib/tfps and /etc/tfps stay behind, so that a reinstall keeps what TFPS
learned. Remove them too if you no longer want it:
sudo rm -rf /var/lib/tfps /etc/tfps
When something does not work
fatal error: 'bpf/bpf_helpers.h' file not found. Installlibbpf-devand run the installer again.no /sys/kernel/btf/vmlinux. The kernel has no BTF or is older than 5.15. Use a distribution kernel.- Nothing is ever blocked.
- Check the startup report's
ignoreiplist and where your SIP arrives from. - A firewall may forward SIP with its own address as the source. Then TFPS
sees one source, and
ignoreipmay already trust it.
- Check the startup report's
- You blocked yourself. A block covers only the SIP ports, so SSH still
works:
sudo tfps_ctl unban <your address>.