Clojure Zippers - Part One
I started working on a replacement for the existing clojure.zip library with a couple
Running the UniFi Controller docker image with macvlan networking solves problems.

By enabling the macvlan driver for Docker you can have each container appear as a layer two (Ethernet) device on your network. This provides extreme network isolation between containers and between containers and the host. Given the broad range of ports[1] required to be exposed and the attraction of Layer 2 adoption of new devices, it's particularly helpful to leverage macvlan for the UniFi controller container.
Note that the macvlan driver must be connected to a physical network adapter that supports promiscuous mode. The Synology RS1619xs+ Ethernet adapters meet this requirement as do most modern adapters. Note also that macvlan support is limtied to Linux hosts[2] -Synology NASs qualify.
Unfortunately it's not possible to create the macvlan network from the Synology GUI so we resort to using the command line. Here we create a docker virtual LAN to which containers can be attached and which is bridged to the physical network of the host:
docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 --ip-range=192.168.1.160/27 --aux-address 'host=192.168.1.160' --opt parent=bond0 mac0
(These commands need to be run as root. sudo is one approach.)
Explanation of parameters:
-d macvlan : create a network using the macvlan driver.--subnet=192.168.1.0/24 : define the physical layer 3 network in front of the bridge.--gateway=192.168.1.1 : define the appropriate gateway for the physical network in front of the bridge.--ip-range=192.168.1.160/27 : define the range of (layer 3) IP addresses that will be found behind the macvlan bridge on the virtual LAN. This effectively provides a pool of addresses that might be assigned dynamically to attached containers by the docker host[3], or statically via parameters when launching containers. You might find this tool useful for selecting the pool.--aux-address 'host=192.168.1.160' : reserve an IP address from the pool. Later in this tutorial the aux-address is used to provide an interface on the virtual LAN for the docker host itself.--opt parent=bond0 : define the physical interface of the host that will be bridged to the virtual LAN. In this example we're using a bonded interface but in the simplest scenario eth0 is a likely choice.mac0 : name the docker networkAt this point the Docker network is created and you can see it in the GUI. You can also see the created network:
docker network ls
References:
The default bridging enabled by the macvlan driver does not create the additional infrastructure required to allow the host to communicate with the virtual LAN. Logic would imply that if the host can communicate with the physical network (192.168.1.0/24 in the example) and containers attached to the virtual LAN (192.168.1.176/27 in the example) can also communicate with the physical network (that's the whole point of this exercise) then the host itself should be able to communicate wih containers attached to the virtual LAN. But that behavior is not supported out of the box. Here we create the infrastructure to plug the gap. These four commands need to be run once, ideally during boot up of the host:
ip link add macvlan0 address 7a:4f:97:de:cc:cc link bond0 type macvlan mode bridge
This creates a virtual interface device macvlan0 subordinate to the "physical" interface bond0. At this point the MAC address is assigned to the virtual interface (see it with ip link show macvlan0) and we hard-code it to avoid having it change at every reboot.
ip addr add 192.168.1.160/32 dev macvlan0
This assigns a static IP address to the virtual interface device macvlan0 (see it with ip addr show macvlan0).
ip link set macvlan0 up
This brings the macvlan0 virtual interface device up. You should be able to ping the interface from the host: ping 192.168.1.160
ip route add 192.168.1.160/27 dev macvlan0
This creates a route to the virtual LAN (192.168.1.160/27) via the virtual interface device macvlan0 (see it with ip route show).
These commands are not permanent. To make them permanent on the Synology NAS, create a "Triggered Task" that executes a "User-defined script" as root in response to the "Boot-up" event. The task (a bash script) should look something like this:
while ! ip link show bond0 | grep -q ‘state UP’; do
sleep 1
done
ip link add macvlan0 address 7a:4f:97:de:cc:cc link bond0 type macvlan mode bridge
ip addr add 192.168.1.160/32 dev macvlan0
ip link set macvlan0 up
ip route add 192.168.1.160/27 dev macvlan0
Now that the networking infrastructure is in place, we can bind the Docker container to our mac0 network. You can do this from the Docker GUI since the mac0 network is visible and available for binding, but there is a big limitation in doing it this way: the IP address assigned to the container by the docker host is not guaranteed to be consistent. Until Synology supports macvlan networks overtly the best solution is to create the container manually. There are serveral good candidates[4] but I've settled on jacobalberty/unifi due to its freshness and configurability. We're going to exploit this configurability to tune the container for running as the sole "tenant" of its virtual LAN host.
After using the Synology GUI to pull the latest image, here we create a container:
docker create --name=UniFiController --net=mac0 --ip=192.168.1.161 -it -e BIND_PRIV=true -e UNIFI_HTTP_PORT=80 -e UNIFI_HTTPS_PORT=443 -e TZ=America/New_York -e RUNAS_UID0=false -v /volume1/docker/Unifi:/unifi jacobalberty/unifi:latest
Explanation of parameters:
--name UniFiController : name the container.--net=mac0 : bind the container to the virtual LAN.--ip=192.168.1.161 : assign the IP address of the virtual interface device.-it : keep STDIN open and allocate a psuedo-TTY.-e BIND_PRIV=true : we need this special privilge to bind to a port less than 1024.-e UNIFI_HTTP_PORT=80 : this image uses this var to configure the UniFi controller to control the HTTP port.-e UNIFI_HTTPS_PORT=443 : this image uses this var to configure the UniFi controller to control the HTTPS port.-e TZ=America/New_York : set the timezone of the container process.-e RUNAS_UID0=false : observe the principal of least privilege and run as a normal user.-v /docker/UniFi:/unifi : bind mount the /docker/UniFi share folder to /unifi in the container (the default location).jacobalberty/unifi:latest : create the container from the latest version of the image.You can tweak some of these parameters from within the Synology Docker GUI. Notice, however, the lack of port mapping for docker: by using the macvlan network instead of host networking we avoid the whole class of port conflicts.
Finally, run your container. The GUI works just fine for this. You can now browse to the assigned IP address and default port (https://192.168.1.161 in this example).
References:
We've created a macvlan virtual network and adapter for Docker, created a virtual interface on the Docker host connecting to the virtual network and launched/configured a Docker container to live on that virtual network. At this point, Docker containers gain a whole new axis of isolation from the host and the port conflicts and mapping hassle typically associated with host networking are gone.
There are several areas that could stand improvement:
macvlan networking support directly in the Synology GUI. This would ease the friction in running containers and provide a single point of administration/monitoring. Currently, for example, there is no way in the GUI to see the IP address assigned to a container bound to the macvlan network. (on Synology)There are permissions issues with Synology's Docker implementation which make creating the container and creating the bind mount from the command line problematic. I finally worked around this issue by creating a throw-away container that created the bind mount point with the GUI. Then I deleted that container and created my "final" container using the command line. So the GUI-created container was a kind of surrogate mother to give birth to the mount point...
This blog post draws greatly from uᴉʍpǝ ɯɐ ᴉ and The Odd Bit.
Per the UniFi documentation. ↩︎
Per the docker docs. ↩︎
Sadly, a DHCP client is not (yet) officially supported. ↩︎
Consider also linuxserver/unifi-controller. ↩︎