A project to use an rtl-sdr or krarkenRF to capture local P25 trunked system radio traffic and record/stream online.
  • Python 97.9%
  • Shell 1.5%
  • Makefile 0.6%
Find a file
Jeff Records ad01404042 chore: Add CLAUDE.md, claude.md, and .claude/ to .gitignore
Untrack Claude Code config files to keep them local-only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 12:32:12 -08:00
config initial file upload 2026-01-18 09:42:06 -08:00
docker initial file upload 2026-01-18 09:42:06 -08:00
docs/img initial file upload 2026-01-18 09:42:06 -08:00
nginx initial file upload 2026-01-18 09:42:06 -08:00
scripts initial file upload 2026-01-18 09:42:06 -08:00
systemd initial file upload 2026-01-18 09:42:06 -08:00
webui initial file upload 2026-01-18 09:42:06 -08:00
.gitignore chore: Add CLAUDE.md, claude.md, and .claude/ to .gitignore 2026-02-09 12:32:12 -08:00
LICENSE Create LICENSE 2026-01-18 09:35:19 -08:00
Makefile initial file upload 2026-01-18 09:42:06 -08:00
README.md Update README.md 2026-01-18 09:33:33 -08:00

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 Lets 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:

  1. SDR hardware (RTL-SDR or KrakenSDR)
  2. Trunk Recorder (control channel tracking and voice decode)
  3. File-based router (copies decoded calls into stream queues by TGID)
  4. Liquidsoap (watches queue folders and produces streams)
  5. Icecast (serves streams internally)
  6. Nginx (HTTPS reverse proxy)
  7. Cloudflare (TLS, DDoS, and edge access)
  8. 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 Lets 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

Lets 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:

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,1
  • config/tg_map_fire_ems.csv
    Talkgroup to queue mapping. Example: 50521,fire 50560,ems
  • config/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_channels list in Hz
  • phase2 true or false based on system
  • optional wacn and sysid if known

Validation and Suggestions

  • Batch validation:

    python3 scripts/validate_tg_config.py
    

    Reports 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.sh deletes 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 /admin and /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 nginx
  • iptables -S and ipset list cf_ipv4 to 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.