Build1 publisher3 min readPublished
WebRTC on a NATed cloud VM advertises its private address
Signaling rides TCP 443 through the infrastructure you already configured. The media is UDP that bypasses the proxy entirely, and on a NATed cloud VM the server advertises a private address until you configure the public one yourself.
The Engineer · Build desk

What happened
- In a dev.to writeup of a streaming server deployment, signaling runs as a WebSocket on TCP 443 that the reverse proxy terminates, while the media is RTP over UDP that goes browser-to-server, bypassing proxy, CDN and tunnel.
- After configuring the public IP explicitly through Pion's setting engine, the author reports a usable candidate pair with a 7ms round trip and a steady 30fps of decoded frames with zero freezes over six seconds.
- The provider security group in front of the instance defaults to closed, and on that VM port 22 was the only one open out of the box, leaving the UDP media range outside the 443 rule that made HTTPS work.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Ruling out the web stack also rules out the tools you know how to use on it: proxy logs, certificate checks and CDN dashboards have no view of the media path.
- decision Anyone shipping a media server to a cloud instance has to choose STUN or an explicitly configured public address before launch, and the choice turns on what the provider's NAT does to ports.
- cost The failure is silent at every layer that logs, so the debugging hours go into the part of the stack that already works; the author says all of it took longer to find than it should have.
- exposure Getting media through means opening a UDP port range on the provider firewall, a rule that sits outside anything an HTTPS tutorial covers.
The candidate list in the SDP describes the media path [4], and on a cloud VM the address the machine honestly reports is the wrong one to advertise. The instance's NIC held a 172.31.x.x address while the public IP lived in the provider's NAT layer in front of it, so the machine had no way to know its own public address [5]. Gathering can only enumerate the addresses on the NIC. Signaling completes, the ICE checks go out to an address nobody on the internet can route to, and nothing in any log says why [6].
The post gives two fixes and says plainly that they are not equivalent [7]. With STUN, the server asks a public STUN server what address the request appeared to come from, and advertises that answer [8]. That holds until the NAT in front of the VM is the kind that assigns a different external port per destination. Then the port you advertise and the port that reaches you differ, and the checks fail again with no explanation [9].
The other fix needs the provider to offer a one-to-one mapping, public IP forwarding straight to the instance on the same ports [10]. In Pion it is one setting engine call, made once before the API is built: `se.SetNAT1To1IPs([]string{opts.PublicIP}, webrtc.ICECandidateTypeHost)`, with the engine then handed to `webrtc.NewAPI` [11]. The advertised candidate then holds regardless of what the NAT is doing [10].
The author took the second route and reports the difference on the wire. Before the change, the checks never completed; after it, a usable pair with a 7ms round trip and a steady 30fps of decoded frames with zero freezes over a six-second test [12]. Six seconds at 30fps is about 180 frames [1]. The post does not name the cloud provider. For that result to transfer you need the same one-to-one mapping from your own provider, the UDP media range open in front of the instance, and a client whose path to the host candidate actually completes a check. 180 frames shows the pair works; loss recovery and behaviour on a contended uplink need a longer run.
A cloud instance has two firewalls. The one on the machine is the one people check: ufw, iptables, firewalld [14]. The provider security group sits in front of the instance, and it decides whether a packet arrives [14]. It defaults to closed, and on the VM in question exactly one port was open out of the box: 22 [15]. You can log in. ICE still fails. Opening 443, getting a certificate and watching the site load finishes the TCP half of the job; the 443 rule covers TCP, and the media ports are UDP in a different range [16].
No proxy setting closes that gap, and the author's point is that a WebSocket is not a suitable transport for a 30fps media stream and no proxy is going to turn into one [20]. His rule is a configuration rule: "the fact that your server is behind NAT is not a detail, it is a first-class configuration input you have to handle deliberately," he wrote [17].
What to watch
- A longer run than six seconds from the same instance, with loss and bitrate figures, would show whether the host-candidate fix holds under load.
- Naming the provider and the NAT behaviour behind that 172.31 instance would settle whether STUN alone would have worked there.
- A Pion release that gathers a provider's public address by default would retire the manual SetNAT1To1IPs step.