From 65d7538027be956979726ee184107ea7b9aa8e37 Mon Sep 17 00:00:00 2001 From: coral Date: Sun, 12 Apr 2026 12:28:53 -0700 Subject: [PATCH] fixes to bans --- README.md | 136 +++++++++++++++++- sipcord-bridge/src/transport/sip/callbacks.rs | 47 ++---- 2 files changed, 148 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index c844dfb..4c6f338 100644 --- a/README.md +++ b/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. +## 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 - 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 Code is AGPLv3 -Dusthillguy track is whatever dusthillguy wishe \ No newline at end of file + +Dusthillguy track is whatever dusthillguy wishes \ No newline at end of file diff --git a/sipcord-bridge/src/transport/sip/callbacks.rs b/sipcord-bridge/src/transport/sip/callbacks.rs index 84757c7..6000315 100644 --- a/sipcord-bridge/src/transport/sip/callbacks.rs +++ b/sipcord-bridge/src/transport/sip/callbacks.rs @@ -471,50 +471,29 @@ pub unsafe extern "C" fn on_incoming_call_cb( } // 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() { let ext_len = extension.len(); let is_numeric = extension.chars().all(|c: char| c.is_ascii_digit()); - // Check for very long extension (permaban, likely fraud) - if ext_len >= ban_mgr.permaban_extension_min_length() && is_numeric { + // Check for invalid extension length (outside valid 1-5 digit range, all 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 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 { tracing::warn!( - "PERMABAN IP {} for very long extension: {} ({} digits, call {})", + "Timed out IP {} for {} extension: {} ({} digits, call {}, offense_level={}, timeout={}s)", ip, + reason, extension, 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, result.offense_level, result.timeout_secs @@ -523,7 +502,7 @@ pub unsafe extern "C" fn on_incoming_call_cb( } } else { tracing::warn!( - "Rejecting suspicious extension: {} ({} digits, call {})", + "Rejecting invalid extension: {} ({} digits, call {})", extension, ext_len, call_id