fixes to bans

This commit is contained in:
coral 2026-04-12 12:28:53 -07:00
parent 5b918355cc
commit 65d7538027
2 changed files with 148 additions and 35 deletions

136
README.md
View file

@ -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
Dusthillguy track is whatever dusthillguy wishes

View file

@ -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