Skip to content
rasonyangPublic

About

All-in-one open source Session Border Controller in pure Go: a SIP/WebRTC edge proxy that keeps FreeSWITCH or Asterisk off the public internet. One static binary, one YAML file: SIP proxy for phones and carriers, RTP/SRTP relay, WebRTC gateway, built-in SIP security.

Topics

Resources

Stars

176 stars

Watchers

8 watching

Forks

Repository files navigation

FreeSBC

An open-source SIP/WebRTC edge proxy with the Caddy experience: one binary, one YAML file, ./freesbc run.

License Go

  • 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).

Install

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.yaml

From source: requires Go >= 1.27.2:

go build -o freesbc ./cmd/freesbc

Run as a service

The 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 -f

The 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.

Quick start

Read before exposing to the public internet: docs/design.md (networking and deployment topology, security model) and docs/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 version

freesbc 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_PORT

Alternatively 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.bind

freesbc.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.

How it fits together

  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.carriers are proxied to that carrier with the switch's private addresses hidden; inbound carrier requests are delivered to the switch's carrier port with an X-FreeSBC-Carrier header.
  • 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 other P- 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.

Features

  • 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).

Documentation

  • docs/config.md: every key, default, validation rule and reload class
  • docs/edge.md: topology, behaviour, switch-side requirements, non-goals, known limitations
  • docs/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 ended reasons, lost ACK, RTP silence, session timers)
  • docs/design.md: the design document, what the code does as implemented
  • examples/switch/: verified FreeSWITCH and Asterisk configurations for the reference topology
  • freesbc.example.yaml: annotated example

Admin & WebUI

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 /metrics and /api/*) for the live dashboard and the Config tab: the running file read-only, a candidate editor with Validate (the same checks as freesbc 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, run freesbc 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_total counts 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_status exposes the status as 0, 1 or 2),
  • scrape http://<admin.listen>/metrics with Prometheus (basic_auth in the scrape config),
  • set admin.pprof: true to also serve Go pprof under http://<admin.listen>/debug/pprof/ behind the same auth (Basic Auth; off by default; restart-only),
  • read live state from /api/status (including a reload object: 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_seconds and freesbc_tls_cert_disk_differs, and the tls_cert_renewed health condition) and /api/config, and the live tables /api/registrations, /api/carrier-registrations, /api/shield/bans, /api/switch-nodes and /api/carriers (read-only; the WebUI's State tab shows them), and check a candidate file with POST /api/config/validate (body: the YAML; response {"valid", "errors", "restart_required"}; it writes nothing),
  • drain the node before a restart: POST /api/drain refuses new carrier calls (and carrier-facing OPTIONS) with 503 and Retry-After: 30, client calls still go through, while calls and registrations in place continue, GET /api/drain reports active_calls, DELETE /api/drain leaves drain mode (runtime state only; see docs/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.

Not in scope

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:

  1. 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).
  2. 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; Asterisk GROUP() / GROUP_COUNT().
  • Maximum call duration: FreeSWITCH sched_hangup; Asterisk Dial L() and TIMEOUT(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.

Status & limitations

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 unless edge.srtp or a carrier's srtp enables it, and then only over TLS/WSS signaling (or edge.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.

Development

go vet ./...
go test ./... -race

The 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.

Layout

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}.

License

Apache License 2.0. See LICENSE.

About

All-in-one open source Session Border Controller in pure Go: a SIP/WebRTC edge proxy that keeps FreeSWITCH or Asterisk off the public internet. One static binary, one YAML file: SIP proxy for phones and carriers, RTP/SRTP relay, WebRTC gateway, built-in SIP security.

Topics

Resources

Stars

176 stars

Watchers

8 watching

Forks

Releases

Packages

Contributors

Languages