Skip to content

# IPv6

Ample Opportunities for Issues

Numerous bug reports have been raised in the past about IPv6. Please make sure your setup around DMS is correct when using IPv6!

## IPv6 networking problems with Docker defaults

### What can go wrong?

If your host system supports IPv6 and an `AAAA` DNS record exists to direct IPv6 traffic to DMS, you may experience issues when an IPv6 connection is made:

- The original client IP is replaced with the gateway IP of a docker network.
- Connections fail or hang.

The impact of losing the real IP of the client connection can negatively affect DMS:

- Users unable to login (Fail2Ban action triggered by repeated login failures all seen as from the same internal Gateway IP)
- Mail inbound to DMS is rejected (SPF verification failure, IP mismatch)
- Delivery failures from sender reputation being reduced (due to bouncing inbound mail from rejected IPv6 clients)
- Some services may be configured to trust connecting clients within the containers subnet, which includes the Gateway IP. This can risk bypassing or relaxing security measures, such as exposing an open relay.

### Why does this happen?

When the host network receives a connection to a containers published port, it is routed to the containers internal network managed by Docker (typically a bridge network).

By default, the Docker daemon only assigns IPv4 addresses to containers, thus it will only accept IPv4 connections (unless a `docker-proxy` process is listening, which the default daemon setting `userland-proxy: true` enables). With the daemon setting `userland-proxy: true` (default), IPv6 connections from the host can also be accepted and routed to containers (even when they only have IPv4 addresses assigned). `userland-proxy: false` will require the container to have atleast an IPv6 address assigned.

This can be problematic for IPv6 host connections when internally the container is no longer aware of the original client IPv6 address, as it has been proxied through the IPv4 or IPv6 gateway address of it's connected network (eg: `172.17.0.1` - Docker allocates networks from a set of default subnets).

This can be fixed by enabling a Docker network to assign IPv6 addresses to containers, along with some additional configuration. Alternatively you could configure the opposite to prevent IPv6 connections being made.

## Prevent IPv6 connections

- Avoiding an `AAAA` DNS record for your DMS FQDN would prevent resolving an IPv6 address to connect to.
- You can also use `userland-proxy: false`, which will fail to establish a remote connection to DMS (provided no IPv6 address was assigned).

With UFW or Firewalld

When one of these firewall frontends are active, remote clients should fail to connect instead of being masqueraded as the docker network gateway IP. Keep in mind that this only affects remote clients, it does not affect local IPv6 connections originating within the same host.

## Enable proper IPv6 support

You can enable IPv6 support in Docker for container networks, however compatibility concerns may affect your success.

The official Docker documentation on enabling IPv6 has been improving and is a good resource to reference.

Enable `ip6tables` support so that Docker will manage IPv6 networking rules as well. This will allow for IPv6 NAT to work like the existing IPv4 NAT already does for your containers, avoiding the above issue with external connections having their IP address seen as the container network gateway IP (provided an IPv6 address is also assigned to the container).

Configure the following in `/etc/docker/daemon.json`

    {
      "ip6tables": true,
      "experimental" : true,
      "userland-proxy": true
    }

- `experimental: true` is currently required for `ip6tables: true` to work.
- `userland-proxy` setting can potentially affect connection behaviour for local connections.

Now restart the daemon if it's running: `systemctl restart docker`.

Next, configure a network with an IPv6 subnet for your container with any of these examples:

Create an IPv6 ULA subnet
About these examples

These examples are focused on a IPv6 ULA subnet which is suitable for most users as described in the next section.

- You may prefer a subnet size smaller than `/64` (eg: `/112`, which still provides over 65k IPv6 addresses), especially if instead configuring for an IPv6 GUA subnet.
- The network will also implicitly be assigned an IPv4 subnet (from the Docker daemon config `default-address-pools`).

User-defined NetworkDefault Bridge (daemon)

The preferred approach is with user-defined networks via `compose.yaml` (recommended) or CLI with `docker network create`:

ComposeCLI

Create the network in `compose.yaml` and attach a service to it:

compose.yaml

    services:
      mailserver:
        networks:
          - dms-ipv6

    networks:
      dms-ipv6:
        enable_ipv6: true
        ipam:
          config:
            - subnet: fd00:cafe:face:feed::/64

Override the implicit `default` network

You can optionally avoid the service assignment by overriding the `default` user-defined network that Docker Compose generates. Just replace `dms-ipv6` with `default`.

The Docker Compose `default` bridge is not affected by settings for the default `bridge` (aka `docker0`) in `/etc/docker/daemon.json`.

Using the network outside of this `compose.yaml`

To reference this network externally (from other compose files or `docker run`), assign the networks `name` key in `compose.yaml`.

Create the network via a CLI command (which can then be used with `docker run --network dms-ipv6`):

    docker network create --ipv6 --subnet fd00:cafe:face:feed::/64 dms-ipv6

Optionally reference it from one or more `compose.yaml` files:

compose.yaml

    services:
      mailserver:
        networks:
          - dms-ipv6

    networks:
      dms-ipv6:
        external: true

This approach is discouraged

The `bridge` network is considered legacy.

Add these two extra IPv6 settings to your daemon config. They only apply to the default `bridge` docker network aka `docker0` (which containers are attached to by default when using `docker run`).

/etc/docker/daemon.json

    {
      "ipv6": true,
      "fixed-cidr-v6": "fd00:cafe:face:feed::/64",
    }

Compose projects can also use this network via `network_mode`:

compose.yaml

    services:
      mailserver:
        network_mode: bridge

Do not use `2001:db8:1::/64` for your private subnet

The `2001:db8` address prefix is reserved for documentation. Avoid creating a subnet with this prefix.

Presently this is used in examples for Dockers IPv6 docs as a placeholder, while mixed in with private IPv4 addresses which can be misleading.

### Configuring an IPv6 subnet

If you've configured IPv6 address pools in `/etc/docker/daemon.json`, you do not need to specify a subnet explicitly. Otherwise if you're unsure what value to provide, here's a quick guide (Tip: Prefer IPv6 ULA, it's the least hassle):

- `fd00:cafe:face:feed::/64` is an example of a IPv6 ULA subnet. ULA addresses are akin to the private IPv4 subnets you may already be familiar with. You can use that example, or choose your own ULA address. This is a good choice for getting Docker containers to their have networks support IPv6 via NAT like they already do by default with IPv4.
- IPv6 without NAT, using public address space like your server is assigned belongs to an IPv6 GUA subnet.
  - Typically these will be a `/64` block assigned to your host, but this varies by provider.
  - These addresses do not need to publish ports of a container to another IP to be publicly reached (thus `ip6tables: true` is not required), you will want a firewall configured to manage which ports are accessible instead as no NAT is involved. Note that this may not be desired if the container should also be reachable via the host IPv4 public address.
  - You may want to subdivide the `/64` into smaller subnets for Docker to use only portions of the `/64`. This can reduce some routing features, and require additional setup / management via a NDP Proxy for your public interface to know of IPv6 assignments managed by Docker and accept external traffic.

### Verify remote IP is correct

With Docker CLI or Docker Compose, run a `traefik/whoami` container with your IPv6 docker network and port 80 published. You can then send a curl request (or via address in the browser) from another host (as your remote client) with an IPv6 network, the `RemoteAddr` value returned should match your client IPv6 address.

    docker run --rm -d --network dms-ipv6 -p 80:80 traefik/whoami
    # On a different host, replace `2001:db8::1` with your DMS host IPv6 address
    curl --max-time 5 http://[2001:db8::1]:80

IPv6 ULA address priority

DNS lookups that have records for both IPv4 and IPv6 addresses (eg: `localhost`) may prefer IPv4 over IPv6 (ULA) for private addresses, whereas for public addresses IPv6 has priority. This shouldn't be anything to worry about, but can come across as a surprise when testing your IPv6 setup on the same host instead of from a remote client.

The preference can be controlled with `/etc/gai.conf`, and appears was configured this way based on the assumption that IPv6 ULA would never be used with NAT. It should only affect the destination resolved for outgoing connections, which for IPv6 ULA should only really affect connections between your containers / host. In future IPv6 ULA may also be prioritized.