A small, fast Linux service that receives unicast UDP on one interface and re-emits the payloads as multicast UDP out one or more different NICs. Built for broadcast plant / stadium / AV-over-IP work where a contribution feed lands on one network and needs to fan out onto isolated multicast distribution VLANs without dragging in a full media server.
- One worker thread per stream, blocking sockets, zero per-packet allocation. The async runtime (tokio) is only used for the web control plane — it never touches the hot path.
- Multi-NIC fan-out — each stream can egress on one or many NICs simultaneously. You can even loop multicast back out the ingress interface.
- Egress NIC pinned by interface name via
IP_MULTICAST_IF. No reliance on routing-table tricks; the packets leave the NIC you named or the worker fails to start. - Tunable kernel socket buffers (
SO_RCVBUF/SO_SNDBUF) for burst tolerance. - TOML config (hand-editable) plus a web UI at
:8080for add / edit / delete / toggle and a 1 Hz live stats view. - Multicast loopback is disabled on egress sockets so the listener never hears itself.
# Build (requires Rust 1.75+)
cargo build --release
# Run (creates config.toml on first launch)
./target/release/udpcaster
# Open the web UI
open http://localhost:8080Requires Rust 1.75+ (rustup default stable).
cargo build --release
# Binary: target/release/udpcasterdocker build -t udpcaster .
docker run --rm --net=host -v ./config.toml:/app/config.toml udpcaster--net=host is required so the container sees the host's NICs and multicast
works correctly.
# 1. Create the service user (no shell, no home).
sudo useradd --system --no-create-home --shell /usr/sbin/nologin udpcaster
# 2. Lay out the install directories.
sudo mkdir -p /opt/udpcaster /etc/udpcaster
sudo cp target/release/udpcaster /opt/udpcaster/
sudo cp -r static /opt/udpcaster/
sudo chown -R udpcaster:udpcaster /opt/udpcaster /etc/udpcaster
# 3. Drop in the unit file and start.
sudo cp udpcaster.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now udpcaster
sudo systemctl status udpcasterThe web UI is now at http://<server>:8080. The config file lives at
/etc/udpcaster/config.toml and is created on first run if missing.
Usage: udpcaster [OPTIONS]
Options:
-c, --config <PATH> Path to TOML config file [default: config.toml]
--web-bind <ADDR> Bind address for the web UI [default: 0.0.0.0:8080]
-h, --help Print help
-V, --version Print version
The rcvbuf_bytes / sndbuf_bytes values in each stream are silently
capped by the kernel limits. Bump them once, system-wide:
# /etc/sysctl.d/99-udpcaster.conf
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.core.rmem_default = 33554432
net.core.wmem_default = 33554432
net.core.netdev_max_backlog = 5000sudo sysctl --systemIf you intend to forward multicast off the local subnet, also enable
multicast routing on the egress NIC (smcrouted, pimd, etc.) — this
relay only sends; it does not route.
Either click + New Stream in the web UI, or hand-edit the config file:
[[streams]]
id = "cam1"
name = "Field Camera 1 -> Video Wall"
enabled = true
# Where the unicast feed arrives.
listen_addr = "10.20.0.5" # IP of the ingress NIC, or 0.0.0.0
listen_port = 5004
# Where to fan it out.
multicast_group = "239.10.20.30"
multicast_port = 5004
egress_interfaces = ["eth1", "eth2"] # one or more NIC names
multicast_ttl = 1 # 1 = stay on the local subnet
# Burst headroom (capped by kernel sysctl).
rcvbuf_bytes = 8388608 # 8 MiB
sndbuf_bytes = 8388608After a hand-edit, restart the service (sudo systemctl restart udpcaster).
Edits made through the web UI are applied live with no restart.
On the relay host, watch the counters in the UI, or:
# See the listener bound:
ss -uap | grep udpcaster
# Confirm packets land on the egress NIC:
sudo tcpdump -i eth1 -n host 239.10.20.30On a downstream multicast receiver:
# Quick smoke test with socat:
socat -u UDP4-RECV:5004,ip-add-membership=239.10.20.30:eth0 -- One ingress port per stream. Two streams cannot share the same
(listen_addr, listen_port)pair — the second worker will fail to bind. - Disable a stream before changing its ports to avoid a brief bind-conflict during reconcile.
- Stats are best-effort counters (
Relaxedatomics). They are exact enough for health monitoring, not for billing. - MTU: this is a pure passthrough — whatever datagram size arrives is what goes out. Make sure both NICs and any switches in between agree on jumbo frames if you use them.
- Security: the web UI has no auth. Put it behind a reverse proxy with
basic auth, or bind it to
127.0.0.1and SSH-tunnel, before exposing it on anything that isn't a trusted management VLAN.
| Symptom | Likely cause |
|---|---|
Worker fails to start: unknown egress interface |
NIC name typo; check ip -br link. |
| Worker starts, counters increment, no packets on the wire | Egress NIC has no IPv4 address — multicast needs a source IP. Assign one even if it's just a /31. |
| RX packets climb but TX errors climb too | sndbuf capped by wmem_max; raise the sysctl. |
| Receivers see nothing despite tcpdump showing the packets | IGMP snooping on the switch with no querier — enable an IGMP querier on that VLAN. |
MIT