Docs Ban SIP scanners with fail2ban

Ban SIP scanners with fail2ban

Have OpenSIPS log scanners and wrong passwords, and fail2ban ban them with nftables, while a phone keeps registering and calling. No sipnab involved.

On this page

fail2ban reads your servers’ logs and bans the addresses that show up in them for the wrong reasons, by adding them to a firewall rule. Make your SIP server write one log line for each scanner request and each wrong password, and fail2ban bans SIP scanners and password guessers. It is the firewall alternative to TFPS: TFPS reads the SIP traffic itself and drops attackers with an XDP program, while fail2ban acts on what your SIP server logged, and bans with an ordinary nftables rule.

This guide sets up OpenSIPS as a registrar that logs scanners and failed passwords, has fail2ban ban on those lines, and proves it from this machine with a scanner, a password guesser and a phone that keeps working. This guide does not use sipnab. When fail2ban is working, Feed fail2ban from sipnab adds sipnab as a second source of bans.

Three terms used below:

  • Filter: the patterns that pick the failure lines out of a log, and the address in each.
  • Jail: one filter put to work: which log it reads, how many failures within how long earn a ban (maxretry, findtime), how long a ban lasts (bantime), and what a ban blocks.
  • Ban action: what fail2ban runs to ban and unban. Here it adds the address to an nftables set, and a rule beside the set refuses that set’s SIP traffic.

Tested on

Every block on this page, and every command in step 9’s table, ran as written, in order, on 2026-10-01, on clean x86_64 virtual machines with 2 cores and 3 GB of memory, Debian 13 (kernel 6.12.111) and Ubuntu 24.04 (kernel 6.8.0), with the scanner and the phone on the same machine as step 4 sets them up. On Debian 13, each fault under When something does not work happened during these runs, with the message it quotes.

SoftwareVersion
fail2banthe distribution’s: 1.1.0 on Debian 13, 1.0.2 on Ubuntu 24.04
OpenSIPS4.0.2, from the OpenSIPS project’s packages
SIPp (for the test call)the distribution’s sip-tester

The examples use 192.0.2.10 as the SIP server’s address. Replace it with yours everywhere it appears. The scanner (198.51.100.60) and the phone (203.0.113.70) are test addresses that step 4 creates on this machine.

Before you start: OpenSIPS

Run step 1 of Use OpenSIPS as your voice stack’s SIP server, installing the 4.0 packages, then come back here. Step 1 below replaces that guide’s configuration with one that authenticates phones and logs what fail2ban needs.

1. Make OpenSIPS log scanners and wrong passwords

Password checks need OpenSIPS’s auth module, which is in its own package. The configuration below registers one test account, 1001, routes its calls to a test callee on this machine, and writes a log line for two things:

  • a request whose User-Agent names a known scanner tool. OpenSIPS answers it with nothing.
  • a request whose credentials are wrong: a wrong password, or a user that does not exist. A request that carries no credentials yet is the normal first half of a login, and is not logged.
# Run all of these, in order.
sudo apt-get install -y opensips-auth-modules
sudo tee /etc/opensips/opensips.cfg >/dev/null <<'EOF'
# OpenSIPS as a registrar that logs what fail2ban bans on.
log_level=3
stderror_enabled=no
syslog_enabled=yes
syslog_facility=LOG_LOCAL0
udp_workers=2

socket=udp:192.0.2.10:5060

mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"

loadmodule "proto_udp.so"
loadmodule "signaling.so"
loadmodule "sl.so"
loadmodule "tm.so"
loadmodule "rr.so"
loadmodule "maxfwd.so"
loadmodule "sipmsgops.so"
loadmodule "usrloc.so"
modparam("usrloc", "working_mode_preset", "single-instance-no-db")
loadmodule "registrar.so"
loadmodule "auth.so"
modparam("auth", "username_spec", "$var(username)")
modparam("auth", "password_spec", "$var(password)")
modparam("auth", "calculate_ha1", 1)

loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/run/opensips/opensips_fifo")

route {
	if (!mf_process_maxfwd_header(10)) {
		send_reply(483, "Too Many Hops");
		exit;
	}

	# Scanner tools that name themselves: log, and answer nothing.
	if ($ua =~ "friendly-scanner|sipvicious|sipcli|VaxSIPUserAgent") {
		xlog("L_WARN", "SIP scanner from $si: User-Agent \"$ua\"\n");
		exit;
	}

	if (has_totag()) {
		if (is_method("ACK") && t_check_trans()) {
			t_relay();
			exit;
		}
		if (!loose_route()) {
			send_reply(404, "Not here");
			exit;
		}
		t_relay();
		exit;
	}
	if (is_method("CANCEL")) {
		if (t_check_trans())
			t_relay();
		exit;
	}
	t_check_trans();

	if (is_method("OPTIONS")) {
		send_reply(200, "OK");
		exit;
	}

	# The one test account. Your registrar reads its accounts from a database.
	$var(username) = "1001";
	$var(password) = "correct-horse-1001";

	if (is_method("REGISTER")) {
		if (!pv_www_authorize("")) {
			$var(rc) = $rc;
			# -1: no such user, -2: wrong password
			if ($var(rc) == -1 || $var(rc) == -2)
				xlog("L_WARN", "SIP auth failure from $si for \"$au\"\n");
			www_challenge("", "auth");
			exit;
		}
		consume_credentials();
		if (!save("location"))
			sl_reply_error();
		exit;
	}

	if (is_method("INVITE")) {
		if (!pv_proxy_authorize("")) {
			$var(rc) = $rc;
			if ($var(rc) == -1 || $var(rc) == -2)
				xlog("L_WARN", "SIP auth failure from $si for \"$au\"\n");
			proxy_challenge("", "auth");
			exit;
		}
		consume_credentials();
		record_route();
		# Where the call goes. Here, a test callee on this machine.
		$du = "sip:127.0.0.1:5070";
		t_relay();
		exit;
	}

	send_reply(405, "Method Not Allowed");
}
EOF
sudo opensips -C -f /etc/opensips/opensips.cfg
sudo systemctl restart opensips
systemctl is-active opensips

OpenSIPS writes these lines to the systemd journal, where they look like this:

... /usr/sbin/opensips[15830]: WARNING:SIP scanner from 198.51.100.60: User-Agent "friendly-scanner"
... /usr/sbin/opensips[15830]: WARNING:SIP auth failure from 198.51.100.60 for "1001"

2. Install fail2ban

# Run all of these, in order.
sudo apt-get install -y fail2ban nftables
fail2ban-client --version

The jail below bans with nftables, so the command installs it too.

3. Tell fail2ban what to look for, and what to ban

The filter reads OpenSIPS’s lines from the journal. The jail bans an address after 3 matching lines within 10 minutes, for an hour, on the SIP ports only: its rule names ports 5060 and 5061, so a banned address can still reach your other services, SSH included:

# Run all of these, in order.
sudo tee /etc/fail2ban/filter.d/opensips.conf >/dev/null <<'EOF'
[INCLUDES]
before = common.conf

[Definition]
_daemon = \S*opensips
failregex = ^%(__prefix_line)sWARNING:SIP scanner from <HOST>: User-Agent
            ^%(__prefix_line)sWARNING:SIP auth failure from <HOST> for
ignoreregex =
journalmatch = _SYSTEMD_UNIT=opensips.service
EOF
sudo tee /etc/fail2ban/jail.d/opensips.local >/dev/null <<'EOF'
[opensips]
enabled   = true
filter    = opensips
backend   = systemd
banaction = nftables[type=multiport]
port      = 5060,5061
protocol  = udp,tcp
maxretry  = 3
findtime  = 10m
bantime   = 1h
EOF
sudo systemctl restart fail2ban
sleep 3
sudo fail2ban-client status opensips

The status shows the jail with Journal matches: _SYSTEMD_UNIT=opensips.service and nobody banned yet. When it starts, the jail also reads the last findtime of the journal, so failures logged before it ran can earn a ban.

_daemon matches the program name the journal records, /usr/sbin/opensips for the packages.

4. Give this machine a scanner and a phone to test with

Each test address lives in a network namespace, a separate network stack on this machine with its own address, linked to this one by a virtual cable. Its requests reach OpenSIPS from that address, as if from another machine:

# Run all of these, in order.
for n in scanner:198.51.100 phone:203.0.113; do
  ns=${n%%:*} net=${n#*:}
  sudo ip netns add $ns
  sudo ip link add f2b-$ns type veth peer name eth0 netns $ns
  sudo ip addr add $net.1/24 dev f2b-$ns
  sudo ip link set f2b-$ns up
  sudo ip -n $ns link set lo up
  sudo ip -n $ns link set eth0 up
done
sudo ip -n scanner addr add 198.51.100.60/24 dev eth0
sudo ip -n scanner route add default via 198.51.100.1
sudo ip -n phone addr add 203.0.113.70/24 dev eth0
sudo ip -n phone route add default via 203.0.113.1

A small SIP client sends the test requests and prints each answer. It uses only Python’s standard library:

# Run all of these, in order.
mkdir -p ~/f2b-test
cat > ~/f2b-test/sipreq.py <<'EOF'
"""Send SIP requests and print each final answer.
usage: sipreq.py SERVER METHOD COUNT [USER-AGENT] [USER PASSWORD]"""
import hashlib, re, socket, sys, uuid

srv, method, count = sys.argv[1], sys.argv[2], int(sys.argv[3])
ua = sys.argv[4] if len(sys.argv) > 4 else "sipreq"
user, pw = (sys.argv[5], sys.argv[6]) if len(sys.argv) > 6 else (None, None)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
s.connect((srv, 5060))
me, port = s.getsockname()


def request(n, ext, auth=""):
    return (f"{method} sip:{srv} SIP/2.0\r\n"
            f"Via: SIP/2.0/UDP {me}:{port};branch=z9hG4bK{uuid.uuid4().hex}\r\n"
            f"Max-Forwards: 70\r\nFrom: <sip:{ext}@{srv}>;tag={n}\r\n"
            f"To: <sip:{ext}@{srv}>\r\nCall-ID: {cid}\r\nCSeq: {n} {method}\r\n"
            f"Contact: <sip:{ext}@{me}:{port}>\r\nExpires: 300\r\n"
            f"User-Agent: {ua}\r\n{auth}Content-Length: 0\r\n\r\n").encode()


def answer(data):
    s.send(data)
    while True:
        try:
            reply = s.recv(65535).decode(errors="replace")
        except socket.timeout:
            return "no answer"
        except ConnectionRefusedError:
            return "refused"
        if int(reply.split(" ", 2)[1]) >= 200:
            return reply


for i in range(count):
    cid = uuid.uuid4().hex
    ext = user or str(100 + i)
    reply = answer(request(1, ext))
    if reply.startswith("SIP/2.0 401") and user:
        c = dict(re.findall(r'(\w+)="([^"]*)"', reply.split("WWW-Authenticate:", 1)[1].split("\r\n", 1)[0]))
        ha1 = hashlib.md5(f"{user}:{c['realm']}:{pw}".encode()).hexdigest()
        ha2 = hashlib.md5(f"{method}:sip:{srv}".encode()).hexdigest()
        resp = hashlib.md5(f"{ha1}:{c['nonce']}:00000001:0a4f113b:auth:{ha2}".encode()).hexdigest()
        auth = (f'Authorization: Digest username="{user}", realm="{c["realm"]}", '
                f'nonce="{c["nonce"]}", uri="sip:{srv}", response="{resp}", '
                f'qop=auth, nc=00000001, cnonce="0a4f113b", algorithm=MD5\r\n')
        reply = answer(request(2, ext, auth))
    print(f"{method} {ext}: " + reply.split("\r\n", 1)[0])
EOF

5. Check that a phone registers and calls

The phone at 203.0.113.70 registers as 1001 with the right password:

sudo ip netns exec phone python3 ~/f2b-test/sipreq.py 192.0.2.10 REGISTER 1 MicroSIP 1001 correct-horse-1001

It prints REGISTER 1001: SIP/2.0 200 OK.

Then it places a call through OpenSIPS, with SIPp playing both ends: a callee on this machine, and the phone, which answers OpenSIPS’s challenge with its password:

# Run all of these, in order.
sudo apt-get install -y sip-tester
cd ~/f2b-test
cat > call.xml <<'EOF'
<?xml version="1.0" encoding="ISO-8859-1" ?>
<scenario name="Call with digest authentication">
  <send retrans="500">
    <![CDATA[
      INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
      From: <sip:1001@[remote_ip]>;tag=[pid]SIPpTag00[call_number]
      To: <sip:[service]@[remote_ip]:[remote_port]>
      Call-ID: [call_id]
      CSeq: 1 INVITE
      Contact: <sip:1001@[local_ip]:[local_port]>
      Max-Forwards: 70
      Content-Type: application/sdp
      Content-Length: [len]

      v=0
      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
      s=-
      c=IN IP[media_ip_type] [media_ip]
      t=0 0
      m=audio [media_port] RTP/AVP 0
      a=rtpmap:0 PCMU/8000
    ]]>
  </send>
  <recv response="100" optional="true"/>
  <recv response="407" auth="true"/>
  <send>
    <![CDATA[
      ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
      From: <sip:1001@[remote_ip]>;tag=[pid]SIPpTag00[call_number]
      To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
      Call-ID: [call_id]
      CSeq: 1 ACK
      Max-Forwards: 70
      Content-Length: 0
    ]]>
  </send>
  <send retrans="500">
    <![CDATA[
      INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
      From: <sip:1001@[remote_ip]>;tag=[pid]SIPpTag00[call_number]
      To: <sip:[service]@[remote_ip]:[remote_port]>
      Call-ID: [call_id]
      CSeq: 2 INVITE
      Contact: <sip:1001@[local_ip]:[local_port]>
      [authentication]
      Max-Forwards: 70
      Content-Type: application/sdp
      Content-Length: [len]

      v=0
      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
      s=-
      c=IN IP[media_ip_type] [media_ip]
      t=0 0
      m=audio [media_port] RTP/AVP 0
      a=rtpmap:0 PCMU/8000
    ]]>
  </send>
  <recv response="100" optional="true"/>
  <recv response="180" optional="true"/>
  <recv response="200" rtd="true" rrs="true"/>
  <send>
    <![CDATA[
      ACK [next_url] SIP/2.0
      [routes]
      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
      From: <sip:1001@[remote_ip]>;tag=[pid]SIPpTag00[call_number]
      To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
      Call-ID: [call_id]
      CSeq: 2 ACK
      Max-Forwards: 70
      Content-Length: 0
    ]]>
  </send>
  <pause milliseconds="2000"/>
  <send retrans="500">
    <![CDATA[
      BYE [next_url] SIP/2.0
      [routes]
      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
      From: <sip:1001@[remote_ip]>;tag=[pid]SIPpTag00[call_number]
      To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
      Call-ID: [call_id]
      CSeq: 3 BYE
      Max-Forwards: 70
      Content-Length: 0
    ]]>
  </send>
  <recv response="200" crlf="true"/>
</scenario>
EOF
sipp -sn uas -i 127.0.0.1 -p 5070 -m 1 -bg
sudo ip netns exec phone sipp -sf call.xml 192.0.2.10:5060 -i 203.0.113.70 -p 5080 -s 2000 \
  -au 1001 -ap correct-horse-1001 -m 1 -timeout 20s -trace_screen -screen_file call.screen >/dev/null
grep -E 'Successful call|Failed call' call.screen

OpenSIPS challenged the call, the phone answered the challenge, the callee answered, and the phone hung up:

  Successful call        |        0                  |        1
  Failed call            |        0                  |        0

The phone’s first INVITE drew a challenge, which is the normal first half of a login, so OpenSIPS logged nothing for it.

6. Watch fail2ban ban a scanner

The scanner walks extensions the way SIPVicious’s svwar does, announcing itself as friendly-scanner. Then it tries once more:

# Run all of these, in order.
sudo ip netns exec scanner python3 ~/f2b-test/sipreq.py 192.0.2.10 REGISTER 5 friendly-scanner
sleep 2
sudo fail2ban-client status opensips
sudo nft list table inet f2b-table
sudo ip netns exec scanner python3 ~/f2b-test/sipreq.py 192.0.2.10 OPTIONS 1

OpenSIPS answered none of the scanner’s REGISTERs: each prints no answer, or refused if the ban was already in place. The jail lists the scanner:

   `- Banned IP list:	198.51.100.60

nftables holds it in the jail’s set, and refuses its SIP traffic:

	set addr-set-opensips {
		type ipv4_addr
		elements = { 198.51.100.60 }
	}
	...
		udp dport { 5060, 5061 } ip saddr @addr-set-opensips reject with icmp port-unreachable

The last request prints OPTIONS 100: refused: the firewall answered with an ICMP port unreachable, and OpenSIPS never saw it.

The phone is not affected. It still registers:

sudo ip netns exec phone python3 ~/f2b-test/sipreq.py 192.0.2.10 REGISTER 1 MicroSIP 1001 correct-horse-1001

7. Lift a ban

# Run all of these, in order.
sudo fail2ban-client set opensips unbanip 198.51.100.60
sudo ip netns exec scanner python3 ~/f2b-test/sipreq.py 192.0.2.10 OPTIONS 1

unbanip prints 1, the number of addresses it unbanned, and the scanner’s OPTIONS gets an answer again: OPTIONS 100: SIP/2.0 200 OK.

8. Watch a password guesser get banned, and the ban run out

A password guesser uses an ordinary User-Agent and a real user name. To see a ban end on its own without waiting an hour, shorten this jail’s bantime to 60 seconds while it runs, then put it back. fail2ban-client set changes the running jail only, and the jail file still says an hour:

# Run all of these, in order.
sudo fail2ban-client set opensips bantime 60
sudo ip netns exec scanner python3 ~/f2b-test/sipreq.py 192.0.2.10 REGISTER 4 MicroSIP 1001 wrong-password
sleep 2
sudo fail2ban-client get opensips banip --with-time
sleep 70
sudo fail2ban-client status opensips
sudo ip netns exec scanner python3 ~/f2b-test/sipreq.py 192.0.2.10 OPTIONS 1
sudo fail2ban-client set opensips bantime 3600

Each guess prints REGISTER 1001: SIP/2.0 401 Unauthorized. A guess or two past the third can still get an answer, because fail2ban acts on the log line after OpenSIPS has written it. banip --with-time shows when the ban started and ends, 60 seconds later:

198.51.100.60 	2026-10-01 03:47:03 + 60 = 2026-10-01 03:48:03

The ban starts from the time of the failure that earned it. After the wait, Banned IP list: is empty and the OPTIONS gets an answer again.

9. Operate it

The commands below, in this order, ban a test address by hand, look at it, restart fail2ban, and lift the ban:

CommandWhat it does
sudo fail2ban-client set opensips banip 198.51.100.61ban an address by hand, for the jail’s bantime, and print how many it banned
sudo fail2ban-client status opensipsthe jail’s failures and its banned addresses
sudo fail2ban-client get opensips banip --with-timeeach banned address, when its ban started and when it ends
sudo systemctl restart fail2banapply a change to the filter or the jail. Bans survive it, because fail2ban keeps them in its database and restores them
sudo fail2ban-client set opensips unbanip 198.51.100.61lift a ban
sudo fail2ban-regex systemd-journal opensipscount the journal lines the filter matches, without banning anything
sudo journalctl -u fail2ban -n 20the fail2ban service’s messages, including a jail that failed to start
sudo tail -n 20 /var/log/fail2ban.logfail2ban’s own log: every ban and unban, by jail and address

Remove the test addresses when you finish testing:

# Run all of these, in order.
sudo ip netns del scanner
sudo ip netns del phone
rm -rf ~/f2b-test

Uninstall. Stopping fail2ban removes its nftables table, so no ban outlives it:

# Run all of these, in order.
sudo systemctl disable --now fail2ban
sudo rm -f /etc/fail2ban/filter.d/opensips.conf /etc/fail2ban/jail.d/opensips.local
sudo apt-get purge -y fail2ban

This keeps the OpenSIPS configuration from step 1. To uninstall OpenSIPS too, see its guide.

When something does not work

  • The jail never bans. Check what the filter matches: sudo fail2ban-regex systemd-journal opensips reports Failregex: 0 total when it matches nothing. If OpenSIPS runs under another unit or another program name, journalmatch or _daemon does not fit it: see how the journal records the program with sudo journalctl -u opensips -n 5.
  • fail2ban does not start. sudo journalctl -u fail2ban -n 20 names the jail and the reason. One jail that fails stops fail2ban, and every jail with it, SSH’s included.