Build a Docker IPv6 Network
Tags: docker, networking, ipv6
Published: 2021-08-26 19:31:22

6️⃣ Docker supports IPv6 addressing and IPv6 network builds.
🔇 But IPv6 is not enabled by default.
🔊 Here's how to turn on IPv6.
🧱 Plus how to build three different v6 networks; the default docker0 bridge network, a user-defined bridge network, and an IPvlan network with access to the public Internet.
🎬 TL;DR? Video link at the foot of the page 👇

## How to enable Docker IPv6 support

The official docs are [here](https://docs.docker.com/config/daemon/ipv6/), but I think they could have done a better job with this. Here's my version.

1. Create a JSON file `/etc/docker/daemon.json` and write to it a key of `ipv6` with a value of `true` and a key of `fixed-cidr-v6` with a value of your chosen IPv6 prefix. This prefix will be assigned to the default docker bridge network, `docker 0`.

    ```json
{
  "ipv6": true,
  "fixed-cidr-v6": "2001:db8:abc1::/64"
}
```
  
2. Save the file.
3. Restart docker: `systemctl restart docker`.
4. Execute the command `docker network ls`. Unless you have previously created your own custom networks, you will see the following:

    ```
$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
9ac6696dded9   bridge    bridge    local
bcc12f998444   host      host      local
32126ee8d073   none      null      local
```
5. Inspect the default docker 'bridge' network with `docker network inspect bridge`. You should see your chosen IPv6 network listed, as well as a gateway address.
    ```
joe@ub6:~$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "9ac6696dded9bec310d3451552df8b399503b09b6019b879e08914f6dd056a9d",
        "Created": "2021-08-25T08:10:48.713298521Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": true,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                },
                {
                    "Subnet": "2001:db8:abc1::/64",
                    "Gateway": "2001:db8:abc1::1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]
```. 
6. Execute the `ip add` command and the `docker0` interface now shows an IPv6 address:

    ```
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:dc:01:46:7a brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 2001:db8:abc1::1/64 scope global
       valid_lft forever preferred_lft forever
```

## Create an IPv6 container
1. Create a container as usual. By default this is assigned to the docker0 bridge network and has an IPv4 & IPv6 address:

    ```
$ docker run -di --name alpine6-1 alpine
0f1e634aacf45bfccca1494d6804bbaeac21b870152ceebdaeea8ad72ae27b3d
$ docker exec alpine6-1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
22: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2001:db8:abc1::242:ac11:2/64 scope global flags 02
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever
```. 

2. Ping the local gateway:
    ```
joe@ub6:~$ docker exec alpine6-1 ping6 2001:db8:abc1::1
PING 2001:db8:abc1::1 (2001:db8:abc1::1): 56 data bytes
64 bytes from 2001:db8:abc1::1: seq=0 ttl=64 time=0.133 ms
64 bytes from 2001:db8:abc1::1: seq=1 ttl=64 time=0.143 ms
64 bytes from 2001:db8:abc1::1: seq=2 ttl=64 time=0.123 ms
```

* The docker0 network is the default bridge network. By default, containers assigned to it will now have IPv4 and IPv6 connectivity to the local gateway and each other.
* However, due to limitations with the docker0 network, the advice from docker is that this network should not be used in Production environments, user-defined bridge networks should be used instead. In the next section I will build one such custom network.

## User-defined bridge network build
* The user-defined bridge network is similar to the docker0 network; it resides on the docker host, and the host acts as the network's gateway.
* The containers assigned to a user-defined bridge network have access to other containers on the same network, to the gateway and to external networks.
* However, because the docker host performs the role of a gateway, external networks will need to have the necessary routing information to be able to communicate with the custom network. See the diagram below.

![Screenshot 2021-09-01 at 22.37.24](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3rwjb6wdr6cn697ydo3o.png)

* In this example, the external server, by default, will not have a route to the user-defined bridge network's subnet, named my-net1 (2001:db8:1::1/64). 
* The docker host acts as the gateway between the external network (2001:db8:eeee::/64) and my-net1.
* The containers attached to my-net1 will be able to send traffic to external networks, using the docker host as their default gateway, but return traffic will fail, the external server will lack the required information to return traffic to the containers.
* Therefore, this situation can be improved by adding a static route to the external server, pointing to 2001:db8:1::/64 via the docker host (2001:db8:eeee::/64).

### Build Step-by-step

1. Enable IPv6 for docker, see above for details.
2. Create a new IPv6 bridge network with the following command:

    ```
docker network create --subnet="<your-v6-prefix>" \
  --gateway="your-gateway-address" \
  --ipv6 \
  <name-of-bridge-network>
```

3. Attach a container to this network using the `--network` flag for `docker run`.
4. Verify with `docker network inspect <your-network>`.
5. For external access, ensure destination networks have a route to `<your-v6-prefix>`.

### Step-by-step example

```

# Create the network
$ docker network create --subnet="2001:db8:1::/64" \
    --gateway="2001:db8:1::1" \
    --ipv6 \
    my-net1
# Create and attach a container
$ docker run -di --name alpine-1 \
    --network my-net1 \
    alpine:latest
# Verify
$ docker network inspect my-net1
[
    {
        "Name": "my-net1",
        "Id": "c71cbef39206ccaf037db82a12993e59851c1ca9ad18f45975cff8a96f85240a",
        "Created": "2021-09-02T08:13:38.448154937Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": true,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.20.0.0/16",
                    "Gateway": "172.20.0.1"
                },
                {
                    "Subnet": "2001:db8:1::/64",
                    "Gateway": "2001:db8:1::1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "736667de9c88a66fa14aa664eaad827bac7e68776288997361b5cda157949b65": {
                "Name": "alpine-1",
                "EndpointID": "88e56b4c8acbc4342ff0f3c0624a8e801b7fc3f4002339d14450645117ddd5b5",
                "MacAddress": "02:42:ac:14:00:02",
                "IPv4Address": "172.20.0.2/16",
                "IPv6Address": "2001:db8:1::2/64"
            }
        },
        "Options": {},
        "Labels": {}
    }
]
```
#### A Note on external communication for IPv6 containers
* In this example I've used the IPv6 prefix assigned to documentation for illustrative purposes.
* As previously mentioned, for containers on this network to communicate with external destinations, the external nodes will require the routing information to the internal v6 network.
* If you wish to provide your containers with access to the public internet, their attached network will need to be a globally routable prefix.
* In my home lab, my ISP currently only provides one globally routable prefix, assigned to my LAN. (I have a /56 assigned to my home but the ISP router is currently locked to only act as the gateway for a single /64 on the LAN).
* In this case, I opted for an IPvlan Layer 2 docker network, this uses the docker host's IPv6 network. In my case, this network is the ISP assigned globally routable network.

## IPvlan network build

The official docs for IPvlan are [here](https://docs.docker.com/network/ipvlan/)
* I'm using the default L2 mode.
* In this mode, the docker host parent interface will operate at Layer 2 only and switch traffic from containers to the external gateway, in my case this is my ISP router, which will then forward on the traffic to the public internet.

See the diagram below; the parent host interface, the containers, and the external gateway sit in the same IPv6 subnet.

![Screenshot 2021-08-26 at 17.28.31](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/766x2jknpq00pc9geffr.png) 
 
### Build Step-by-step
1. Create the IPvlan network with the following command:

    ```
docker network create -d ipvlan \
  --subnet=<your-network-address>::/64 \
  --gateway=<your-gateway-address>  \
  --ipv6 -o parent=<your-host-parent-interface> \
  <your-network-name>
```

    The parent interface sits in the same subnet as the gateway. Check 'ip addr' to confirm the interface name.
    The created network is dual-stack, with both IPv4 and IPv6 addressing. Unless you specifically configure the IPv4 addressing, it is a /16 taken from the 172/8 range.

  
2. Create a new container and connect it to the IPvlan network.

    ```
    docker run -di --name alpine6-2 \
      --network <your-network-name> alpine
    ```
    This creates a container and attaches it to the IPvlan network.
    The container is dual-stack, with an IPv4 address and IPv6 address. 
    Verify with `ip addr`.

3. To test, ping an IPv6 site, such as `youtube.com`:

    ```
$ docker exec alpine6-2 ping youtube.com
PING youtube.com (2a00:1450:4009:815::200e): 56 data bytes
64 bytes from 2a00:1450:4009:815::200e: seq=0 ttl=117 time=11.663 ms
64 bytes from 2a00:1450:4009:815::200e: seq=1 ttl=117 time=16.935 ms
64 bytes from 2a00:1450:4009:815::200e: seq=2 ttl=117 time=11.311 ms
```

# TL;DR? 👇

{% youtube ghqBzad5efc %}