- Python 97.9%
- Shell 1.5%
- Makefile 0.6%
Untrack Claude Code config files to keep them local-only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|---|---|---|
| config | ||
| docker | ||
| docs/img | ||
| nginx | ||
| scripts | ||
| systemd | ||
| webui | ||
| .gitignore | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
P25 Trunked Radio Streaming Platform
A secure, open-source stack for monitoring and streaming unencrypted APCO P25 trunked radio (Phase I and Phase II).
Pipeline: Trunk Recorder -> Liquidsoap -> Icecast -> Nginx (TLS) -> Cloudflare (edge) -> Listeners.
This repository includes:
- Turnkey deployment script (
scripts/tcern_deploy.sh) - Docker-based streaming services (Liquidsoap and Icecast)
- Nginx reverse proxy with TLS integration
- Cloudflare real IP restoration and origin allowlist firewall
- Talkgroup routing and All-Calls exclusions
- Web-based TG management UI (Flask)
- Validation and suggestion tools for talkgroups and mappings
Legal note: Monitor only unencrypted traffic. Comply with all applicable laws, regulations, and system policies.
Features
- P25 decoding and trunk tracking (Phase I and Phase II) using Trunk Recorder
- Multiple listener streams (All-Calls, Fire, EMS) via Liquidsoap and Icecast
- File-based router for TG to queue classification and All-Calls exclusions
- Nginx reverse proxy with either Cloudflare Origin Certificate or Let’s Encrypt DNS-01 at origin
- Cloudflare real IP restoration and daily refresh of Cloudflare networks
- Firewall allowlist so only Cloudflare can reach ports 80 and 443 on the origin
- Web UI to add or update TG mappings, exclusions, and talkgroup labels
- Validation warnings for TGs that are excluded and unmapped
- Daily suggestions from recent call logs to help build out talkgroups and mappings
Architecture
End-to-end flow:
- SDR hardware (RTL-SDR or KrakenSDR)
- Trunk Recorder (control channel tracking and voice decode)
- File-based router (copies decoded calls into stream queues by TGID)
- Liquidsoap (watches queue folders and produces streams)
- Icecast (serves streams internally)
- Nginx (HTTPS reverse proxy)
- Cloudflare (TLS, DDoS, and edge access)
- Listeners (VLC, browser, apps)
See docs/img/p25_system_diagram.mmd and docs/img/p25_system_diagram.png.
Repository Structure
p25-streaming-platform/
├─ README.md
├─ DEPLOYMENT.md # Optional: if you include the separate deployment guide
├─ docker/
│ ├─ docker-compose.yml
│ └─ liquidsoap/
│ ├─ tcern-all.liq
│ ├─ tcern-fire.liq
│ └─ tcern-ems.liq
├─ nginx/
│ ├─ icecast-proxy.conf
│ └─ icecast-proxy-ssl.conf
├─ systemd/
│ ├─ trunk-recorder@.service
│ ├─ streams-compose.service
│ ├─ tgui.service
│ ├─ purge-recordings.service
│ ├─ purge-recordings.timer
│ ├─ cf-realip-update.service
│ ├─ cf-realip-update.timer
│ ├─ cf-ipsets-update.service
│ └─ cf-ipsets-update.timer
├─ scripts/
│ ├─ tcern_deploy.sh
│ ├─ encode_upload.sh
│ ├─ purge_recordings.sh
│ ├─ validate_tg_config.py
│ └─ gen_tg_suggestions.py
├─ config/
│ ├─ talkgroups.csv
│ ├─ tg_map_fire_ems.csv
│ ├─ tg_exclude_all.csv
│ ├─ suggestions/ # populated by gen_tg_suggestions.py
│ └─ examples/
│ ├─ cloudflare.ini.template
│ └─ .env.example
├─ webui/
│ ├─ app.py
│ └─ requirements.txt
└─ docs/
└─ img/
├─ p25_system_diagram.mmd
└─ p25_system_diagram.png
Prerequisites
- Ubuntu Server 22.04 or newer (root or sudo)
- SDR hardware: RTL-SDR or KrakenSDR
- DNS name proxied by Cloudflare (orange cloud enabled)
- TLS at origin: choose Cloudflare Origin Certificate or Let’s Encrypt DNS-01 via certbot-dns-cloudflare
- Outbound internet access for package and container downloads
Quick Start
Unzip or clone this repository onto your Ubuntu host and run the unified installer.
Cloudflare Origin Certificate at origin (recommended for Cloudflare-only access):
sudo -i
./scripts/tcern_deploy.sh \
--user <linux_user> \
--server your.domain \
--tls origin \
--cc 772106250 \
--retention-days 3
Let’s Encrypt at origin using DNS-01 (requires Cloudflare token file with Zone:DNS:Edit):
sudo mkdir -p /etc/letsencrypt
sudo sh -c 'echo "dns_cloudflare_api_token = <YOUR_TOKEN>" > /etc/letsencrypt/cloudflare.ini'
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
sudo -i
./scripts/tcern_deploy.sh \
--user <linux_user> \
--server your.domain \
--tls letsencrypt-dns \
--cf-ini /etc/letsencrypt/cloudflare.ini \
--cc 772106250 \
--retention-days 3
After install:
- Streams (example mounts):
- Status: https://your.domain/status.xsl
- Admin: https://your.domain/admin (basic auth created at install)
If using Cloudflare Origin Certificate, upload the cert and key to:
/etc/nginx/ssl/origin.crt
/etc/nginx/ssl/origin.key
Then run: nginx -t && systemctl reload nginx and set Cloudflare SSL to Full (strict).
Web UI (Talkgroup Management)
The management UI runs on 127.0.0.1:8085 and is exposed at /tg through Nginx with the same basic auth as /admin.
Enable UI:
pip3 install -r webui/requirements.txt
sudo mkdir -p /opt/radio/tgui
sudo cp webui/app.py /opt/radio/tgui/
sudo systemctl enable --now tgui
sudo nginx -t && sudo systemctl reload nginx
Open: https://your.domain/tg
Use the UI to:
- Add or update TG to queue mappings (for example, map 50521 to fire)
- Add or remove All-Calls exclusions
- Edit talkgroups.csv (AlphaTag, Description, Priority)
- View unknown TGIDs from recent call log
- See warnings if a TG is excluded and unmapped
Configuration
Key files:
config/talkgroups.csv
Catalog of talkgroups and labels. Example: Decimal,AlphaTag,Description,Priority 50521,FireDisp,County Fire Dispatch,1 50560,EMSDisp,County EMS Dispatch,1config/tg_map_fire_ems.csv
Talkgroup to queue mapping. Example: 50521,fire 50560,emsconfig/tg_exclude_all.csv
TGIDs to remove from the All-Calls stream only. Per-queue streams are not affected. #50900 #50850
Trunk Recorder configuration is at /opt/radio/config/config.json after install. Update:
control_channelslist in Hzphase2true or false based on system- optional
wacnandsysidif known
Validation and Suggestions
-
Batch validation:
python3 scripts/validate_tg_config.pyReports TGs that are excluded and unmapped, and mapped TGs missing from talkgroups.csv.
-
Suggestions from call logs:
python3 scripts/gen_tg_suggestions.py ls -l config/suggestions/Produces CSV of unknown TGs with estimated counts to prioritize labeling and mapping.
Operations
Common services:
systemctl status trunk-recorder@<linux_user>
systemctl status streams-compose
systemctl status tgui
systemctl status cf-realip-update.timer
systemctl status cf-ipsets-update.timer
systemctl status purge-recordings.timer
Update Dockerized services:
cd /opt/streams
docker compose pull
docker compose up -d
Update Trunk Recorder:
cd /opt/trunk-recorder
git pull
./install.sh
systemctl restart trunk-recorder@<linux_user>
Retention:
scripts/purge_recordings.shdeletes older recordings daily based on--retention-days.
Security Model
- Nginx is the only public ingress; Icecast is internal only.
- Cloudflare terminates client TLS and filters traffic.
- Firewall allowlist permits ports 80 and 443 only from Cloudflare IP ranges; SSH is limited to your allowlist.
- Nginx basic auth protects
/adminand/tg. - Cloudflare IP ranges and real IP include file are refreshed daily.
Recommendations:
- Keep Cloudflare DNS proxied (orange cloud).
- For Origin CA at origin, set Cloudflare SSL to Full (strict).
- Rotate admin and Icecast passwords periodically.
- Consider external secret management for production environments.
Troubleshooting
No audio in streams:
- Check Trunk Recorder logs: journalctl -u trunk-recorder@<linux_user> -f
- Verify control channel frequency and SDR gain/ppm.
- Confirm new call files appear in
/opt/streams/media/<queue>.
Streams do not play:
- Verify Cloudflare orange cloud is on.
nginx -t && systemctl restart nginxiptables -Sandipset list cf_ipv4to confirm firewall and allowlist.
Mermaid diagram errors:
- Use
docs/img/p25_system_diagram.mmd(ASCII-only, no HTML entities).
Contributing
- Open issues with exact OS version, SDR hardware, and logs.
- Do not commit secrets. Use the templates in
config/examples/.
License
MIT License. See LICENSE.
Acknowledgements
- Trunk Recorder
- Liquidsoap and Savonet
- Icecast
- Nginx
- Cloudflare
All are well-supported open-source or free components widely used in production environments.