A working registration, inbound call, and outbound call prove that FreeSWITCH can carry traffic. They do not prove that the switch is ready for the public internet.
Toll fraud usually needs a chain of mistakes. An attacker finds a SIP listener, obtains an extension credential, or gets an unauthenticated INVITE into a dialplan that can reach a carrier gateway. Europol describes a familiar result in its guidance on telecommunications fraud: a sudden volume of long calls concentrated on a small set of expensive destinations. The bill arrives at the end of the chain. Each earlier control should have had a chance to stop the call.
This tutorial uses Ubuntu and the FreeSWITCH vanilla configuration as its baseline, while accepting that softphones, mobile networks, and home broadband addresses change. The finished setup has five observable outcomes: a legitimate endpoint can register from a dynamic public address; repeated bad authentication attempts trigger a temporary ban; only documented carrier addresses can reach the trunk-facing profile; ordinary extensions can dial only approved patterns; and an international or premium-rate pattern is rejected locally without reaching the carrier.
Find the Configuration That the Running Process Uses
Package installations commonly use /etc/freeswitch. A default source installation commonly uses /usr/local/freeswitch/conf. The FreeSWITCH getting-started guide documents both locations. Ask the running process instead of editing a directory that may be inactive:
# 中文注释:读取运行中实例实际使用的配置目录和日志目录
fs_cli -x 'global_getvar conf_dir'
fs_cli -x 'global_getvar log_dir'
# 中文注释:查看 SIP profile 和常见监听端口
fs_cli -x 'sofia status'
sudo ss -lntup | rg ':(5060|5080|8021|5066|7443)\b'
Use grep -E if rg is unavailable. The examples below assume /etc/freeswitch. Back up that directory before editing it and check that the backup actually exists. Restarting a Sofia profile interrupts calls on that profile, so profile changes belong in a maintenance window.
The example carrier network 203.0.113.0/24 is a documentation-only placeholder and does not belong in a production configuration. Endpoints in this design register directly from dynamic public addresses, so there is no endpoint source-IP allowlist.
Allowlist Fixed Sources and Open Only the Endpoint Transport You Need
Moving SIP from port 5060 to a less familiar number may reduce log noise. It is not access control. Carrier addresses are usually fixed and can be allowlisted. Public endpoints often move between networks, so their actual SIP transport must remain reachable while FreeSWITCH authentication and automatic banning handle hostile requests.
This example uses UFW, Ubuntu's default firewall frontend. Preserve your actual SSH management path before enabling it. The Ubuntu Server firewall guide documents source-network and destination-port rules.
# 中文注释:先保留 SSH 管理入口;非默认 SSH 应按真实端口和来源地址收紧
sudo ufw allow OpenSSH
# 中文注释:默认拒绝入站,保留正常出站连接
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 中文注释:只让运营商信令网段访问 external profile
sudo ufw allow proto udp from 203.0.113.0/24 to any port 5080
sudo ufw allow proto tcp from 203.0.113.0/24 to any port 5080
# 中文注释:动态公网分机需要访问 internal profile;本例只开放实际使用的 UDP
# 中文注释:终端不用 TCP 时,不要顺手开放 5060/tcp
sudo ufw allow 5060/udp
# 中文注释:公网分机还需要媒体可达,端口范围必须与 vars.xml 一致
# 中文注释:已拆分媒体地址或边界策略时,应按真实拓扑进一步收紧
sudo ufw allow 16384:32768/udp
# 中文注释:检查规则,启用日志,再启用防火墙
sudo ufw status verbose
sudo ufw logging on
sudo ufw enable
A carrier may use different networks for SIP signaling and RTP media. An incomplete media policy often produces one-way audio, silence, or intermittent calls. Keep the carrier-facing signaling profile allowlisted and obtain the carrier's complete signaling networks, media networks, and transports. If every endpoint supports SIP over TLS, you can expose only a TLS listener, but that requires correct certificates, endpoint verification, and matching profile settings—not merely changing the port to 5061.
Apply the same policy to a cloud security group or upstream firewall. A strict UFW rule cannot make traffic pass through a cloud firewall that rejects it. A permissive cloud rule becomes dangerous as soon as someone clears the host rules.
Check the Source Again with a FreeSWITCH ACL
The host firewall sits outside FreeSWITCH. An application ACL checks the source inside the switch. Keeping the same verified address inventory in both places limits the damage from a mistake in either layer.
Add the carrier list inside <network-lists> in autoload_configs/acl.conf.xml:
<!-- 中文注释:运营商白名单默认拒绝,只允许官方公布的信令网段 -->
<list name="trusted-carriers" default="deny">
<node type="allow" cidr="203.0.113.0/24"/>
</list>
Apply the carrier list in the <settings> block of sip_profiles/external.xml, and keep the public context:
<!-- 中文注释:外部中继来话只能进入 public,不能直达可出站的 default -->
<param name="context" value="public"/>
<!-- 中文注释:external 通常按运营商 IP 建立信任,未通过 ACL 的请求直接拒绝 -->
<param name="apply-inbound-acl" value="trusted-carriers"/>
<param name="auth-calls" value="false"/>
auth-calls=false does not mean that the world should have unauthenticated access to your gateway. It fits a carrier-facing profile when the firewall and ACL already restrict sources and the public dialplan accepts only assigned DIDs.
Dynamic public endpoints cannot use apply-register-acl without breaking whenever their address changes. Tighten authentication explicitly in the <settings> block of sip_profiles/internal.xml instead:
<!-- 中文注释:internal profile 上的 INVITE 必须完成 SIP 摘要认证 -->
<param name="auth-calls" value="true"/>
<!-- 中文注释:盲注册和盲认证只适用于测试,生产环境显式关闭 -->
<param name="accept-blind-reg" value="false"/>
<param name="accept-blind-auth" value="false"/>
<!-- 中文注释:认证用户名必须与 From 用户一致,减少身份混用 -->
<param name="inbound-reg-force-matching-username" value="true"/>
<!-- 中文注释:业务不需要一号多终端时关闭多注册,共享分机不要照抄 -->
<param name="multiple-registrations" value="false"/>
The FreeSWITCH Sofia profile manual identifies accept-blind-reg and accept-blind-auth as testing settings. It also documents inbound-reg-force-matching-username. Setting multiple-registrations=false affects shared extensions and multi-device users, so confirm that constraint before making it explicit.
The FreeSWITCH ACL manual says that reloadacl reloads XML and rebuilds the network lists. The apply-inbound-acl and internal profile changes still require the relevant profile to restart:
# 中文注释:先验证 XML 并重载 ACL,任何错误都应阻止后续重启
fs_cli -x 'reloadxml'
fs_cli -x 'reloadacl'
# 中文注释:Profile 重启影响现有通话,只在维护窗口执行
fs_cli -x 'sofia profile external restart'
fs_cli -x 'sofia profile internal restart'
Feed Repeated Registration Failures to Fail2ban
Direct public registration means that port 5060 will receive scans and password guesses. FreeSWITCH's mod_fail2ban listens for Sofia events and writes explicit registration failures to a dedicated log. System Fail2ban can then ban the source. The module is not loaded by every package configuration, so verify both the module and log directory first:
# 中文注释:确认模块是否存在;false 表示需要安装或构建当前版本匹配的模块
fs_cli -x 'module_exists mod_fail2ban'
# 中文注释:模块存在但未加载时加载,并读取专用日志所在的根目录
fs_cli -x 'load mod_fail2ban'
fs_cli -x 'global_getvar log_dir'
Add this line to autoload_configs/modules.conf.xml so the module loads after a restart. Its default configuration normally writes $${log_dir}/fail2ban.log, but verify that path on the target host.
<!-- 中文注释:记录 Sofia 注册失败事件,供系统 Fail2ban 读取 -->
<load module="mod_fail2ban"/>
Install the operating system's Fail2ban package:
# 中文注释:从系统仓库安装,与发行版的 systemd 和 UFW 配置保持一致
sudo apt update
sudo apt install fail2ban
Then create /etc/fail2ban/filter.d/freeswitch-register.local:
[Definition]
# 中文注释:锚定 mod_fail2ban 完整事件行,避免普通日志文字误封地址
failregex = ^A registration failed user\[[^]]*\] ip\[<HOST>\] at\[.*\]$
# 中文注释:管理地址应在 jail 的 ignoreip 单独配置,不在这里写宽泛排除式
ignoreregex =
Create /etc/fail2ban/jail.d/freeswitch-register.local. These thresholds are a starting point. A call center, a shared NAT exit, or a bulk device rollout needs values based on its normal failure baseline:
[freeswitch-register]
# 中文注释:启用注册失败监控,并引用上面的过滤器
enabled = true
filter = freeswitch-register
# 中文注释:替换成 global_getvar log_dir 返回目录下的真实日志
logpath = /var/log/freeswitch/fail2ban.log
# 中文注释:官方 ufw 动作要求 UFW 已启用,封禁后应检查实际规则
banaction = ufw
# 中文注释:十分钟八次失败后封一小时,必须按正常业务调整
maxretry = 8
findtime = 10m
bantime = 1h
Test the expression before applying it. Review both matched and missed lines, then restart Fail2ban only after the sample is clean:
# 中文注释:用真实日志检查匹配,避免宽泛正则造成误封
sudo fail2ban-regex /var/log/freeswitch/fail2ban.log /etc/fail2ban/filter.d/freeswitch-register.local
# 中文注释:重启后确认 jail 已启动,并查看失败与封禁计数
sudo systemctl restart fail2ban
sudo fail2ban-client status freeswitch-register
The FreeSWITCH module documentation provides the registration-failure format. The official Fail2ban UFW action assumes that UFW is active. Do not run bad-password tests from the same public address that carries SSH management. To release a controlled test address, run sudo fail2ban-client set freeswitch-register unbanip TEST_IP.
Fail2ban reduces repeated guesses from one address. It is less effective when an attacker rotates addresses, and a stolen credential that authenticates successfully produces no failed-login ban. Outbound permissions, carrier limits, and monitoring remain mandatory.
Remove Demo Users and Shared Passwords
The vanilla configuration ships with test users 1000 through 1019 and a shared default_password of 1234. These values are documented in the official getting-started guide. Remove unused demo users from the included directory/default/ path. Give every remaining endpoint an independent random secret.
# 中文注释:为每个分机单独生成一次 48 位十六进制随机口令
openssl rand -hex 24
A minimal user entry can look like this:
<include>
<!-- 中文注释:2001 是示例分机,password 必须换成独立随机值 -->
<user id="2001">
<params>
<param name="password" value="REPLACE_WITH_RANDOM_SECRET"/>
</params>
<variables>
<!-- 中文注释:认证通过后进入内部拨号上下文 -->
<variable name="user_context" value="default"/>
<!-- 中文注释:只授予中国大陆手机号权限,标签名由运维定义 -->
<variable name="toll_allow" value="domestic-mobile"/>
<variable name="accountcode" value="2001"/>
</variables>
</user>
</include>
An obscure extension number is still not a secret. Do not reuse a carrier portal, SSH, or another phone's password. Keep credentials out of screenshots, tickets, and logs. When a handset is lost or a softphone profile leaks, revoke that user without rotating every endpoint.
Keep Carrier Traffic in the Public Context
The official public-context guide describes the separation plainly. Unauthenticated carrier traffic on the external profile enters public. Authenticated internal users get the broader default dialplan. Setting the external profile's context to default exposes outbound-capable routing to external requests.
Keep explicit DID matches in dialplan/public/. This example sends one assigned number to extension 2001:
<include>
<extension name="inbound-main-number">
<!-- 中文注释:只匹配运营商分配的完整 DID,不使用任意数字通配 -->
<condition field="destination_number" expression="^861055501234$">
<!-- 中文注释:命中后只转到明确的内部目标 -->
<action application="transfer" data="2001 XML default"/>
</condition>
</extension>
</include>
Do not place a ^\d+$ rule in public that bridges to a carrier. Do not transfer any arbitrary external destination into default for another routing pass. Caller ID can be spoofed and must not serve as an authentication credential.
Make Outbound Routing Deny by Default
Successful endpoint authentication does not grant a reason to call every destination. toll_allow labels a user's permission. It has no effect unless the outbound dialplan checks it. The FreeSWITCH routing documentation shows the same two-part design: put the variable in the user directory, then test it in the dialplan.
This rule lets a user with domestic-mobile permission call mainland China mobile numbers. It accepts an 11-digit number or the same number prefixed with 86, then sends a normalized 86 number to the carrier:
<include>
<extension name="allow-mainland-mobile">
<!-- 中文注释:先检查认证用户是否拥有该类出站权限 -->
<condition field="${toll_allow}" expression="(^|,)domestic-mobile(,|$)">
<!-- 中文注释:只接收明确号码格式,拒绝短号和任意国际前缀 -->
<condition field="destination_number" expression="^(?:86)?(1[3-9]\d{9})$">
<!-- 中文注释:carrier 必须替换成实际 gateway 名称 -->
<action application="bridge" data="sofia/gateway/carrier/86$1"/>
<anti-action application="hangup" data="CALL_REJECTED"/>
</condition>
<anti-action application="hangup" data="CALL_REJECTED"/>
</condition>
</extension>
</include>
Replace broad outbound rules with this pattern. Do not leave an earlier ^\d+$ or ^(.+)$ bridge beside it. If a permissive rule matches first and completes the bridge, the restrictive rule never runs. Add separate, explicit patterns and permissions for landlines, emergency numbers, short codes, or other required services.
Leave international and premium-rate routes absent by default. If a small group needs them, create separate permissions and destination patterns. Do not grant every user an international label or open every number following 00 or + with one catch-all expression.
Keep the Event Socket Local
The Event Socket commonly listens on TCP 8021. An authenticated client can execute FreeSWITCH API commands, so this is much more than a status endpoint. The Event Socket manual recommends a specific bind address, a narrow ACL, and a strong password.
For local fs_cli and local applications, make the choice explicit in autoload_configs/event_socket.conf.xml:
<configuration name="event_socket.conf" description="Socket Client">
<settings>
<!-- 中文注释:只监听本机,公网和局域网主机都不能直连 -->
<param name="listen-ip" value="127.0.0.1"/>
<param name="listen-port" value="8021"/>
<!-- 中文注释:使用独立强口令,不保留示例值 ClueCon -->
<param name="password" value="REPLACE_WITH_ANOTHER_RANDOM_SECRET"/>
<param name="apply-inbound-acl" value="loopback.auto"/>
</settings>
</configuration>
Use an SSH tunnel or a dedicated management network for remote administration. Do not add a UFW rule that exposes 8021 to the internet. If an application genuinely needs a remote connection, bind a dedicated private address and allow only its management network.
Detect Abuse and Cap the Damage
Configuration can drift, and endpoints can be compromised. Add controls at the carrier: disable unused international and premium destinations, cap concurrent calls, set a spending limit, and enable anomaly notifications. Product names and available limits vary by carrier. Choose thresholds from your normal traffic rather than copying a universal number.
The vanilla configuration loads mod_cdr_csv by default. According to the FreeSWITCH CDR guide, its master file normally lives at cdr-csv/Master.csv under the FreeSWITCH log directory. Verify the module and find that directory:
# 中文注释:确认 CDR 模块已加载,并读取当前日志根目录
fs_cli -x 'module_exists mod_cdr_csv'
fs_cli -x 'global_getvar log_dir'
# 中文注释:观察当前通话数量和分机注册状态,建立正常业务基线
fs_cli -x 'show calls count'
fs_cli -x 'sofia status profile internal reg'
Monitoring should reveal a sudden increase in calls, one extension targeting an unfamiliar country or number range, sustained long calls outside normal usage, or carrier spending that diverges from the usual pattern. CDRs help with detection and investigation. They do not block a call.
Verify Five Outcomes
A successful reloadxml is not a security test. Test from both trusted and untrusted network positions, and never place a real premium-rate test call.
- Register a test extension from a dynamic public address with the correct credential. It should appear in
sofia status profile internal reg. - Submit controlled bad-password attempts from an address that does not carry SSH management. After the threshold, it should appear in the banned list from
fail2ban-client status freeswitch-register; unban it when the test ends. - Send a SIP request to port 5080 from a non-carrier address. The firewall or ACL should reject it, while a legitimate carrier call still reaches an assigned DID.
- Use a test extension with
domestic-mobilepermission to call a mainland mobile number you control. The call should connect and produce a CDR. - Dial a pattern outside the allowlist, such as
00870123456789. FreeSWITCH should reject it locally, and the carrier portal should show no corresponding attempt.
When a check fails, follow the call path: host firewall, FreeSWITCH ACL or Fail2ban, SIP authentication, context, toll_allow, destination pattern, then gateway. That order matches how the call moves through the system and keeps the investigation focused.
Common Gaps That Remain
Fail2ban acts only after failures occur, and an attacker can rotate addresses. Use it on the public endpoint listener that cannot be restricted by source. Continue to reject unrelated sources at the firewall and ACL on the carrier listener, where addresses are fixed.
TLS protects SIP signaling in transit. It does not repair a broad outbound dialplan or disable expensive destinations at the carrier. Deploy it where endpoints and carriers support it, but do not treat a TLS listener as protection against toll fraud by itself.
WebSocket 5066, WSS 7443, IPv6 SIP profiles, and web management panels are easy to miss. Close or stop what you do not use. Apply the same source restrictions, authentication, and logging to what remains. IPv4 firewall rules do not automatically cover IPv6.
Keep This Go-Live Checklist
- Unused vanilla test users are gone, and every endpoint has an independent random secret.
- Port 5060 exposes only the endpoint transport in use, with strict authentication and failed-registration banning active. Port 5080 accepts only documented carrier signaling networks.
- RTP sources match the carrier's documentation, and two-way audio has been verified.
- The external profile remains in
public; the public dialplan has no arbitrary outbound bridge or unconditional transfer intodefault. - Each user receives only the required
toll_allowvalues, and outbound rules check both permission and a specific destination pattern. - Port 8021 binds to localhost or a dedicated management network, and the sample password is gone.
- CDRs are written successfully. Carrier destination controls, concurrency caps, spending limits, and alerts are active.
- Correct registration from a dynamic public address, failed-authentication banning, carrier inbound calling, an allowed outbound call, and a rejected outbound pattern have all been tested.
For a focused review of an existing FreeSWITCH deployment, prepare the FreeSWITCH version, installation method, sanitized SIP profiles, ACLs, directory entries, public/default dialplans, listening ports, and the carrier's published signaling and media networks. Do not send extension passwords, carrier secrets, real bills, or internal IP addresses. You can find my code and contact links on GitHub and my personal site.