Ethernet switches and cables.

Configuring IPv6 on Linux with nmcli

The Problem to be Solved:

I wanted to reduce the work involved in updating my local DNS and DHCPv6 server when my ISP changes my globally routeable IPv6 address block. Open source applications strongly prefer IPv6. A connection with ssh or sftp will attempt to connect to the IPv6 address first, and then after a timeout, roll back to IPv4. I want to be able to use hostnames that correctly resolve to IPv6 addresses.

That meant changing the IPv6 generation algorithm from its default behavior to the predictable EUI-64 method. That in turn meant that I must figure out how to do that with the nmcli command.

I would be doing this on multiple Linux distributions. While the nmcli has its own peculiar and long-winded syntax, I only have to solve this problem once. The command-line solution works the same across Linux distributions.

Now, before you cringe at my use of EUI-64 and SLAAC and start lecturing me about privacy issues: I only use this solution, and I only suggest that you might want to use it, within a trusted organization. For me, that's my home (and consulting lab). When I take my laptop to the coffee shop, it will still generate randomized interface identifiers on those other WiFi networks.

The Context:

Linux Mint Oracle Linux Raspberry Pi OS OSMC Media Center

My primary desktop and my laptop both run Linux Mint.

A desktop I use to mirror media storage (pictures, music, videos) runs Oracle Linux, because that also gives me an added test platform for a consulting project that uses Red Hat Enterprise Linux.

I own several Raspberry Pi single-board computers that run Raspberry Pi OS. It's based on Debian Linux, and used to be called Raspbian. One is my DNS and DHCPv6 server, two do ACARS aircraft tracking and some GnuRadio experiments, one is my OSMC-based media server.

Once I figured out how to accomplish what I show you here, I ran the corresponding commands on all those systems, changing the connection names as needed.

My ISP is Xfinity, operating as Comcast. They allocate me a globally routeable /64 block of IP addresses. My router, a Netgear R6220, can handle the IPv6. Notice that Xfinity/Comcast has assigned my router's external interface an address in the 2001:558:6002:39::/64 address block, and told it to use the 2601:249:4300:4f7::/64 address block internally. The whois command shows me that both of the blocks 2001:558::/29 and 2601::/20 are assigned to Comcast.

Screenshot of Netgear router showing IPv6 configuration.

My Own DNS, and Thus DHCP

I do not want to use my ISP's DNS service, because they do the nonsense of providing bogus answers that would direct my browser to advertisers' sites if I make a typo in a URL. No, if I misspell the hostname in a URL, I want to the browser to tell me that they're no such host and I seem to have made a typing error.

1: The U.S. DoD STIG requires two IPv4 addresses for name servers. My consulting project thus avoids false-positive DNS shortcomings while I test the hardening scripts I'm developing.

One Raspberry Pi with two IPv4 addresses1 is my DNS and DHCP server for IPv4 and IPv6.

I use the kc9rg.org domain internally. It's my ham radio callsign, I have never gotten around to registering that domain.

My DHCP server tells hosts how to do DNS and routing on IPv4:

root@ns1# more /etc/dhcp/dhcpd.conf
[... several lines deleted ...]
# option definitions common to all supported networks...
option domain-name "kc9rg.org";
option domain-name-servers 192.168.1.3, 192.168.1.4;
ddns-update-style none;

# Internal network, run the server on 192.168.1.0/24 only.
subnet 192.168.1.0 netmask 255.255.255.0 {
	authoritative;
	# Default gateway, netmask -- the router is manually configured
	# to use that IPv4/netmask on its inside interface.
	option routers 192.168.1.254;
	option subnet-mask 255.255.255.0;
	# 30 days by default, 30 days max
	default-lease-time 2592000;
	max-lease-time 2592000;

	range dynamic-bootp 192.168.1.220 192.168.1.249;

	# Configure the laser printer
		host hpljp3015 {
		hardware ethernet 3c:4a:92:c0:aa:21;
		fixed-address 192.168.1.40;
        }
	# Configure the Blu-ray player
		host bluray {
		hardware ethernet 00:1c:50:ac:72:1e;
		fixed-address 192.168.1.42;
        }
	[... blocks for more devices deleted ...]
}

On IPv6, each host picks up the /64 address block and the default route in the ICMPv6 Router Advertisement packets coming from the router. My DHCPv6 service fills in the IPv6 DNS details:

root@ns1# more /etc/dhcp/dhcpd6.conf
[... several lines deleted ...]
# Global definitions for name server address(es) and domain search list
subnet6 2601:249:4300:4f7::/64 {
	option dhcp6.name-servers 2601:249:4300:4f7:dea6:32ff:fe36:a94e;
	option dhcp6.domain-search "kc9rg.org";
	default-lease-time 2592000;
	max-lease-time 2592000;
}

IPv6 Addresses

An IPv6 address is 128 bits long. In most situations, the first 64 bits are the network prefix and the last 64 bits are the interface identifier. That's the IPv6 terminology, I'm used to "network ID" and "host ID" for the IPv4 equivalents.

Now, within an organization, feel free to consider the first 48 (or more) bits of the network prefix as the routing prefix and the remaining 16 (or fewer) bits as a subnet ID. However, I would expect an ISP to assign your organization a globally routeable /64 block.

Now, how to assign the interface identifiers, the host IDs?

The obvious choices are:

So, my solution will be:

EUI-64 Addresses

Observe what's happening for the wireless interface on my laptop after I have made the needed fix. I want to use the "global dynamic" IPv6 address:

cromwell@laptop:$ ip link show wlp1s0
2: wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DORMANT group default qlen 1000
    link/ether 7c:88:99:1b:c2:34 brd ff:ff:ff:ff:ff:ff
cromwell@laptop:$ ip -6 addr show wlp1s0
2: wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    inet6 2601:249:4300:4f7:58ba:4c62:181f:529/64 scope global temporary dynamic
       valid_lft 345587sec preferred_lft 75302sec
    inet6 2601:249:4300:4f7:7e88:99ff:fe1b:c234/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 345587sec preferred_lft 345587sec
    inet6 fe80::7e88:99ff:fe1b:c234/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

Match the colors to see what has happened with the two pieces of the MAC address. The 16-bit 0xfffe block indicates that this is an EUI-64 encapsulated interface identifier.

MAC address:  7c:88:99:1b:c2:34
              \---+--/ \---+--/
	          |        |
     manufacturer ID      incremented by manufacturer so
                          that all of its devices have unique
			  6-octet or 48-bit MAC addresses

                                interface identifier
				          |
                                 /--------+--------\
IPv6 address:  2601:249:4300:4f7:7e88:99ff:fe1b:c234
               \-------+-------/ \--+--/\-+-/\--+--/
	               |            |     |     |
           network prefix,          |     |     |
	   a.k.a. net ID            |   FF:FE   |
	                            |           |
		  manufacturer ID from       device-unique
		  the MAC address with       segment of the
		  bit #7 set to 1:           MAC address
		  0x7c = 1111100
		  0x7e = 1111110

The first 48 bits of my allocated IPv6 block, 2601:249:4300::/48, haven't changed for ages, at least a few years and maybe since Comcast first started supporting IPv6 here. The remaining 16 bits of the network prefix change every few months, once in a while just a few weeks apart. I believe that I'm noticing a correlation with electrical power outages on the scale of at least a neighborhood, at least for some fraction of the changes. So, Comcast is using 2601:249:4300::/48 as a routing prefix to get to the area where I live in West Lafayette, and then with the following 16 subnet bits they could have up to 216 = 65,536 possible customer subnets within that routing prefix. That's plenty for a town with multiple ISPs and 44,595 residents at the last census.

My cable router has dual-band wireless, 2.4 GHz for 802.11b/g/n and 5 GHz for 802.11a/n/ac.

The Raspberry Pi that is my DNS/DHCP server has a 1 Gbps Ethernet interface plugged into a switch, and 802.11 WiFi associated with the router. For the wireless side it reports:

cromwell@ns1$ iwconfig
lo        no wireless extensions.

eth0      no wireless extensions.

wlan0     IEEE 802.11  ESSID:"FBI_van4"
	  Mode:Managed  Frequency:5.745 GHz  Access Point: 38:94:ED:FA:48:8C
	  Bit Rate=24 Mb/s   Tx-Power=31 dBm
	  Retry short limit:7   RTS thr:off   Fragment thr:off
	  Power Management:on
	  Link Quality=49/70  Signal level=-61 dBm
	  Rx invalid nwid:0  Rx invalid crypt:0  Rx invalid frag:0
	  Tx excessive retries:0  Invalid misc:0   Missed beacon:0

Let's see what happened after a recent Comcast change to my IPv6 block, from 2601:249:4300:1ec4::/64 to 2601:249:4300:4f7::/64, from subnet 0x1ec4 to subnet 0x04f7:

cromwell@ns1$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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 noprefixroute 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether dc:a6:32:36:a9:4d brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.4/24 brd 192.168.1.255 scope global noprefixroute eth0
       valid_lft forever preferred_lft forever
    inet6 2601:249:4300:4f7:9c2:87db:97cc:14e6/64 scope global dynamic noprefixroute 
       valid_lft 345534sec preferred_lft 345534sec
    inet6 fe80::c8c9:8e5d:7580:eb30/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether dc:a6:32:36:a9:4e brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.3/24 brd 192.168.1.255 scope global noprefixroute wlan0
       valid_lft forever preferred_lft forever
    inet6 2601:249:4300:4f7:df45:31c8:f6c6:d75b/64 scope global dynamic noprefixroute 
       valid_lft 345534sec preferred_lft 345534sec
    inet6 fe80::5b28:c9b4:79aa:fede/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

There are my two IPv4 addresses on the DNS server: 192.168.1.3 and 192.168.1.4. However, neither of the interface identifiers of the globally routeable IPv6 addresses:
2601:249:4300:4f7:9c2:87db:97cc:14e6
2601:249:4300:4f7:df45:31c8:f6c6:d75b
bear any resemblance to the MAC addresses of those interfaces:
dc:a6:32:36:a9:4d
dc:a6:32:36:a9:4e
Nor do they bear any resemblance to what the IPv6 interface identifiers were before the recent change.

Using nmcli

Many Linux subsystems are far more complex than what's needed by the typical user. And nmcli and the NetworkManager daemon it controls and queries are striking examples of that. It's not too bad to get a list of defined network connections:

cromwell@ns1$ nmcli connection show
NAME                UUID                                  TYPE      DEVICE
Wired connection 1  5e2b664a-6db9-3635-9959-98d3bd981b48  ethernet  eth0
FBI_van4            e36f354e-40ea-4733-8683-6345cc55fc05  wifi      wlan0
lo                  5630b038-69fe-47dc-8503-b2b4c37836ab  loopback  lo

A full report on just one of them drowns me in detail:

cromwell@ns1$ nmcli connection show FBI_van4
[... over 160 lines of output! ...]
cromwell@ns1$ nmcli connection show 'Wired connection 1'
[... over 130 lines of output! ...]

However, if I know what I'm looking for, I can ask for just that:

cromwell@ns1$ nmcli --get-values ipv6.addr-gen-mode connection show FBI_van4
default

That's a lot of typing for a one-word answer, but default is the answer that tells me why this isn't yet behaving as I want it to.

"Ah," you say, "so just how does the default work?"

Unfortunately, the answer to that is "It depends" and the details are voluminous. Somewhere over 400 lines into the 1,300-line manual page for nmcli it tells you that the settings and property names and their default behaviors are described in the 5,140-line manual page for nm-settings-nmcli. But it's worse. In getting this far, I found lots of commentary about how default for the ipv6.addr-gen-mode setting is actually ambiguous, it depends on multiple other settings described in other multi-thousand-line manual pages.

Fortunately in my case, default in any of its variations is never what I was wanting, so at least I found the thing that was mis-set and didn't have to go searching for an understanding of the ambiguous default. My goal needs eui64.

Should the designers of the nmcli command and the NetworkManager daemon be beat about the head and shoulders? Yes.

It gets even worse. That long-winded command has everything spelled out, making for a slight advantage for understanding what it's doing. But the options and parameters, or at least many of them, can be abbreviated, kind of like Cisco configuration commands. However, the degree to which a particular one can be abbreviated may be context-dependent. These versions all do the same thing:

cromwell@ns1$ nmcli --get-values ipv6.addr-gen-mode connection show FBI_van4
default
cromwell@ns1$ nmcli -g ipv6.addr-gen-mode connection show FBI_van4
default
cromwell@ns1$ nmcli -g ipv6.addr-gen-mode con sho FBI_van4
default
cromwell@ns1$ nmcli -g ipv6.addr-gen-mode c s FBI_van4
default

This, of course, is ridiculous.

Anyway, here's the pair of changes I need to make to change the wired Ethernet and the WiFi connection use EUI-64 to generate the IPv6 interface identifiers, and the pair of checks to verify that they worked:

cromwell@ns1$ sudo bash
root@ns1# set -o vi
root@ns1# nmcli connection modify FBI_van4 ipv6.addr-gen-mode eui64
root@ns1# nmcli connection modify 'Wired connection 1' ipv6.addr-gen-mode eui64
root@ns1# nmcli --get-values ipv6.addr-gen-mode connection show FBI_van4
eui64
root@ns1# nmcli --get-values ipv6.addr-gen-mode connection show 'Wired connection 1'
eui64

Now I can tell the NetworkManager daemon to restart. I'm not going to waste time speculating on what it might do if I told it to continue running while doing reload instead.

root@ns1# systemctl restart NetworkManager

And, let's see if we don't have EUI-64 based IPv6 addresses now:

root@ns1# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether dc:a6:32:36:a9:4d brd ff:ff:ff:ff:ff:ff
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DORMANT group default qlen 1000
    link/ether dc:a6:32:36:a9:4e brd ff:ff:ff:ff:ff:ff
root@ns1# ip -6 addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 2601:249:4300:4f7:dea6:32ff:fe36:a94d/64 scope global dynamic noprefixroute
       valid_lft 345555sec preferred_lft 345555sec
    inet6 2601:249:4300:4f7:9c2:87db:97cc:14e6/64 scope global dynamic noprefixroute
       valid_lft 345497sec preferred_lft 345497sec
    inet6 fe80::c8c9:8e5d:7580:eb30/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 2601:249:4300:4f7:dea6:32ff:fe36:a94e/64 scope global dynamic noprefixroute
       valid_lft 345555sec preferred_lft 345555sec
    inet6 fe80::dea6:32ff:fe36:a94e/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

Yes! Problem solved!

If I ask for a connection list on my laptop, I get an enormous amount of output — the two currently active plus all the connections used in the past preceded by "Auto".

cromwell@laptop:$ nmcli connection show
NAME                                 UUID                                  TYPE      DEVICE
FBI_van4                             ff1ae12b-d38d-4279-8e50-5f10b6da1a1d  wifi      wlp1s0
lo                                   19d7e852-32dd-43c6-9ce1-ebde78f6965e  loopback  lo
Auto *WIFI-AIRPORT                   54d9a0a0-f546-4a9e-a4c3-45d64c66f5e1  wifi      --
Auto .ONCF                           daee3f75-34a5-43db-a490-19adabdf4342  wifi      --
Auto 12 T?nar                        542d560c-8dfe-477d-9d31-c9f46c89c631  wifi      --
Auto @FuriousSpoonChi Free Wi-Fi     8f5dcf64-59af-4d81-9873-dc0558b2a9c7  wifi      --
Auto @NewLineTavern Free Wi-Fi       4c5fe568-c2cc-4842-bb4a-cd5402c5ec64  wifi      --
Auto ALPHASTUDIOS 2,4G               f86a91b8-a27f-4a9a-b60a-0f3200474d92  wifi      --
Auto AMIGuest1                       546ffd4a-42b4-472e-8f75-28d8da7df281  wifi      --
Auto APARTHOTEL PAPAFOTIS            120aba94-d152-460e-934c-b1cea2277313  wifi      --
Auto ATL Free Wi-Fi                  5458f446-484f-44b3-832b-45dab71bafbf  wifi      --
Auto Adam snack                      f10975d1-24d0-40b3-8eab-5349785e3f17  wifi      --
Auto Amtrak_WiFi                     8374f654-787c-4219-b5ec-758acc7bb62d  wifi      --
Auto Auberge la palmerie             910fae66-894a-49d3-a2c4-a009222ac1d2  wifi      --
[... a few hundred more lines deleted ...]

There are the WiFi connections at every single coffee shop, bar, airport, train, and budget accommodation where I have turned on my laptop over the past few years. All of them except the two in my home will still have ipv6.addr-gen-mode set to default, which is still doing whatever confusingly documented thing it was doing before this project.

Now to update the BIND zone files...

Going Deeper

RFC 7217
A Method for Generating Semantically Opaque Interface Identifiers with IPv6 Stateless Address Autoconfiguration (SLAAC)
RFC 8064
Recommendation on Stable IPv6 Interface Identifiers