UDP NAT Hole Punching handling
Hi,
I'm working on handling UDP NAT hole punching for using RTCP and RTP over UDP inside docker without host networking. The problem here is that source UDP ports from publisher and reader are different than those announced in the SETUP request. It makes clientData matching mechanism in UDP listeners impossible to find client addr in the map. I have developed a "dirty" solution based on modifying the library. Any new incoming UDP frame is used to modify u.clients structure in serverudpl.go and also is modifying setuppedTracks udpRTPPort and udpRTPPort in setuppedTracks. The problem is that choice if the user wants to perform such operations should be made on the library consumer layer.
I did my first try of moving unknown frame handling to serverhandler #32. However, it still requires making a lot of struct fields public to make it possible to modify established udp ports. Now I'm thinking of creating a method modifyClient (basing on add/removeClient) in serverudpl.go but there is an issue with mutexes with calling handler inside run() causes u.clientsMutex.Lock().
This approach also requires a separate method in serversession.go named ChangeTrackAddr() to modify track udpPort.
@aler9 What do you think of such an approach, do you have any hints or maybe an idea of how to do it better and cleaner?
Hello, to add additional source ports it's enough to add additional entries to u.clients. It's useless to touch udpRTPPort and udpRTPPort, since they are used only to call addClient.
The question is how to distinguish a publisher from another. There is another piece of data in RTP packets that allows to distinguish between clients and grants a minimum of security, and it's the SSRC, but it needs to be set correctly during SETUP by the publisher, and not all publishers do it correctly.
Therefore, a way match clients and packets could be
- extract a track SSRC from the Transport header in SETUP and save it in
setuppedTracks - compare the SSRC of each incoming frame with saved SSRCs, and pull session accordingly
To overcome NAT on the receiver side it is also required to change udpRTPPort. In WriteFrame every RTP frame is sent to Port: track.udpRTPPort. So modifying u.clients is required to receive frames (RTP/RTCP from publisher and RTCP from readers). Changing udpRTPPort is required to send RTP frames to receivers. Changing udpRTCPPort is required to send RTCP reports back to clients.
Distinguishing a publisher from a reader is tricky and rather not possible in the general scenario. But when library user has control over Publisher and Receiver implementation it is easy to analyze incoming frames payloads and determine their source. For example in my case, I am using FFmpeg-based clients so UDP Hole punching frames are defined in ff_rtp_send_punch_packets function in FFmpeg sources. I need to check if SSRC bytes can be filled in those frames without modifying FFmpeg. For now, I'm observing dummy, minimal hole punching frames:
[128 0 0 0 0 0 0 0 0 0 0 0] RTP 12 and [128 201 0 1 0 0 0 0 0 0 0 0] RTCP 8
RTCP and RTP frames can be distinguished using rtcpReceiver.ProcessFrame. The problem arises when multiple readers are allowed and connecting simultaneously. Inside docker, each remote connection is from the same "docker gateway" IP address so it is impossible to distinguish them. In my application number of clients is limited so blocking new connections before UDP matching on the previous one is done is the solution.
To overcome NAT on the receiver side it is also required to change udpRTPPort
To overcome NAT you're right, while if you simply want to overcome Docker's isolation, this is not necessary, you can send packets to the declared ports and they'll get delivered