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

How is the IPv6 link-local address calculated?

+2
−0

I was told that an IPv6 link-local address — typically starting with fe80:: — is derived from the interface's MAC address. Here are some instruction on how to do the conversion.

But the conversion result doesn't match my actual IPv6 link-local address according to ip a. My IPv6 link-local addresses and MAC addresses don't seem related at all.

How is the IPv6 link-local address really calculated?

I'm on Arch Linux using NetworkManager 1.42.6.

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

1 comment thread

Direct use of MAC addresses for IPv6 SLAAC is discouraged (1 comment)

1 answer

+1
−0

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 linked Wikipedia article, EUI-64 is deprecated 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. If the connection was created with a tool like nm-connection-editor, the default is "stable-privacy".

From the documentation:

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

You can also use nmcli:

nmcli -g ipv6.addr-gen-mode connection show YourConnection

If you get the value "default", consult the global NetworkManager config file, 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, we can see that the function nm_utils_ipv6_addr_set_stable_privacy_with_host_id 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 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.

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.

History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »