Post History
It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of that converter, called modified EUI-64. As mentioned in the linke...
Answer
#7: Post edited
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/NetworkManager/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/NetworkManager/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/NetworkManager/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
# After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare the calculated address to the address created by NetworkManager, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/NetworkManager/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/NetworkManager/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/NetworkManager/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the address is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare the calculated address to the address created by NetworkManager, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
#6: Post edited
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/NetworkManager/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/NetworkManager/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare the calculated address to the address created by NetworkManager, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/NetworkManager/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/NetworkManager/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/NetworkManager/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare the calculated address to the address created by NetworkManager, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
#5: Post edited
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/NetworkManager/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/NetworkManager/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/NetworkManager/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/NetworkManager/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare the calculated address to the address created by NetworkManager, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
#4: Post edited
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
6. The host ID which is either: The contents of `/var/lib/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/secret_key` and `/etc/machine_id`- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/NetworkManager/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/NetworkManager/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
#3: Post edited
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/secret_key` and `/etc/machine_id`
The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe:80::` to get the link-local address.- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe:80::`.- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).[]()
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
#2: Post edited
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe:80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe:80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
echo "Calculate the IPv6 stable privacy link local address, as NetworkManager does it";- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
>&2 echo "Usage: Call this script without arguments to get the link local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- ---
If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).
- It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons.
- Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy".
- From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode):
- >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217.
- You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection:
- ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png)
- You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en):
- nmcli -g ipv6.addr-gen-mode connection show YourConnection
- If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used.
- ## How does NetworkManager calculate the stable privacy link-local address?
- When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256:
- 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000`
- 2. The interface name, for example "enp0s25"
- 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con`
- 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details.
- 5. The host ID length.
- 6. The host ID which is either: The contents of `/var/lib/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/secret_key` and `/etc/machine_id`
- The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe:80::` to get the link-local address.
- This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5).
- The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe:80::`.
- #!/bin/bash
- set -o nounset
- # Returns the host ID in plain hexdump style ("ab01cdefa8").
- get_host_id_hex(){
- local -r secret_key_file="$1";
- # File must be readable
- if [ -r "$secret_key_file" ]; then
- if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then
- # Secret key file is version 2
- # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c"
- (
- # Get the file size in bytes
- stat -c '%s' "$secret_key_file" | tr -d '\n';
- printf ' ';
- cat "$secret_key_file";
- tr -d '\n' < /etc/machine-id;
- ) \
- | sha256sum \
- | awk '{print $1}'
- else
- # Secret key file is version 1
- # xxd produces line breaks after a certain amount of characters → cols 0 removes those
- xxd -p -cols 0 "$secret_key_file"
- fi
- else
- >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root.";
- fi
- }
- # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes.
- as_bytes(){
- printf '%s' "$1" | xxd -r -p;
- }
- # Shortens an IPv6 address.
- shorten_ip_address() {
- expanded_ip="$1"
- # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon)
- # "0000:" → "0:"
- # "0a00:" → "a00:"
- output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip");
- # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a".
- # A zero-group of hextets is, for example, ":0:0".
- # Replace the largest group of zero hextets with a double colon.
- # If two zero-groups are equal in length, replace the first one.
- # A single zero-group such as ":0" in "abc:0" is not replaced with "::",
- # hence we use "count>=2"
- for ((count=8; count>=2; count--)); do
- rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output");
- if [ "$rule_2_output" != "$output" ]; then
- output="$rule_2_output"
- break;
- fi
- done
- echo "$output"
- }
- # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents.
- nr_of_bytes_in_hexdump(){
- local -r hexdump="$1";
- local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars);
- # Two hex digits represent one byte
- printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))";
- }
- # Converts a decimal number to a 4-byte big-endian integer in hex.
- # E.g., Input: 32 → Output: "00000020"
- to_4_byte_big_endian(){
- # Convert to hex and left-pad with zeros to 8 characters for 4 bytes
- printf '%08x' "$1"
- }
- calculate_address() {
- local -r conn_name="$1";
- echo "Connection is \"$conn_name\"";
- local -r network_id=$(nmcli -g connection.uuid con show "$conn_name");
- echo "Network ID is $network_id";
- local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name");
- echo "Interface name is $ifname";
- # First eight bytes of LL address
- local -r network_prefix="fe80000000000000";
- echo "Assuming network prefix of $network_prefix"
- # Duplicate address detection counter. We're not doing DAD and it won't be incremented
- local -r dad_counter_hex=$(to_4_byte_big_endian 0);
- local -r secret_key_file="/var/lib/NetworkManager/secret_key";
- echo "Secret key file is $secret_key_file";
- local -r host_id_hex=$(get_host_id_hex "$secret_key_file");
- if [ -n "$host_id_hex" ]; then
- printf 'Host ID: %s\n' "$host_id_hex";
- local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex"));
- printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex";
- local -r last_eight_bytes_of_address=$(
- (
- as_bytes "$network_prefix";
- printf '%s\0' "$ifname";
- printf '%s\0' "$network_id";
- as_bytes "$dad_counter_hex";
- as_bytes "$nr_of_bytes_in_host_id_hex";
- as_bytes "$host_id_hex";
- ) \
- | sha256sum \
- | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes
- );
- # Add colons between hextets (two-byte groups)
- local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address");
- shorten_ip_address "$expanded_ip_addr"
- fi
- }
- main(){
- echo "Calculate the IPv6 stable privacy link-local address, as NetworkManager does it";
- if (( $# == 0 ));then
- echo "No argument provided → Using first active connection"
- local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//')
- elif (( $# == 1 )); then
- local -r conn_name="$1"
- else
- >&2 echo "Usage: Call this script without arguments to get the link-local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\""
- exit 1
- fi
- calculate_address "$conn_name"
- }
- main "$@"; exit
- The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with
- ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local'
- You might want to change `enp0s25` to the interface name from which you want to query the link-local address.
- ---
- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).[]()
#1: Initial revision
It's NetworkManager that sets the IPv6 link-local address and per default it doesn't use the conversion from MAC to IPv6 address of [that converter](https://ben.akrin.com/mac-address-to-ipv6-link-local-address-online-converter/), called [modified EUI-64](https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64). As mentioned in the linked Wikipedia article, EUI-64 is [deprecated](https://datatracker.ietf.org/doc/html/rfc8064#section-1) for privacy and security reasons. Instead of EUI-64, NetworkManager commonly uses the "stable-privacy" address generation mode for IPv6 link-local addresses. The rules for the default value of the address generation mode [depend on which component configured the connection](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/e6a33c04ebe1ac84e31628911e25bdfd7534dd3c). If the connection was created with a tool like `nm-connection-editor`, the default is "stable-privacy". From the [documentation](https://networkmanager.dev/docs/api/latest/nm-settings-nmcli.html#nm-settings-nmcli.property.ipv6.addr-gen-mode): >The value of stable-privacy enables use of cryptographically secure hash of a secret host-specific key along with the connection's stable-id and the network address as specified by RFC7217. You can check which mode you're using in `nm-connection-editor`, going to the "IPv6 Settings" tab of your connection: ![nm-connection-editor screenshot with IPv6 address generation mode](https://i.imgur.com/hnsl4og.png) You can also use [`nmcli`](https://man.archlinux.org/man/nmcli.1.en): nmcli -g ipv6.addr-gen-mode connection show YourConnection If you get the value "default", consult the global [NetworkManager config file](https://man.archlinux.org/man/NetworkManager.conf.5.en), commonly at `/etc/NetworkManager/NetworkManager.conf`. If there's no value for `ipv6.addr-gen-mode`, then "stable privacy" is used. ## How does NetworkManager calculate the stable privacy link-local address? When digging into NetworkManager's source code or [debugging NetworkManager](https://linux.codidact.com/posts/288925), we can see that the function [`nm_utils_ipv6_addr_set_stable_privacy_with_host_id`](https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.43.10-dev/src/core/nm-core-utils.c#L3553) calculates the link-local address by hashing this data using SHA-256: 1. The network prefix which are the first 8 bytes of the link-local address. For example `fe80000000000000` 2. The interface name, for example "enp0s25" 3. The network ID. See the IDs of your networks with `nmcli -g name,uuid con` 4. A count starting at zero that's incremented if the generated link-local address turns out to be already in use on the network. See [Duplicate Address Detection](https://www.rfc-editor.org/rfc/rfc4429) for details. 5. The host ID length. 6. The host ID which is either: The contents of `/var/lib/secret_key`. Or the SHA-256 hash of, essentially, `/var/lib/secret_key` and `/etc/machine_id` The first 8 bytes of this hash are then appended to the network prefix which is commonly `fe:80::` to get the link-local address. This conforms to the [algorithm specified in RFC 7217](https://www.rfc-editor.org/rfc/rfc7217#section-5). The following shell script attempts to reimplement NetworkManager's calculation for the common case (there are also code paths in NetworkManager for situations where `/var/lib/secret_key` or `/etc/machine_id` don't exist on the machine). It assumes that the network prefix for the link-local address is `fe:80::`. #!/bin/bash set -o nounset # Returns the host ID in plain hexdump style ("ab01cdefa8"). get_host_id_hex(){ local -r secret_key_file="$1"; # File must be readable if [ -r "$secret_key_file" ]; then if [ "$(head --bytes=5 "$secret_key_file")" = "nm-v2" ]; then # Secret key file is version 2 # Adapted from function "_host_id_hash_v2" in "nm-core-utils.c" ( # Get the file size in bytes stat -c '%s' "$secret_key_file" | tr -d '\n'; printf ' '; cat "$secret_key_file"; tr -d '\n' < /etc/machine-id; ) \ | sha256sum \ | awk '{print $1}' else # Secret key file is version 1 # xxd produces line breaks after a certain amount of characters → cols 0 removes those xxd -p -cols 0 "$secret_key_file" fi else >&2 echo "Error: Secret key file \"$secret_key_file\" is not readable. Try again as root."; fi } # First and only argument is a plain hexadecimal string representation of bytes such as "a0eef9" wich is converted to actual bytes. as_bytes(){ printf '%s' "$1" | xxd -r -p; } # Shortens an IPv6 address. shorten_ip_address() { expanded_ip="$1" # Rule 1: Remove leading zeros in each hextet (two-byte group separated by colon) # "0000:" → "0:" # "0a00:" → "a00:" output=$(sed -E 's/(^|:)0{1,3}/\1/g' <<< "$expanded_ip"); # After rule 1, the addresses is shortened to, e.g., "0:ab:cdef:0:0:a". # A zero-group of hextets is, for example, ":0:0". # Replace the largest group of zero hextets with a double colon. # If two zero-groups are equal in length, replace the first one. # A single zero-group such as ":0" in "abc:0" is not replaced with "::", # hence we use "count>=2" for ((count=8; count>=2; count--)); do rule_2_output=$(sed -E "s/(^0|:0){$count}:?/::/" <<< "$output"); if [ "$rule_2_output" != "$output" ]; then output="$rule_2_output" break; fi done echo "$output" } # Takes a plain hex dump ("abcd0189") and produces the number of bytes it represents. nr_of_bytes_in_hexdump(){ local -r hexdump="$1"; local -r hexdump_nr_of_chars=$(printf '%s' "$hexdump" | wc --chars); # Two hex digits represent one byte printf '%s' "$(( "$hexdump_nr_of_chars" / 2 ))"; } # Converts a decimal number to a 4-byte big-endian integer in hex. # E.g., Input: 32 → Output: "00000020" to_4_byte_big_endian(){ # Convert to hex and left-pad with zeros to 8 characters for 4 bytes printf '%08x' "$1" } calculate_address() { local -r conn_name="$1"; echo "Connection is \"$conn_name\""; local -r network_id=$(nmcli -g connection.uuid con show "$conn_name"); echo "Network ID is $network_id"; local -r ifname=$(nmcli -g general.ip-iface con show "$conn_name"); echo "Interface name is $ifname"; # First eight bytes of LL address local -r network_prefix="fe80000000000000"; echo "Assuming network prefix of $network_prefix" # Duplicate address detection counter. We're not doing DAD and it won't be incremented local -r dad_counter_hex=$(to_4_byte_big_endian 0); local -r secret_key_file="/var/lib/NetworkManager/secret_key"; echo "Secret key file is $secret_key_file"; local -r host_id_hex=$(get_host_id_hex "$secret_key_file"); if [ -n "$host_id_hex" ]; then printf 'Host ID: %s\n' "$host_id_hex"; local -r nr_of_bytes_in_host_id_hex=$(to_4_byte_big_endian $(nr_of_bytes_in_hexdump "$host_id_hex")); printf 'Nr of bytes in host ID (hex): %s\n' "$nr_of_bytes_in_host_id_hex"; local -r last_eight_bytes_of_address=$( ( as_bytes "$network_prefix"; printf '%s\0' "$ifname"; printf '%s\0' "$network_id"; as_bytes "$dad_counter_hex"; as_bytes "$nr_of_bytes_in_host_id_hex"; as_bytes "$host_id_hex"; ) \ | sha256sum \ | head -c 16 # Get the first 16 characters of the hash which represent 8 bytes ); # Add colons between hextets (two-byte groups) local -r expanded_ip_addr=$(sed -E 's/.{4}/\0:/g; s/:$//' <<< "$network_prefix""$last_eight_bytes_of_address"); shorten_ip_address "$expanded_ip_addr" fi } main(){ echo "Calculate the IPv6 stable privacy link local address, as NetworkManager does it"; if (( $# == 0 ));then echo "No argument provided → Using first active connection" local -r conn_name=$(nmcli --mode multiline con show --active | head -n1 | cut -d ':' -f2- | sed -e 's/^[[:space:]]*//') elif (( $# == 1 )); then local -r conn_name="$1" else >&2 echo "Usage: Call this script without arguments to get the link local address of the first active NetworkManager connection. Optionally, pass a connection name as the first argument. See all available connections with \"nmcli con\"" exit 1 fi calculate_address "$conn_name" } main "$@"; exit The script requires root since it reads `/var/lib/NetworkManager/secret_key`. To (programmatically) compare if the calculated address matches the actual address, you can get your current link-local address with ip -json -6 a show enp0s25 scope link | jq -r '.[0].addr_info[1].local' --- If you don't use NetworkManager for adding IP addresses to interfaces, see [this answer](https://superuser.com/questions/1297852/debian-mint-raspbian-ubuntu-how-to-force-slaac-eui64-ipv6-autoconfiguration#1297875).