Skip to content

GeoIP SIP Firewall Runbook

Operational reference for the India-only GeoIP filter that protects the SIP ports on the cloud Asterisk servers. This is the perimeter layer — it drops non-India source IPs at the kernel before Asterisk or fail2ban ever see the packet. fail2ban is the second layer, for abuse that originates from inside India (see Fail2Ban Runbook).

Both environments run an identical stack as of 2026-05-31:

Env Host SIP ports WireGuard allow-ifaces
Prod 147.93.168.216 5060, 5080 (UDP+TCP) wg1 + src 10.10.10.0/24
Staging 94.136.188.221 5060, 5080 (UDP+TCP) wg0 (prod/NUC tunnel) + wg1 (customer tunnels, 10.20.0.0/16)

The only per-host difference is which WireGuard interface(s) carry trusted SIP — prod relays the Tata trunk over wg1; staging receives the prod/NUC tunnel on wg0 and customer tunnels on wg1. The allow-rules are tailored per host so tunnel traffic is never GeoIP-filtered; everything else flows through the same india_sip set + DROP.

If the india_sip ipset is empty, ALL SIP is dropped

The firewall is allow-list by membership: the DROP rule fires for any SIP packet whose source isn't in the india_sip set (or the WireGuard range). An empty or missing set means every customer phone is dropped → total SIP outage. After any reboot, ipset restore, or maintenance, verify the set has ~7,000+ entries (see Verify).

How it works

Three moving parts:

Part What it is Where
india_sip ipset hash:net set of ~7,100 Indian CIDR blocks kernel; saved to /etc/ipset.conf
iptables INPUT rules allow WireGuard + allow india_sip + DROP the rest, on UDP and TCP ports 5060/5080 kernel; saved to /etc/iptables/rules.v4
Monthly refresh update-india-geoip.sh re-downloads the CIDR list and swaps the set cron 0 3 1 * *

The iptables rule chain (INPUT, evaluated top-down)

These rules are inserted ahead of the ufw chains so they decide SIP traffic first:

1. ACCEPT  udp  in=wg1                          dports 5060,5080         # WireGuard iface
2. ACCEPT  udp  src 10.10.10.0/24               dports 5060,5080         # GeoIP: Allow WireGuard SIP
3. ACCEPT  udp  match-set india_sip src         dports 5060,5080         # GeoIP: Allow India SIP
4. DROP    udp                                  dports 5060,5080         # GeoIP: Drop non-India SIP
5. ACCEPT  tcp  src 10.10.10.0/24               dports 5060,5080         # (same four rules, TCP)
6. ACCEPT  tcp  match-set india_sip src         dports 5060,5080
7. DROP    tcp                                  dports 5060,5080

The WireGuard allows (rules 1–2) come first so NUC-relayed Tata trunk traffic (10.10.10.0/24) is never subject to GeoIP — the NUC's public egress IP is irrelevant because the packets arrive over the tunnel.

CIDR data source

update-india-geoip.sh downloads the aggregated India zone from ipdeny.com:

https://www.ipdeny.com/ipblocks/data/aggregated/in-aggregated.zone

~7,100 aggregated IN CIDR blocks. The script aborts if the download returns fewer than 5,000 lines (guards against a truncated/empty download wiping the set).

Boot persistence

Two units restore state on reboot, in order:

  1. ipset-restore.service (oneshot, Before=netfilter-persistent.service) — runs ipset restore -f /etc/ipset.conf so the set exists before the iptables rules that reference it load. On stop it ipset saves back.
  2. netfilter-persistent — loads /etc/iptables/rules.v4 (the match-set rules).

Order matters: if iptables loaded a --match-set india_sip rule before the set existed, the restore would fail. ipset-restore.service running first prevents that.

Verify the filter is healthy

ssh root@147.93.168.216

# 1. Set exists and is populated (expect ~7000+)
ipset list india_sip -t            # header only: 'Number of entries: 7103'

# 2. Rules are present and matching traffic
iptables -L INPUT -v -n --line-numbers | grep -A1 -i "india\|GeoIP"
#   Look at the pkts counter on the ACCEPT match-set line (India traffic flowing)
#   and the DROP line (attackers being blocked) — both should be non-zero.

# 3. Boot units are enabled
systemctl is-enabled ipset-restore.service netfilter-persistent

# 4. Saved file matches the live set
echo "live:  $(ipset list india_sip | grep -c '^[0-9]')"
echo "saved: $(grep -c '^add india_sip' /etc/ipset.conf)"

Healthy example from 2026-05-31:

Counter Value Meaning
india_sip entries 7,103 CIDR blocks loaded
Rule 3 ACCEPT (UDP India) pkts 642K legit Indian SIP flowing
Rule 4 DROP (UDP non-India) pkts 42,739 foreign attackers dropped
Rule 7 DROP (TCP non-India) pkts 1,899 foreign TCP SIP probes dropped

Common operations

Manually refresh the CIDR list now

ssh root@147.93.168.216
/usr/local/sbin/update-india-geoip.sh        # writes to stdout
# verify it printed 'Updated india_sip: <N> entries' and N > 7000
ipset list india_sip -t

The script uses an atomic swap: builds india_sip_new, ipset swaps it over the live set (zero-downtime — no window where the set is empty), destroys the old, then ipset save > /etc/ipset.conf. There is no need to reload iptables; the rules reference the set by name, so the swapped-in contents apply instantly.

Whitelist a specific non-India IP (e.g. an overseas softphone, a vendor)

The GeoIP set is meant for India CIDRs only. To allow a one-off foreign IP, add a dedicated ACCEPT rule above the DROP rule rather than polluting the set:

ssh root@147.93.168.216
cp /etc/iptables/rules.v4 /etc/iptables/rules.v4.bak-$(date +%F)
# Insert before the DROP (find the line number from iptables -L INPUT --line-numbers)
iptables -I INPUT <N> -s <IP>/32 -p udp -m multiport --dports 5060,5080 \
  -m comment --comment "Allow overseas: <who>" -j ACCEPT
iptables -I INPUT <N> -s <IP>/32 -p tcp -m multiport --dports 5060,5080 \
  -m comment --comment "Allow overseas: <who>" -j ACCEPT
netfilter-persistent save        # persist to rules.v4

Document who/why in the comment — undocumented allow rules are how perimeters rot.

Confirm whether a customer is being GeoIP-dropped

# Are their packets even reaching us, and are they being dropped at the SIP rules?
tcpdump -ni eth0 "host <CUSTOMER_IP> and udp port 5080"     # packets arriving?
ipset test india_sip <CUSTOMER_IP>                          # in the India set?
# 'NOT in set' + packets arriving + phone can't register  → GeoIP is dropping them

If a legitimate Indian customer's IP isn't in the set (ISP allocated a block newer than the last successful refresh), either run the refresh, or add their /24 as an explicit ACCEPT rule as above.

Restore after a wipe / failed boot (SIP totally down)

If ipset list india_sip shows the set missing or empty and SIP is down:

ssh root@147.93.168.216
# Rebuild from the saved file (fast, no download):
ipset restore -f /etc/ipset.conf
ipset list india_sip -t                 # confirm entries restored
# If the saved file is also bad, re-download live:
/usr/local/sbin/update-india-geoip.sh

If iptables rules themselves are gone (set present but no DROP rules):

netfilter-persistent reload              # reloads /etc/iptables/rules.v4
iptables -L INPUT -v -n | grep -i geoip  # confirm the GeoIP rules are back (6 on prod, 8 on staging)

Resolved: monthly auto-refresh silent failure (set -e + ((count++)))

Fixed on prod and staging 2026-05-31. For most of its life the cron refresh never completed — the set was static, seeded by hand on 2026-04-04.

What was wrong

update-india-geoip.sh ran under set -euo pipefail and counted adds with:

ipset add india_sip_new "$cidr" 2>/dev/null && ((count++))

On the first successful add, ((count++)) is post-increment: it evaluates the old value 0, and an arithmetic expansion that evaluates to 0 returns exit status 1. Under set -e that non-zero status killed the script before it ever reached ipset swap / ipset save. (Confirmed by reproducing set -e; count=0; true && ((count++)) → exits 1.)

Evidence it had been silently failing: /var/log/geoip-update.log showed only Downloading India IP ranges... for the May 1 run and never the closing Updated india_sip: <N>; /etc/ipset.conf mtime was stuck at 2026-04-04; the live set (7,103) exactly matched that stale saved file. It kept working only because the manual seed was close enough and Indian CIDRs drift slowly.

The fix (applied)

ipset add india_sip_new "$cidr" 2>/dev/null && count=$((count+1)) || true

count=$((count+1)) is an assignment (always exit 0); the trailing || true also swallows a duplicate-add failure. After applying, a manual run printed Updated india_sip: 7142 entries (up from 7,103 — 39 CIDRs had been missing since April) and /etc/ipset.conf mtime advanced to 2026-05-31. The prod script was backed up to update-india-geoip.sh.bak-2026-05-31 first.

Still open: no alerting on cron failure

The cron still redirects output to /var/log/geoip-update.log, which nobody reads. A future silent failure (download outage, ipdeny.com schema change, ipset error) would again rot undetected. Recommended follow-up: have the script ping a healthcheck (or raise a Netdata alarm) when it does not print the Updated india_sip: <N> success line. Until then, the manual check is:

# Should show a recent date and a healthy count on both hosts:
tail -1 /var/log/geoip-update.log; stat -c '%y' /etc/ipset.conf; ipset list india_sip -t | grep entries

History

Date Change
2026-04-04 india_sip set seeded manually on prod (7,103 CIDRs); iptables GeoIP rules + ipset-restore.service established.
2026-05-31 Fixed the set -e + ((count++)) bug so the monthly cron actually refreshes (prod set 7,103 → 7,142). Stood up the full GeoIP stack on staging (94.136.188.221) to mirror prod: ipset + iptables rules (allow wg0/wg1 + india_sip, DROP rest, UDP+TCP), ipset-restore.service, netfilter-persistent, monthly cron.