CallFXO is a lightweight SIP + WebRTC bridge server for dialing PSTN through FXO gateways.
It provides:
- SIP registrar for FXO boxes (REGISTER)
- SIP UAC call control for outbound INVITE to a selected FXO box
- WebRTC audio bridge in the browser (PCMU passthrough)
- Optional TURN / TURNS ICE server support for NAT traversal
- Web UI for users, FXO boxes, dialing, DTMF, call logs, and personal contacts
- Password change:
- user can change only their own password
- admin can change any user's password
- Role-based access control (
admin/user) - SQLite persistence
- WebSocket signaling and live FXO online/offline status updates
Confirmed working with:
- Cisco SPA3000
- Cisco SPA232D
For these devices, minimum setup is:
- Set Proxy to your CallFXO SIP address (for example
192.168.1.10:5060).
- If it's not working, try
TCPinstead ofUDPas SIP protocol.
- Set SIP username/password to values created in CallFXO FXO box management.
- Enable PSTN Line.
- Enable Register.
- Go 1.25+
- A browser with WebRTC + PCMU support
- Network reachability between browser, CallFXO server, and FXO box
- Copy the example config and edit it:
cp config.yaml.example config.yaml- Set at least:
sip.advertised_ipandmedia.public_ipto the IP reachable by FXO boxes and browsersdatabase.pathto your SQLite fileauth.access_ttl_minutes/auth.refresh_ttl_hoursto your desired session lifetimemedia.ice_stun_urlsand optionallymedia.ice_turn_urlsfor browser / app ICE traversalfcm.*only if you want Android / browser push notifications
- Start server:
go run . -config config.yaml- Open web UI:
http://<server-ip>:8080
- Login with bootstrap admin:
- first run (DB not exists): check console output for generated password
- existing DB: use your existing admin password
Dockerfile and docker-compose.yml are included.
- In
config.yaml, set:
database.path: /data/callfxo.dbsip.advertised_ipandmedia.public_ipto your host/server reachable IP
- Start:
docker compose up -d --buildExposed ports:
8080/tcp(web)5060/tcpand5060/udp(SIP)12000-12100/udp(RTP dynamic range, default compose profile)
Note: compose sets net.ipv4.ip_local_port_range=12000 12100 so RTP ephemeral ports stay inside mapped UDP range.
When the database file does not exist (first run), the server auto-generates:
- bootstrap admin password
- auth session secret (cookie signing secret)
Both are persisted in SQLite (app_settings for secret) and loaded into memory on startup.
The generated bootstrap admin password is printed to the server console once at first initialization.
Change this in production.
config.yaml example fields:
http:
listen: ":8080"
sip:
transport: "tcp"
listen: "0.0.0.0:5060"
realm: "callfxo"
domain: "callfxo.local"
advertised_ip: "192.168.26.160"
contact_user: "callfxo"
media:
rtp_bind_ip: "0.0.0.0"
public_ip: "192.168.26.160"
ice_stun_urls:
- "stun:stun.l.google.com:19302"
ice_turn_urls:
- "turn:turn.example.com:3478?transport=udp"
- "turn:turn.example.com:3478?transport=tcp"
- "turns:turn.example.com:5349?transport=tcp"
ice_turn_username: ""
ice_turn_credential: ""
ice_turn_shared_secret: ""
ice_turn_credential_ttl_minutes: 60
database:
path: "./callfxo.db"
auth:
cookie_name: "callfxo_access"
access_ttl_minutes: 60
refresh_ttl_hours: 720
fcm:
enabled: false
project_id: ""
app_id: ""
api_key: ""
messaging_sender_id: ""
auth_domain: ""
storage_bucket: ""
measurement_id: ""
vapid_key: ""
service_account_json: ""Use config.yaml.example as the authoritative template. Legacy auth.session_ttl_hours is still read for compatibility, but new deployments should use access_ttl_minutes and refresh_ttl_hours.
For double-NAT or restrictive network environments, configure TURN or TURNS in media.ice_turn_urls.
CallFXO supports both:
- fixed TURN credentials via
media.ice_turn_username/media.ice_turn_credential - coturn shared-secret dynamic credentials via
media.ice_turn_shared_secret
If media.ice_turn_shared_secret is set, CallFXO generates short-lived credentials for Web and Android clients automatically.
An example coturn deployment is included in coturn/README.md.
- The server is the source of truth for Firebase config. Android and web clients fetch
/api/push/configafter login and cache it locally. - Set
fcm.enabled: trueonly when you want push notifications for incoming calls. fcm.service_account_jsonmust point to a readable Firebase service account JSON file on the server. This is required for sending FCM messages.- Browser push also needs
fcm.vapid_key. - If FCM is disabled or not configured, foreground web / app calling still works; only push wake-up notifications are skipped.
admin: manage users + FXO boxes, and place callsuser: place calls only
- Choose FXO box + enter number + click Dial
- DTMF keypad available during call
- Click a phone number in Call Logs or Contacts to auto-fill the dial input
- Call logs and contacts are per-user
- This project is designed for PCMU passthrough
G711u(no server-side transcoding). - If your browser/device path cannot negotiate PCMU, add a transcoder externally.