An open-source SIP/WebRTC edge proxy with the Caddy experience: one binary, one YAML file, ./freesbc run.
- Pure Go, one static binary, zero external dependencies: no database, no Redis, no kernel modules, and no external media process.
- Keeps your switch off the public internet: FreeSBC is the only element with a public address. FreeSWITCH or Asterisk stays on a private LAN and only ever talks to FreeSBC.
- Phones, browsers and carriers through one public address: SIP/UDP, SIP/TCP and SIP/TLS phones, WS/WSS WebRTC browsers, and carriers (the switch uses FreeSBC as its outbound proxy).
- Embedded WebUI, REST API and Prometheus metrics behind a bcrypt-checked admin login (a login form for the WebUI, Basic Auth for the API and metrics).
Prebuilt binary: each release carries freesbc_<version>_<os>_<arch>.tar.gz for linux and darwin on amd64 and arm64, plus SHA256SUMS. Each archive holds the binary, this README, the license and freesbc.example.yaml (linux archives add freesbc.service, see Run as a service):
v=v0.2.0 os=linux arch=amd64 # os: linux|darwin, arch: amd64|arm64
curl -LO https://github.com/rasonyang/freesbc/releases/download/$v/freesbc_${v}_${os}_${arch}.tar.gz
curl -LO https://github.com/rasonyang/freesbc/releases/download/$v/SHA256SUMS
shasum -a 256 -c --ignore-missing SHA256SUMS # Linux: sha256sum -c --ignore-missing SHA256SUMS
tar -xzf freesbc_${v}_${os}_${arch}.tar.gz && cd freesbc_${v}_${os}_${arch}
./freesbc check -c freesbc.example.yamlFrom source: requires Go >= 1.27.2:
go build -o freesbc ./cmd/freesbcThe linux archives also hold freesbc.service, a systemd unit that runs FreeSBC as the unprivileged freesbc user, with CAP_NET_BIND_SERVICE for listen ports below 1024 (such as wss: 443) and a read-only filesystem. FreeSBC writes no files: logs go to stderr, so journald needs nothing. As root, from the unpacked archive:
useradd --system --no-create-home --shell /usr/sbin/nologin freesbc
install -m 0755 freesbc /usr/local/bin/freesbc
install -m 0644 freesbc.service /etc/systemd/system/freesbc.service
install -d -m 0750 -o root -g freesbc /etc/freesbc
/usr/local/bin/freesbc init -c /etc/freesbc/freesbc.yaml # prompts on a terminal; in a script pass --switch etc. (see Quick start)
chown root:freesbc /etc/freesbc/freesbc.yaml && chmod 0640 /etc/freesbc/freesbc.yaml
/usr/local/bin/freesbc check -c /etc/freesbc/freesbc.yaml
systemctl enable --now freesbc
journalctl -u freesbc -fThe config and any TLS cert and key it names must be readable by the freesbc group (root:freesbc, mode 0640), so the service cannot change its own config; init writes mode 0600 owned by root, hence the chown and chmod. To start from the annotated freesbc.example.yaml instead, install -m 0640 -o root -g freesbc it in place of init and edit the addresses. For ${VAR} values such as admin.password_hash, put KEY=value lines in /etc/freesbc/freesbc.env (same owner and mode); the unit reads it if present. freesbc check does not read that file, so check such a config with env $(grep -v '^#' /etc/freesbc/freesbc.env) /usr/local/bin/freesbc check -c /etc/freesbc/freesbc.yaml (not . freesbc.env, which would expand the $ in a bcrypt hash). For bursty traffic raise net.core.rmem_max and net.core.wmem_max to at least 4 MiB and 1 MiB so the UDP SIP sockets get their requested buffers (docs/edge.md). There is no ExecReload: a shield.* edit reloads by itself when the file is saved, and any other key needs systemctl restart freesbc.
Read before exposing to the public internet:
docs/design.md(networking and deployment topology, security model) anddocs/edge.md(switch-side requirements).
./freesbc init -c freesbc.yaml # asks for the switch, detects the addresses
./freesbc check -c freesbc.yaml # validate; errors name the line and key
./freesbc run -c freesbc.yaml
./freesbc version # print the build versionfreesbc init writes a minimal config (mode 0600) and refuses to overwrite an existing file. On a terminal it prompts for each value, showing a detected default that Enter accepts: edge.switch first (no default), then private.ip (the local address that routes to the switch), public.bind (the local address toward the default route), public.ip and the UDP port, and finally an optional admin password (no echo, stored as a bcrypt hash). It asks before contacting https://api.ipify.org for public.ip (falling back to cloud instance metadata); public.bind is omitted when it equals public.ip. When they differ (1:1 NAT), init, check and run print the ports your router must forward to public.bind (every edge.listen port and the rtp range, same numbers); see docs/config.md. It fails if private.ip equals public.bind: the topology needs a second IP or NIC. When stdin is not a terminal it never prompts; give flags or environment variables (flag, then env, then detected default):
./freesbc init -c freesbc.yaml --switch 10.77.0.10:5060 \
--private-ip 10.77.0.2 --public-ip 203.0.113.7 --public-bind 172.31.5.10
# also: --udp-port N, --no-public-lookup; env FREESBC_SWITCH, FREESBC_PRIVATE_IP,
# FREESBC_PUBLIC_IP, FREESBC_PUBLIC_BIND, FREESBC_UDP_PORTAlternatively copy the annotated freesbc.example.yaml and edit it by hand.
A minimal config (every key is documented in docs/config.md):
public:
ip: 203.0.113.7 # advertised to phones, browsers, carriers
private:
ip: 10.77.0.2 # faces the switch
edge:
switch: [10.77.0.10:5060] # the switch, literal IP:port
listen:
udp: 5060 # ports on public.bindfreesbc.yaml is gitignored since it may hold real addresses and a password hash. -c defaults to freesbc.yaml; a positional argument is a usage error.
The config file is watched. A shield edit is validated and applied atomically; a bad edit never takes down the process, because the previous config stays active and the error is logged. Everything else (addresses, rtp, tls, edge, admin) is read once at startup: a reload that edits it is logged as a warning listing the keys, and the running process keeps its startup values until restarted.
On SIGINT/SIGTERM FreeSBC drops its calls with their media released; no BYE is sent. A second SIGINT/SIGTERM exits at once.
Internet ──> FreeSBC 203.0.113.7 public: SIP/UDP/TCP/TLS, WS/WSS, RTP, WebRTC
│
│ private LAN/VPN: plain SIP/UDP + RTP
│ one fixed socket, 10.77.0.2:5060
v
switch (FreeSWITCH / Asterisk) 10.77.0.10 no public IP, no port forward
│
└── carriers: the switch sends carrier traffic to FreeSBC as its outbound proxy
- Clients. REGISTER is proxied to the switch verbatim (the switch is the registrar; FreeSBC holds no credential) with the Contact rewritten to carry an
fsbc=token. Calls to clients and from clients are proxied with FreeSBC on the path. - Carriers. The switch owns carrier accounts, line selection and failover, and points each gateway at FreeSBC as outbound proxy. Requests to a host listed in
edge.carriersare proxied to that carrier with the switch's private addresses hidden; inbound carrier requests are delivered to the switch's carrier port with anX-FreeSBC-Carrierheader. - Topology hiding. Every SDP body is constructed by FreeSBC, never copied from the other leg, and all media is anchored on FreeSBC ports. No SDP, Contact or Request-URI carries the switch's address to a public party, or the public party's address to the switch. Every public leg, client or carrier, also hides the switch in Via, Record-Route, Contact and identity headers; client legs additionally drop the switch's Server, User-Agent, Warning,
X-and otherP-headers. The Call-ID and the NOTIFY and MESSAGE bodies are the switch's own text and pass through, so set the switch to keep its name out of them: switch-side requirements. - Shield. Per-IP rate limiting (separate limits for carrier sources) and scanner fingerprinting with an in-memory ban; every denial is a silent drop. FreeSBC touches no kernel firewall state. The private socket is trusted and exempt, so keep it unreachable from anywhere else.
Details: docs/edge.md (behaviour, switch-side requirements, limitations) and docs/design.md.
- Edge proxy: SIP/UDP, TCP, TLS, WS and WSS interworking in front of a private switch; REGISTER proxying; a multi-switch pool with per-user hashing.
- Carrier path: carrier directory by literal IP or DNS (SRV/A/AAAA, cached), switch-to-carrier proxying with topology hiding, inbound carrier delivery to the switch's carrier port, and deterministic carrier registration tokens.
- Media anchoring: RTP relay on FreeSBC ports with hardened first-packet latching and a silence watchdog; no transcoding.
- WebRTC: ICE-Lite, DTLS-SRTP and RTCP-mux terminated for browsers (any of
edge.listen.ws/wss), relayed to plain RTP. - Built-in security: per-IP rate limiting, scanner fingerprinting, in-memory bans, admission control for public INVITEs and REGISTER enumeration.
- Embedded WebUI + REST API: a live dashboard, a read-only config view with candidate validation and diff, Prometheus metrics, bcrypt admin login (form for the WebUI, Basic Auth for the API).
docs/config.md: every key, default, validation rule and reload classdocs/edge.md: topology, behaviour, switch-side requirements, non-goals, known limitationsdocs/homer.md: mirror SIP signaling to a HOMER collector over HEP (setup, what is mirrored, security notes)docs/troubleshooting.md: calls that connect and then drop (call endedreasons, lost ACK, RTP silence, session timers)docs/design.md: the design document, what the code does as implementedexamples/switch/: verified FreeSWITCH and Asterisk configurations for the reference topologyfreesbc.example.yaml: annotated example
Enable the optional admin block (a bcrypt password_hash; generate with freesbc hash-password; the user is always admin), then:
- browse
http://<admin.listen>/(the login form, a cookie session; Basic Auth remains for/metricsand/api/*) for the live dashboard and the Config tab: the running file read-only, a candidate editor with Validate (the same checks asfreesbc check, listing restart-only keys the candidate changes) and a line diff against the running file. The admin API never writes the config: edit the file, runfreesbc check -c freesbc.yaml, and the watcher reloads it, as with nginx. Keep secrets as${ENV}references, - the Audit tab (
GET /api/audit) lists the last 256 admin events: sign-ins (first successful login, failed login, rate-limited), logouts and drain changes (drain_on,drain_off);freesbc_admin_auth_failures_totalcounts the failures. The source is the connecting address, so behind a reverse proxy it is the proxy's, - the Health tab and header badge (
GET /api/health,GET /api/health/history) show whether anything needs attention: a switch node cooling down, carrier DNS failing, the RTP pool at 90 % or more, the shield ban table at its cap, 2xx ACKs not coming back from one side (3 in 10 min), the TLS certificate under 30 days from expiry or expired, plus the last 200 raise and clear events (in memory; thresholds are fixed, alerting stays in Prometheus;freesbc_admin_health_statusexposes the status as 0, 1 or 2), - scrape
http://<admin.listen>/metricswith Prometheus (basic_authin the scrape config), - set
admin.pprof: trueto also serve Go pprof underhttp://<admin.listen>/debug/pprof/behind the same auth (Basic Auth; off by default; restart-only), - read live state from
/api/status(including areloadobject: restart-only keys pending and the last failed reload),/api/calls,/api/health(overall status and the active conditions),/api/audit,/api/tls(the loaded TLS certificate, its expiry and whether the file on disk has been renewed; also the Overview's TLS card,freesbc_tls_cert_expiry_timestamp_secondsandfreesbc_tls_cert_disk_differs, and thetls_cert_renewedhealth condition) and/api/config, and the live tables/api/registrations,/api/carrier-registrations,/api/shield/bans,/api/switch-nodesand/api/carriers(read-only; the WebUI's State tab shows them), and check a candidate file withPOST /api/config/validate(body: the YAML; response{"valid", "errors", "restart_required"}; it writes nothing), - drain the node before a restart:
POST /api/drainrefuses new carrier calls (and carrier-facing OPTIONS) with503andRetry-After: 30, client calls still go through, while calls and registrations in place continue,GET /api/drainreportsactive_calls,DELETE /api/drainleaves drain mode (runtime state only; seedocs/edge.md).
Bind the admin listener private. Validation rejects a non-loopback admin.listen unless admin.allow_remote: true is set, which also requires the top-level tls identity and then serves HTTPS. Read the security model section of docs/design.md before setting it.
FreeSBC does only what is necessary to make one switch on a private LAN safely reachable from the public internet. Anything the switch (FreeSWITCH or Asterisk) already does well, FreeSBC does not do.
A feature is in scope only if it passes one of these:
- Only the edge can see or do it: the public wire before rewrite, TLS/WSS/WebRTC termination, NAT and latching, topology hiding, media anchoring, public-side admission, or FreeSBC's own state (sockets, ports, bindings, bans, reloads).
- The switch cannot do it well from behind the edge.
Everything else stays on the switch; FreeSBC's contribution is a documented switch-side example (see examples/switch/), not a feature. Concrete examples that stay on the switch:
- Per-carrier and per-user concurrency and CPS limits: FreeSWITCH
limit,max-sessions,sessions-per-second; AsteriskGROUP()/GROUP_COUNT(). - Maximum call duration: FreeSWITCH
sched_hangup; AsteriskDialL()andTIMEOUT(absolute). - Toll-fraud protection: switch authentication plus dialplan rules.
- Call history and CDR: the switch's CDR and CEL.
- Call lifecycle events: the switch's event system.
- RTCP reporting to HOMER: the switch receives the relayed RTCP and can report it.
- Config version history: the operator's VCS.
- Editing the config through the admin API: edit the file, run
freesbc check, and the watcher reloads it. Like nginx, there is no write API.
FreeSBC targets small and medium deployments on a single node. Preliminary single-host numbers (Docker on an Apple M3 Max VM, FreeSBC pinned to 2 cores, generator and switch on the same physical CPU, SIPp as the switch): about 1500 call setups per second, about 450 concurrent G.711 calls and about 800 concurrent WebRTC SRTP calls under 80% of the two cores, about 400 DTLS handshakes per second, and 19000 registrations held. These are not three-host or soak results and will differ on real hardware. Method, caveats, profiles and the first bottlenecks are in docs/performance.md; the rerunnable harness is in test/perf/.
Non-goals: transcoding, CDR, clustering, and being a registrar in its own right. The edge proxy proxies registrations to the switch rather than owning users or credentials.
- No transcoding; left to the switch.
- All state is in memory; a restart drops every call, dialog and binding.
- SDES-SRTP (
a=crypto) is off unlessedge.srtpor a carrier'ssrtpenables it, and then only over TLS/WSS signaling (oredge.allow_insecure_sdes); browser legs get DTLS-SRTP. - Switches are UDP only (carriers may be udp, tcp or tls); switches are literal
IP:port, with no DNS. - Call-ID passes through the proxy unchanged, and so do the NOTIFY and MESSAGE bodies the switch writes (MWI, BLF); a switch that puts its own name in either leaks it, and the settings that decide those names are on the switch.
- No PUBLISH, and no TURN or full ICE.
See known limitations for the full list.
go vet ./...
go test ./... -raceThe edge suite runs entirely on 127.0.0.1 and passes on macOS and Linux. See CLAUDE.md for per-package test ports and the opt-in FreeSWITCH interop test.
cmd/freesbc/ argv parsing, signal handling, exit codes
internal/
app/ construction and lifecycle: builds every component and runs it
config/ freesbc.yaml: parse, validate, hot reload
sip/ SIP protocol primitives
sip/sdp/ SDP subsystem (parse, codec negotiation, construction)
edge/ SIP/RTP/WebRTC proxy between public parties and the switch
media/ RTP/RTCP relay, port pools, WebRTC leg (ICE/DTLS/SRTP)
shield/ per-IP rate limiting, scanner fingerprinting, in-memory bans
admin/ operator HTTP API, Prometheus metrics, embedded WebUI
Everything is under internal/: the only consumer is cmd/freesbc, so no package here carries an API promise. Dependencies run one way: cmd/freesbc -> app -> {edge, admin}.
Apache License 2.0. See LICENSE.