No description
  • Rust 99.4%
  • Dockerfile 0.5%
  • C 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
coral 996359c50c
Some checks failed
Build and publish Docker image / ci (push) Has been cancelled
chore: Release
2026-09-08 07:52:22 -07:00
.github/workflows Merge remote-tracking branch 'sipcord-bridge/master' 2026-08-25 16:10:42 -07:00
pjsua chore: Release 2026-09-04 08:31:45 -07:00
sipcord-bridge chore: Release 2026-09-08 07:52:22 -07:00
wav big one 2026-03-20 16:08:41 -07:00
.env.example ip discovery 2026-09-04 08:30:34 -07:00
.gitignore big one 2026-03-20 16:08:41 -07:00
.gitmodules s 2026-08-25 13:59:20 -07:00
Cargo.lock lock 2026-09-08 07:52:19 -07:00
Cargo.toml lul 2026-09-03 20:54:35 -07:00
config.toml test 2026-08-17 20:36:24 +02:00
Dockerfile Improve the 'max_calls/file handle/connection' limits to be more... modern 2026-08-26 11:02:01 -07:00
LICENSE.md more formatting 2026-03-20 16:14:57 -07:00
README.md more fax 2026-09-08 07:52:13 -07:00

SIPcord Bridge

This is a slice of the code that powers SIPcord that you can use to self host something similar. It's not the full SIPcord package but rather the core functionality used in SIPcord with ways to build your own backend adapter. SIPcord itself uses this as a component of the full build so the code is the same that runs on the public bridges.

This means you have to build the call routing backend yourself. I am including a static-router backend which you can use to map extensions in a TOML file like this

[extensions]
1000 = { guild = 123456789012345620, channel = 987654321012345620 }
2000 = { guild = 123456789012345620, channel = 111222333444555620 }

but if you want more fancy routing you have to build it. You can easily use sipcord-bridge as a library and provide your own routers by implementing the Backend trait.

This was written a mix between myself and claude, sure, some of it's big slop but the parts I care about are not.

Can you help me set this up?

No. I am not providing support for this as my goal is to run sipcord.net, not support self hosting. If you want to run this self hosted, feel free to use this code but you are on your own here.

I have a feature request!

PR's welcome. No really, feel free to implement it and contribute.

AI Generated Setup Instructions

These instructions were written by Claude. They might be wrong. Remember — no support is provided.

Prerequisites

  • A Discord bot with voice permissions. Create one at https://discord.com/developers/applications, enable the Message Content intent, and grab the bot token.
  • A server with a public IP (or port-forwarded UDP). SIP uses UDP 5060 and RTP uses UDP 10000-15000 by default.
  • Docker (recommended) or Rust nightly toolchain if building from source.

1. Invite the bot to your server

Use this URL format, replacing YOUR_CLIENT_ID:

https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&scope=bot&permissions=36701184

The bot needs View Channel + Connect + Speak permissions in voice channels.

2. Get Discord channel IDs

Enable Developer Mode in Discord (Settings > App Settings > Advanced > Developer Mode). Right-click a voice channel and click "Copy Channel ID". Do the same for the server (guild) by right-clicking the server name.

3. Create the dialplan

Create a dialplan.toml mapping extensions to Discord channels:

[extensions]
1000 = { guild = 123456789012345678, channel = 987654321012345678 }
2000 = { guild = 123456789012345678, channel = 111222333444555666 }

Each extension is what you'll dial from your SIP phone. Pick any numbers you like.

Create a directory for your deployment:

mkdir sipcord && cd sipcord

Create a .env file:

DISCORD_BOT_TOKEN=your_bot_token_here
SIP_PUBLIC_HOST=your.server.ip.or.hostname

Create a docker-compose.yml:

services:
  sipcord-bridge:
    image: ghcr.io/coral/sipcord-bridge:latest
    container_name: sipcord-bridge
    restart: always
    ulimits:
      nofile:
        soft: 4096
        hard: 4096
    network_mode: host
    env_file:
      - .env
    volumes:
      - ./dialplan.toml:/app/dialplan.toml:ro
      # Uncomment to persist data across restarts:
      # - ./data:/var/lib/sipcord

Place your dialplan.toml in the same directory, then:

docker compose up -d
docker logs -f sipcord-bridge

You should see it load the dialplan and start listening.

4b. Build from source

Requires Rust nightly (for portable_simd) and system dependencies for pjproject (OpenSSL, Opus, libtiff, etc). See the Dockerfile for the full list.

git clone https://github.com/coral/sipcord-bridge.git
cd sipcord-bridge
git submodule update --init --recursive
cargo run --release -p sipcord-bridge

If you already cloned the repository, run git submodule update --init --recursive from its root before building.

The binary reads config.toml from the working directory (or CONFIG_PATH), the dialplan from ./dialplan.toml (or DIALPLAN_PATH), and sound files from ./wav/ (or SOUNDS_DIR).

5. Configure your SIP phone

Point your SIP client at your server's IP on port 5060 (UDP). The static router does not perform authentication, so any SIP client can connect — just dial the extension number you configured.

Example Oink (or any softphone) setup:

  • SIP Server: your.server.ip
  • Port: 5060
  • Transport: UDP
  • Username/Password: anything (ignored by static router)

Dial 1000 (or whatever you put in dialplan.toml) and you should hear the bot join the Discord voice channel.

Environment variables reference

Variable Default Description
DISCORD_BOT_TOKEN (required) Discord bot token
SIP_PUBLIC_HOST (required) Public IP/hostname for SIP
SIP_PORT 5060 SIP listening port
RTP_PORT_START 10000 Start of RTP port range
RTP_PORT_END 15000 End of RTP port range
RTP_PUBLIC_IP (auto-detected) Public IPv4 address for RTP media; set to override discovery
CONFIG_PATH ./config.toml Path to config.toml
DIALPLAN_PATH ./dialplan.toml Path to dialplan.toml
SOUNDS_DIR ./wav Path to sound files directory
DATA_DIR /var/lib/sipcord Persistent data directory
DEV_MODE false Enable dev mode logging
RUST_LOG sipcord_bridge=info,pjsip=warn Log level filter

NAT / Firewall notes

If your server is behind NAT, you need to:

  • Forward UDP port 5060 (SIP signaling)
  • Forward UDP ports 10000-15000 (RTP media)
  • Set SIP_PUBLIC_HOST to your public IP
  • Leave RTP_PUBLIC_IP unset to discover it from https://api.sipcord.net/ip at startup
  • Set RTP_PUBLIC_IP explicitly when the public IP for RTP differs from the bridge's outbound IPv4 address

Automatic discovery requires outbound HTTPS access and fails startup if the endpoint cannot return a valid IPv4 address.

For servers with both a public and private interface (e.g. behind a load balancer), you can set SIP_LOCAL_HOST and SIP_LOCAL_CIDR so local clients get the private IP in Contact headers:

SIP_LOCAL_HOST=192.168.1.100
SIP_LOCAL_CIDR=192.168.1.0/24

Fax support

The bridge can receive faxes (both G.711 passthrough and T.38 UDPTL). Received faxes are demodulated via SpanDSP and posted as PNG images to a Discord text channel. To set up fax, add a mapping with a text channel ID in your dialplan — the bridge routes faxes to text channels and voice calls to voice channels automatically.

Fax reception can continue for up to 60 minutes while making progress, with a five-minute inactivity timeout. G.711 fax calls use a 200 ms jitter prefetch and a one-second buffer, with progressive audio discard, silence suppression, and speech packet-loss concealment disabled.

Discord posts show at most the first 10 usable pages and include a truncation notice when more were received. Failed uploads retry twice (after two and five seconds) by editing the same message. If delivery still fails, the original TIFF and delivery.json context are retained under ${DATA_DIR}/failed-faxes/<session>/ for manual recovery. If that archive cannot be written, the original fax temp directory is kept and its path is logged. Retained files are not automatically deleted or retried after a restart.

Acknowledgements

License

Code is AGPLv3

Dusthillguy track is whatever dusthillguy wishes