Forwarding large RTP port ranges to Freeswitch in Docker

tl;dr If you run Freeswitch in Docker and need to forward a RTP port range, you'll run into problems. You can try using the iptable solution explained here. (Exactly the kind of manual setup mess that container deployment is there to solve.)

How do you forward a large number of ports to a container? You will ask yourself this question for example if you run Freeswitch as a SIP/RTP server using Docker, because somehow you need to get your RTP audio packets audio from the host through the Docker network to your container. I've struggled with various issues. Here are my notes.

The initial problem

Let's say you want to potentially use ports 16384 all through 25000 for RTP. In docker-compose, that's one line in the ports section: - "16384-25000:16384-25000". Easy! (But don't do it!)

Currently, if you start a container like that on your laptop, your computer will likely freeze very soon. For each individual port that's being connected between host and container, Docker starts one “userland proxy” process. With ps, you'll see tons of docker-proxy processes. To be precise, you'll see one per port, which means in the example above, you'll have 8000+ processes forwarding one port each.

And so your computer first runs out of free physical memory and then of swap memory, too. Game over.

Using the host network

A simple and fast-to-do solution is for the Freeswitch container to use the host-network.


This partially defeats the purpose of using Docker, though. As the Docker man page suggests:

“Note: the host mode gives the container full access to local system services such as D-bus and is therefore considered insecure.”

Does not sound that great, doesn't it. Let's look for other ways.

NAT hairpinning / Disabling the user-land proxy

What's the userland proxy, anyway? Introduced in 0.6.0, it solved the issue of containers on the same host communicating with each other and seeing each other as separate hosts on the virtual network. (netfilter support was not far enough yet.)

As we can see, having one process per port mapping only works for small mappings, so they are trying to make it one userland proxy per daemon, but that change is not there yet. They tried removing it as the default, but that's not easy, because the used NAT localhost hairpinning is only supported on kernels > 2.6, does not work on MacOS, and does not support IPv6. Even on a new Linux kernel and using only IPv4, the proxy might still be necessary because there is an elusive kernel bug, which hopefully dies in 4.16.

At least, since February 2016 (version 2.17) you can try disabling the userland-proxy. If you're on Linux, using only IPv4, then it may work for you. (If you run into the Kernel bug, you'll need to restart the machine.) Hint: this solution is not great either.

Fire up your editor, create or open /etc/docker/daemon.json, and make sure you've the following config entry in there:

        "userland-proxy": false

This configuration makes Docker configure iptable rules to do this forwarding. As it turns out, when starting a container, this solution no longer uses up all your memory, yey! But, you'll die of old age instead. Allocating the port ranges takes forever. (Half an hour to start one container. Does that seem right to you? Oh, and your Docker daemon will not be responding in that time.)

If you look at the iptables in the meantime though, you'll get a small heart attack because Docker creates one rule per port. But, I hear you scream in agony, iptable rules support port ranges! Right you are! And that points us to the next solution (which works for us at engageSPARK and may actually work for you).

Manual hairpinning

The sad truth is: Docker does not currently support large port ranges. It tries. And fails. The people at bettervoice had the same problem with their Freeswitch container and described their solution. So, as the bettervoice team recommends, let's create the port forwarding manually (as root):

iptables -A DOCKER -t nat -p udp -m udp ! -i docker0 --dport 16384:25000 -j DNAT --to-destination $CIP:16384-25000
iptables -A DOCKER -p udp -m udp -d $CIP/32 ! -i docker0 -o docker0 --dport 16384:25000 -j ACCEPT
iptables -A POSTROUTING -t nat -p udp -m udp -s $CIP/32 -d $CIP/32 --dport 16384:25000 -j MASQUERADE

If you're curious, what MASQUERADE does, read the description of this issue. CIP is the IP address of the container; you can get it with docker inspect.

The final solution

If none of this works, the final solution is the most simple of all: Don't use Docker. Run Freeswitch natively on a dedicated machine and be done with it.

Docker makes deployment easier! You don't need a sysadmin anymore! Any dev can deploy now … until they can't. Another instance where it's very visible that Docker actually adds another technology layer on top, which makes the overall system more complex, not less.