# Network, firewall and attacks Most call problems come from the network between the server and the internet, and any PBX on the internet is attacked within hours of going up. This page covers both. ## NAT: a server behind a router Calls carry the server's address inside the SIP messages. Behind a router (NAT), the server has to know its **public IP** and which networks are **local**, or the other side sends the audio to the wrong place. 1. **Turn off SIP ALG** (also called "SIP helper" or "SIP fixup") on every router between the server and the internet. It rewrites SIP messages and is the classic cause of one-way audio and calls dropping after 30 seconds. Where to find it depends on the router; search for " disable SIP ALG". 2. **Forward ports** to the server's LAN IP: - UDP 5060 (SIP); - UDP 10000–20000 (audio; see [Call audio ports](#call-audio-ports) to use another range); - TCP 443 (and 80 with the bundled Caddy); - TCP 5060 only for phones or trunks set to TCP (Settings → Security → Accept SIP over TCP; phones this server sets up use UDP); - TCP 5061 only if you use SIP over TLS. - If nothing reaches the server on UDP 5060 while other UDP ports do (System → Diagnostics, a **Complete network test**), the router or the ISP keeps that port for itself: often an ISP box with phone jacks, or a SIP ALG you can't turn off. Set another **SIP port** in Settings → Security, for example 5080, forward that instead, restart telephony, and set the phones up again. Trunks keep working: they're reached from the port the server registers from. 3. **Double NAT** (the ISP's box in front of your own router): forward the ports on both, or put your router in the ISP box's DMZ / bridge mode. 4. **Public IP and local networks.** - The wizard's Network step sets both (go back to it at `/setup?step=3`). - The public IP is also on **Settings → Network**: **Detect** fills it in. - Local networks are the networks that reach the server without NAT. The default (`192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12`) fits most offices. 5. **Dynamic IP?** Tick **"Re-check the public IP every 5 minutes"** on Settings → Network. - When the IP changes, SelfPhoneNG updates its settings and emails the admins ("Public IP changed to …"). - **Then restart telephony** so Asterisk uses the new address: on the server, run `docker compose restart asterisk` (calls in progress drop). The dashboard shows "SIP port or transport changes need a telephony restart" until you do, and **System** shows the command. ![Settings → Network](img/settings-network.png) ## Call audio ports Call audio (RTP) uses UDP **10000–20000** unless you set another range in `.env` (or, in a Portainer stack, in the api's environment): ```sh RTP_PORT_START=40000 RTP_PORT_END=40199 ``` Then `docker compose up -d` (the api writes the range into Asterisk's config), and open **the same range** on the firewall and the router instead of 10000–20000. Each call uses two ports, so 200 ports is plenty for a small office. Pick a range nothing else on the server uses: System → Diagnostics counts how many ports in the range are already taken. ## Diagnostics: is the server reachable? **System → Diagnostics** checks what usually stops calls from working outside the office, and says how to fix each problem it finds. **This server** runs every time you open the page: - the public IP is set, and is the address the server really reaches the internet from; - the host name's DNS record points at that address (with Cloudflare, it must be "DNS only": its proxy carries web pages, not SIP); - the server's LAN address is in the local networks; - whether phones may sign in from anywhere (the phone network limit refuses phone apps on mobile data), and whether phone setup is limited to the local networks; - Asterisk is listening on the SIP ports; - how many call audio ports are free. **From the internet** needs a **network test probe**: a small program, run on a machine outside your network, that connects back to the server the way a phone on mobile data or your SIP provider does. A server can't test this itself: most routers either don't route a connection to their own public address back in, or route it without going through the port forwards. - **Quick network test**: TCP 80 and 443, and SIP on 5060 over UDP (and TCP if it's on). - **Complete network test**: also SIP over TLS (if on), the browser phone's `/ws`, and a sample of the call audio ports (the server listens on them while the probe sends to them). To run a probe, on any machine outside your network (a small VPS is plenty). It needs about 50 MB of memory and one open TCP port (8080). **Without Docker:** each release has the probe as a single executable for Linux, with everything it needs inside (`selfphoneng-probe-linux-x64`, or `-arm64` for ARM machines), next to `SHA256SUMS`. With Node 22 or newer installed you can take the small `selfphoneng-probe.cjs` instead. ```sh v=v0.0.13 # the release curl -fLO https://git.dou.bet/spng/selfphoneng/releases/download/$v/selfphoneng-probe-linux-x64 curl -fLO https://git.dou.bet/spng/selfphoneng/releases/download/$v/SHA256SUMS sha256sum --check --ignore-missing SHA256SUMS sudo install -m 755 selfphoneng-probe-linux-x64 /usr/local/bin/selfphoneng-probe curl -fLO https://git.dou.bet/spng/selfphoneng/raw/tag/$v/deploy/probe/selfphoneng-probe.service sudo cp selfphoneng-probe.service /etc/systemd/system/ sudo systemctl enable --now selfphoneng-probe sudo ufw allow 8080/tcp ``` (On GitHub, the downloads are at `https://github.com/iamdoubz/selfphoneng/releases/download/$v/…`.) To try it first without the service, run `./selfphoneng-probe-linux-x64` and open `http://:8080/healthz`. `PORT` changes the port; behind your own HTTPS reverse proxy on the same machine that sets X-Forwarded-For, also set `TRUST_PROXY=true`. The probe then only listens on 127.0.0.1 (`HOST` changes it) and only believes X-Forwarded-For from there. **With Docker:** ```sh docker compose -f deploy/probe/compose.yml up -d ``` Then put its address, for example `http://203.0.113.50:8080`, in **Settings → Network → Network test probe**. The probe only ever tests the address a request comes from, so nobody can use it against someone else, and each address gets a few tests per 10 minutes. Its SIP test is one OPTIONS request; if you run fail2ban or CrowdSec, add the probe's address to their allow list so a test never counts toward a ban. **Give the probe a key (strongly recommended).** Without one, anyone who finds your probe can use it to test their own address, and spend its few tests per 10 minutes. After saving the address, click **Generate key** under it, copy the `PROBE_KEY=…` line, and add it to the probe: under `environment:` in `deploy/probe/compose.yml` (`PROBE_KEY: '…'`), or as an `Environment=PROBE_KEY=…` line in the service file (`sudo systemctl edit selfphoneng-probe`). Restart the probe. From then on it only answers this server, and System → Diagnostics stops warning about it. The key is shown only once; a new probe address drops it, so generate one for the new probe. ## Firewall Allow only the ports in the [port table](README.md#ports). All SelfPhoneNG containers use the host's network, so the host firewall (ufw, firewalld, nftables) applies to them directly. With **ufw**: ```sh ufw allow OpenSSH ufw allow 443/tcp ufw allow 80/tcp # only with the bundled Caddy ufw allow 5060/udp ufw allow 10000:20000/udp # or your RTP_PORT_START:RTP_PORT_END ufw enable ``` ### Only voip.ms may reach SIP (no remote phones) If every desk phone is in the office, nothing else on the internet needs port 5060. Limit it to voip.ms and your local networks. 1. **Look up your POP's address** (the server you put on the trunk): ```sh dig +short chicago1.voip.ms ``` 2. **Save this as `/etc/nftables.d/selfphoneng.nft`**, with that address in `sip_ok`. Add any other office that has phones (the same networks as Settings → Security → "Other networks allowed"). ```nft table inet selfphoneng { set sip_ok { type ipv4_addr flags interval elements = { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 208.100.60.8 } } chain input { type filter hook input priority 0; policy accept; meta l4proto { tcp, udp } th dport { 5060, 5061 } ip saddr != @sip_ok drop meta nfproto ipv6 meta l4proto { tcp, udp } th dport { 5060, 5061 } drop } } ``` 3. **Load it:** `nft -f /etc/nftables.d/selfphoneng.nft`. To keep it after a reboot, include it from `/etc/nftables.conf`. This table only drops SIP from elsewhere and leaves your other rules alone. voip.ms can change its addresses: if the trunk stops registering one day, run `dig` again. ## Desk phones only from the office **Settings → Security → "Only accept desk phones from the networks below"** makes Asterisk refuse desk phones signing in from anywhere but your local networks and the ones you list. A stolen phone password is then useless from outside. - Browser phones aren't affected: they come in through HTTPS. - **"Only to phones on the local networks"** does the same for the phone setup files. ## Blocking password guessers: fail2ban or CrowdSec Asterisk writes every failed sign-in to `data/asterisk-logs/security.log`. Use one of the two tools below; both read that file on the host. ### fail2ban Install it (`apt install fail2ban`) and create `/etc/fail2ban/jail.d/selfphoneng.conf`. Change the path to where you cloned SelfPhoneNG: ```ini [asterisk] enabled = true logpath = /opt/selfphoneng/data/asterisk-logs/security.log maxretry = 5 findtime = 10m bantime = 1d # never ban your own offices ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ``` Then: ```sh systemctl restart fail2ban fail2ban-client status asterisk ``` The stock `asterisk` filter already understands the log. We test it against real failed sign-ins (`scripts/e2e/m11-t7.mjs`). ### CrowdSec Install CrowdSec and a bouncer (), then: ```sh cscli collections install crowdsecurity/asterisk ``` Add `/etc/crowdsec/acquis.d/selfphoneng.yaml`: ```yaml filenames: - /opt/selfphoneng/data/asterisk-logs/security.log labels: type: asterisk ``` and run `systemctl reload crowdsec`. #### Slow scanners: add a second rule The collection's rules (`crowdsecurity/asterisk_bf`, and `asterisk_user_enum` for many different usernames from one address) hold 5 failed sign-ins per address and let one go every 10 seconds. An address is banned only after 6 failures in about a minute. That stops fast password guessers, but not the common SIP scanners that space their tries out. Each try writes two failures (`InvalidAccountID`, then `ChallengeResponseFailed`), and the scanner comes back every 20 seconds or slower, so the count drains as fast as it fills and the scanner is never banned. You see it as `log_failed_request ... No matching endpoint found` / `Failed to authenticate` in the Asterisk log, from the same address, for hours. Add a slower rule next to the stock ones, `/etc/crowdsec/scenarios/selfphoneng-asterisk-slow.yaml`: ```yaml type: leaky name: selfphoneng/asterisk-slow-bf description: "Asterisk failed sign-ins spread over time (slow SIP scanners)" filter: evt.Meta.log_type == 'asterisk_failed_auth' groupby: evt.Meta.source_ip capacity: 10 leakspeed: 5m blackhole: 5m labels: service: asterisk confidence: 3 spoofable: 0 classification: - attack.T1110 behavior: "sip:bruteforce" label: "Asterisk slow bruteforce" remediation: true ``` ```sh systemctl reload crowdsec cscli scenarios list | grep selfphoneng # listed, marked local ``` It counts the same failures from the same parser; only the bucket is different: | | Stock: `crowdsecurity/asterisk_bf` | This rule: `selfphoneng/asterisk-slow-bf` | | ----------------- | ----------------------------------------- | ------------------------------------------------ | | Holds | 5 failures | 10 failures | | Lets one go every | 10 seconds | 5 minutes | | Bans after | 6 failures within about a minute | 11 failures, at more than one per 5 minutes | | A scanner every… | 20 s: never banned | 20 s: banned after about 2 minutes | | Comes from | the CrowdSec hub (`cscli hub upgrade`) | this file; change it as you like | Keep both: the stock rule still bans fast guessers within a minute. How long a ban lasts is set by CrowdSec's profiles, the same for both (4 hours by default, `/etc/crowdsec/profiles.yaml`). fail2ban's settings above (5 failures in 10 minutes) already catch slow scanners. The catch: one of your own phones with an old password, signing in from outside your networks, adds up failures too and is banned after about 11 tries. CrowdSec's standard whitelist (`crowdsecurity/whitelists`) skips private addresses, so phones in the office are safe. Add the public address of any other office or remote worker to a whitelist, `/etc/crowdsec/parsers/s02-enrich/selfphoneng-whitelist.yaml`: ```yaml name: selfphoneng/whitelist description: "Our offices and remote phones" whitelist: reason: "our offices" ip: - "198.51.100.7" ``` #### Checking CrowdSec ```sh cscli metrics show acquisition parsers scenarios ``` - **Acquisition:** `security.log`'s _Lines read_ goes up as Asterisk writes. _Lines parsed_ is lower, and that's normal: only failed sign-ins are parsed, other security events (challenges sent, successful sign-ins) are left out. - **Scenarios:** _Overflows_ for `asterisk_bf` and `selfphoneng/asterisk-slow-bf` is the number of bans each has made. To follow one log line through the parser and the rules, give `cscli explain` a line from `security.log`: ```sh cscli explain --type asterisk \ --log "$(grep ChallengeResponseFailed /opt/selfphoneng/data/asterisk-logs/security.log | tail -1)" ``` Point `--file` at the log itself, never at `acquis.d/selfphoneng.yaml`: that parses the YAML's own lines as if they were log lines, and every one shows as a parser failure. To ban an address now, and to see the bans: ```sh cscli decisions add --ip 203.0.113.5 --duration 7d --reason "SIP scanner" cscli decisions list ``` ## Web sign-in attacks The web sign-in is rate limited. Failed attempts are written to the api's log as `"event":"auth.failed"` with the `ip`, so you can feed them to fail2ban or CrowdSec too (`docker compose logs api`). Turn on two-step sign-in for every admin. ## Toll fraud Someone with a stolen SIP password calling expensive international numbers is the attack that costs real money. Keep: - international calling **off** at voip.ms and in **Outbound rules** unless you need it; - **IP restriction** on the voip.ms sub-account, allowing only your server's public IP: a stolen trunk password used from another server skips every limit below ([voip.ms setup](voipms.md#1-in-the-voipms-portal)); - the voip.ms **balance alerts and spending limits** on; - the **desk phone network limit** above on if your phones are all in the office; - the **outside calls at once** limits (Settings → Security) as low as your office needs: per extension (3 by default) and for the whole system (10). A stolen password then runs up at most that many calls at a time. Forwarded calls count for the person who set the forward; 911 is never limited. Callers over a limit hear "all circuits are busy". Never reuse a SIP password. SelfPhoneNG generates long random ones; phones get them from the setup files.