mirror of
https://github.com/coral/sipcord-bridge.git
synced 2026-04-13 04:42:43 -06:00
fixes to bans
This commit is contained in:
parent
5b918355cc
commit
65d7538027
136
README.md
136
README.md
|
|
@ -20,6 +20,139 @@ This was written a mix between myself and claude, sure, some of it's big slop bu
|
||||||
|
|
||||||
**PR's welcome**. No really, feel free to implement it and contribute.
|
**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=36700160
|
||||||
|
```
|
||||||
|
The bot needs 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:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[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.
|
||||||
|
|
||||||
|
### 4a. Run with Docker (recommended)
|
||||||
|
|
||||||
|
Create a directory for your deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir sipcord && cd sipcord
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DISCORD_BOT_TOKEN=your_bot_token_here
|
||||||
|
SIP_PUBLIC_HOST=your.server.ip.or.hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
sipcord-bridge:
|
||||||
|
image: ghcr.io/coral/sipcord-bridge:latest
|
||||||
|
container_name: sipcord-bridge
|
||||||
|
restart: always
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --release -p sipcord-bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
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` | *(same as SIP_PUBLIC_HOST)* | Public IP for RTP media (if different from SIP) |
|
||||||
|
| `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
|
||||||
|
- If the public IP for RTP differs from SIP, also set `RTP_PUBLIC_IP`
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```env
|
||||||
|
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.
|
||||||
|
|
||||||
### Acknowledgements
|
### Acknowledgements
|
||||||
|
|
||||||
- Thanks to [dusthillguy](https://dusthillguy-music-blog1.tumblr.com/) for letting me use the song [*"Joona Kouvolalainen buttermilk"*](https://www.youtube.com/watch?v=IK1ydvw3xkU) as hold music.
|
- Thanks to [dusthillguy](https://dusthillguy-music-blog1.tumblr.com/) for letting me use the song [*"Joona Kouvolalainen buttermilk"*](https://www.youtube.com/watch?v=IK1ydvw3xkU) as hold music.
|
||||||
|
|
@ -29,4 +162,5 @@ This was written a mix between myself and claude, sure, some of it's big slop bu
|
||||||
### License
|
### License
|
||||||
|
|
||||||
Code is AGPLv3
|
Code is AGPLv3
|
||||||
Dusthillguy track is whatever dusthillguy wishe
|
|
||||||
|
Dusthillguy track is whatever dusthillguy wishes
|
||||||
|
|
@ -471,50 +471,29 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension-length ban checks use config values
|
// Extension-length ban checks use config values
|
||||||
|
// Both long and suspicious extensions use progressive timeouts (no permabans)
|
||||||
if let Some(ban_mgr) = crate::services::ban::global() {
|
if let Some(ban_mgr) = crate::services::ban::global() {
|
||||||
let ext_len = extension.len();
|
let ext_len = extension.len();
|
||||||
let is_numeric = extension.chars().all(|c: char| c.is_ascii_digit());
|
let is_numeric = extension.chars().all(|c: char| c.is_ascii_digit());
|
||||||
|
|
||||||
// Check for very long extension (permaban, likely fraud)
|
// Check for invalid extension length (outside valid 1-5 digit range, all numeric)
|
||||||
if ext_len >= ban_mgr.permaban_extension_min_length() && is_numeric {
|
// Uses progressive timeouts - legitimate users recover, scanners escalate
|
||||||
|
if is_numeric && ext_len >= ban_mgr.suspicious_extension_min_length() {
|
||||||
if let Some(ip) = source_ip {
|
if let Some(ip) = source_ip {
|
||||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
||||||
let result = ban_mgr.record_permanent_ban(ip, "very_long_extension");
|
let reason = if ext_len >= ban_mgr.permaban_extension_min_length() {
|
||||||
|
"very_long_extension"
|
||||||
|
} else {
|
||||||
|
"suspicious_extension"
|
||||||
|
};
|
||||||
|
let result = ban_mgr.record_offense(ip, reason);
|
||||||
if result.should_log {
|
if result.should_log {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"PERMABAN IP {} for very long extension: {} ({} digits, call {})",
|
"Timed out IP {} for {} extension: {} ({} digits, call {}, offense_level={}, timeout={}s)",
|
||||||
ip,
|
ip,
|
||||||
|
reason,
|
||||||
extension,
|
extension,
|
||||||
ext_len,
|
ext_len,
|
||||||
call_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tracing::warn!(
|
|
||||||
"Rejecting very long extension: {} ({} digits, call {})",
|
|
||||||
extension,
|
|
||||||
ext_len,
|
|
||||||
call_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
pjsua_call_hangup(*call_id, 404, ptr::null(), ptr::null());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for mid-length suspicious extension (progressive timeout)
|
|
||||||
if ext_len >= ban_mgr.suspicious_extension_min_length()
|
|
||||||
&& ext_len <= ban_mgr.suspicious_extension_max_length()
|
|
||||||
&& is_numeric
|
|
||||||
{
|
|
||||||
if let Some(ip) = source_ip {
|
|
||||||
if ban_mgr.is_enabled() && !ban_mgr.is_whitelisted(&ip) {
|
|
||||||
let result = ban_mgr.record_offense(ip, "suspicious_extension");
|
|
||||||
if result.should_log {
|
|
||||||
tracing::warn!(
|
|
||||||
"Timed out IP {} for suspicious extension: {} (call {}, offense_level={}, timeout={}s)",
|
|
||||||
ip,
|
|
||||||
extension,
|
|
||||||
call_id,
|
call_id,
|
||||||
result.offense_level,
|
result.offense_level,
|
||||||
result.timeout_secs
|
result.timeout_secs
|
||||||
|
|
@ -523,7 +502,7 @@ pub unsafe extern "C" fn on_incoming_call_cb(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Rejecting suspicious extension: {} ({} digits, call {})",
|
"Rejecting invalid extension: {} ({} digits, call {})",
|
||||||
extension,
|
extension,
|
||||||
ext_len,
|
ext_len,
|
||||||
call_id
|
call_id
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue