01
No head-of-line blocking
Loss on the agent's link drops one packet, which RTP and the codec already handle. It never delays the packets behind it the way UDP-inside-TCP does.
Many tunnels can't carry UDP at all, and the ones that can often wrap UDP inside TCP, so one lost packet freezes every call behind it. Wormhop sends each UDP packet as its own QUIC datagram. Signalling goes over an HTTPS tunnel with WebSockets, media over a reserved UDP port or port range.
$wormhop udp 31882 --remote-port 31882 # single-port SFU$wormhop udp 30000-30099 --remote-port 30000 # port-range SFU$wormhop http 7880 --subdomain rtc # signalling over wss://01
Loss on the agent's link drops one packet, which RTP and the codec already handle. It never delays the packets behind it the way UDP-inside-TCP does.
02
Reserve one UDP port for SFUs that multiplex (LiveKit's udp_port, Pion's ICE UDP mux, a mediasoup WebRtcServer) or up to 100 ports for SFUs that want a port per session.
03
With 5% packet loss injected on the agent's link, only 0.27% of RTP went missing after NACK recovery and the median round trip stayed at 1.3 ms.
04
If a network blocks UDP to the server, the agent says so and carries UDP over its TCP connection, so the call still connects.
01/Example: LiveKit
LiveKit multiplexes every call onto one UDP port, which makes it the simplest SFU to tunnel. Reserve a subdomain, UDP 31882 and TCP 21881 in the dashboard first.
Step 1
Same port numbers locally and on the edge, and no STUN lookup of its own address.
port: 7880 rtc: udp_port: 31882 # inside the server's UDP pool tcp_port: 21881 # ICE over TCP, inside the TCP pool use_external_ip: false
Step 2
export LIVEKIT_KEYS="devkey: $(openssl rand -hex 32)" livekit-server --config livekit.yaml \ --node-ip "$(dig +short tunnel.wormhop.dev | tail -1)"
Step 3
[tunnels.livekit-signal] proto = "http" addr = "7880" subdomain = "my-livekit" [tunnels.livekit-udp] proto = "udp" addr = "31882" remote_port = 31882 [tunnels.livekit-tcp] proto = "tcp" addr = "21881" remote_port = 21881
Step 4
Open meet.livekit.io, choose Custom, and enter wss://my-livekit.wormhop.dev with a token.
lk token create --api-key devkey --api-secret <secret> \ --join --room demo --identity alice --valid-for 24h
02/Example: StreamCore
StreamCore runs one mediasoup worker per port, so it takes a small range of UDP ports and a matching range of TCP ports for ICE over TCP. It is in early development; this is the network setup it uses.
Step 1
Four workers on UDP 31000-31003 and TCP 21000-21003, announcing the edge's IP.
HTTP_PORT=1188 RTC_WORKERS=4 RTC_START_PORT=31000 RTC_TCP_START_PORT=21000 RTC_LISTEN_IP=127.0.0.1 ANNOUNCED_IP=<dig +short tunnel.wormhop.dev>
Step 2
Reserve a 4-port UDP range at 31000 and a 4-port TCP range at 21000 first.
[tunnels.streamcore-api] proto = "http" addr = "1188" subdomain = "my-streamcore" [tunnels.streamcore-udp] proto = "udp" addr = "31000-31003" remote_port = 31000 [tunnels.streamcore-tcp] proto = "tcp" addr = "21000-21003" remote_port = 21000
Questions
Any SFU that lets you advertise an external IP and pin its UDP port: LiveKit (rtc.udp_port, --node-ip), StreamCore and other mediasoup servers (WebRtcServer with announcedAddress), Pion (SetICEUDPMux, SetNAT1To1IPs) and Janus (rtp_port_range, nat_1_1_mapping).
Not for clients that can reach UDP. Clients behind firewalls that block UDP still need TURN over TCP/TLS, or your SFU's ICE-TCP port exposed with a TCP tunnel.
UDP tunnels and reserved ports start on Hobbyist. Pro raises that to 100 reserved ports for port-range SFUs and 25 Mbit/s, plenty for developing and testing a few calls at a time. Wormhop is for development and personal use, not for hosting a production video service.