Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Post History

60%
+1 −0
Q&A How is the IPv6 link-local address calculated?

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...

posted 10mo ago by Matthias Braun‭  ·  edited 10mo ago by Matthias Braun‭

Answer
#7: Post edited by user avatar Matthias Braun‭ · 2023-07-08T22:39:35Z (10 months ago)
fix grammar in script
  • 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 by user avatar Matthias Braun‭ · 2023-07-08T22:32:15Z (10 months ago)
fix path
  • 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 by user avatar Matthias Braun‭ · 2023-07-08T22:30:16Z (10 months ago)
fix wording
  • 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 by user avatar Matthias Braun‭ · 2023-07-08T22:27:42Z (10 months ago)
Fix path to NetworkManager's secret key file
  • 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 by user avatar Matthias Braun‭ · 2023-07-08T22:18:37Z (10 months ago)
fix typos
  • 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 by user avatar Matthias Braun‭ · 2023-07-08T22:17:08Z (10 months ago)
fix spelling
  • 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 by user avatar Matthias Braun‭ · 2023-07-08T22:07:12Z (10 months ago)
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).