diff --git a/man/systemd.network.xml b/man/systemd.network.xml
index 020a5ffb721..8a6211429df 100644
--- a/man/systemd.network.xml
+++ b/man/systemd.network.xml
@@ -424,13 +424,27 @@
Specifies how IPv6 link local address is generated. Takes one of
eui64, none, stable-privacy and
- random. When unset, the kernel's default will be used. Note that if
- LinkLocalAddressing= is no or
- ipv4, then IPv6LinkLocalAddressGenerationMode= will
- be ignored. Also, even if LinkLocalAddressing= is yes
- or ipv6, setting
- IPv6LinkLocalAddressGenerationMode=none disables to configure an IPv6
- link-local address.
+ random. When unset, stable-privacy is used if
+ IPv6StableSecretAddress= is specified, and if not,
+ eui64 is used. Note that if LinkLocalAddressing= is
+ no or ipv4, then
+ IPv6LinkLocalAddressGenerationMode= will be ignored. Also, even if
+ LinkLocalAddressing= is yes or
+ ipv6, setting IPv6LinkLocalAddressGenerationMode=none
+ disables to configure an IPv6 link-local address.
+
+
+
+ IPv6StableSecretAddress=
+
+ Takes an IPv6 address. The specified address will be used as a stable secret for
+ generating IPv6 link-local address. If this setting is specified, and
+ IPv6LinkLocalAddressGenerationMode= is unset, then
+ IPv6LinkLocalAddressGenerationMode=stable-privacy is implied.
+ If this setting is not specified, and stable-privacy is set to
+ IPv6LinkLocalAddressGenerationMode=,
+ then a stable secret address will be generated from the local machine ID and the interface
+ name.
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
index c1ee05a043c..86010121236 100644
--- a/src/network/networkd-network-gperf.gperf
+++ b/src/network/networkd-network-gperf.gperf
@@ -97,6 +97,7 @@ Network.DHCP, config_parse_dhcp,
Network.DHCPServer, config_parse_bool, 0, offsetof(Network, dhcp_server)
Network.LinkLocalAddressing, config_parse_link_local_address_family, 0, offsetof(Network, link_local)
Network.IPv6LinkLocalAddressGenerationMode, config_parse_ipv6_link_local_address_gen_mode, 0, offsetof(Network, ipv6ll_address_gen_mode)
+Network.IPv6StableSecretAddress, config_parse_in_addr_non_null, AF_INET6, offsetof(Network, ipv6ll_stable_secret)
Network.IPv4LLRoute, config_parse_bool, 0, offsetof(Network, ipv4ll_route)
Network.DefaultRouteOnDevice, config_parse_bool, 0, offsetof(Network, default_route_on_device)
Network.IPv6Token, config_parse_address_generation_type, 0, 0
diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c
index 4e4ae7b76be..011cb944625 100644
--- a/src/network/networkd-network.c
+++ b/src/network/networkd-network.c
@@ -175,6 +175,10 @@ int network_verify(Network *network) {
if (network->ipv6ll_address_gen_mode == IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE)
SET_FLAG(network->link_local, ADDRESS_FAMILY_IPV6, false);
+ if (in6_addr_is_set(&network->ipv6ll_stable_secret) &&
+ network->ipv6ll_address_gen_mode < 0)
+ network->ipv6ll_address_gen_mode = IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY;
+
/* IPMasquerade implies IPForward */
network->ip_forward |= network->ip_masquerade;
diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h
index ce030d2661e..341cf477cec 100644
--- a/src/network/networkd-network.h
+++ b/src/network/networkd-network.h
@@ -209,6 +209,7 @@ struct Network {
/* link local addressing support */
AddressFamily link_local;
IPv6LinkLocalAddressGenMode ipv6ll_address_gen_mode;
+ struct in6_addr ipv6ll_stable_secret;
bool ipv4ll_route;
/* IPv6 RA support */
diff --git a/src/network/networkd-setlink.c b/src/network/networkd-setlink.c
index 2c3803e02c2..4f250003385 100644
--- a/src/network/networkd-setlink.c
+++ b/src/network/networkd-setlink.c
@@ -591,18 +591,10 @@ int link_request_to_set_addrgen_mode(Link *link) {
mode = IN6_ADDR_GEN_MODE_NONE;
else if (link->network->ipv6ll_address_gen_mode >= 0)
mode = link->network->ipv6ll_address_gen_mode;
- else {
- r = sysctl_read_ip_property(AF_INET6, link->ifname, "stable_secret", NULL);
- if (r < 0) {
- /* The file may not exist. And even if it exists, when stable_secret is unset,
- * reading the file fails with ENOMEM when read_full_virtual_file(), which uses
- * read() as the backend, and EIO when read_one_line_file() which uses fgetc(). */
- log_link_debug_errno(link, r, "Failed to read sysctl property stable_secret, ignoring: %m");
-
- mode = IN6_ADDR_GEN_MODE_EUI64;
- } else
- mode = IN6_ADDR_GEN_MODE_STABLE_PRIVACY;
- }
+ else if (in6_addr_is_set(&link->network->ipv6ll_stable_secret))
+ mode = IN6_ADDR_GEN_MODE_STABLE_PRIVACY;
+ else
+ mode = IN6_ADDR_GEN_MODE_EUI64;
r = link_request_set_link(link, SET_LINK_ADDRESS_GENERATION_MODE, link_set_addrgen_mode_handler, &req);
if (r < 0)
diff --git a/src/network/networkd-sysctl.c b/src/network/networkd-sysctl.c
index a67e10e8125..6c7a606dfb5 100644
--- a/src/network/networkd-sysctl.c
+++ b/src/network/networkd-sysctl.c
@@ -11,6 +11,9 @@
#include "string-table.h"
#include "sysctl-util.h"
+#define STABLE_SECRET_APP_ID_1 SD_ID128_MAKE(aa,05,1d,94,43,68,45,07,b9,73,f1,e8,e4,b7,34,52)
+#define STABLE_SECRET_APP_ID_2 SD_ID128_MAKE(52,c4,40,a0,9f,2f,48,58,a9,3a,f6,29,25,ba,7a,7d)
+
static int link_update_ipv6_sysctl(Link *link) {
assert(link);
@@ -211,6 +214,48 @@ int link_set_ipv6_mtu(Link *link) {
return sysctl_write_ip_property_uint32(AF_INET6, link->ifname, "mtu", mtu);
}
+static int link_set_ipv6ll_stable_secret(Link *link) {
+ _cleanup_free_ char *str = NULL;
+ struct in6_addr a;
+ int r;
+
+ assert(link);
+ assert(link->network);
+
+ if (link->network->ipv6ll_address_gen_mode != IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_STABLE_PRIVACY)
+ return 0;
+
+ if (in6_addr_is_set(&link->network->ipv6ll_stable_secret))
+ a = link->network->ipv6ll_stable_secret;
+ else {
+ sd_id128_t key;
+ le64_t v;
+
+ /* Generate a stable secret address from machine-ID and the interface name. */
+
+ r = sd_id128_get_machine_app_specific(STABLE_SECRET_APP_ID_1, &key);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to generate key: %m");
+
+ v = htole64(siphash24_string(link->ifname, key.bytes));
+ memcpy(a.s6_addr, &v, sizeof(v));
+
+ r = sd_id128_get_machine_app_specific(STABLE_SECRET_APP_ID_2, &key);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to generate key: %m");
+
+ v = htole64(siphash24_string(link->ifname, key.bytes));
+ assert_cc(sizeof(v) * 2 == sizeof(a.s6_addr));
+ memcpy(a.s6_addr + sizeof(v), &v, sizeof(v));
+ }
+
+ r = in6_addr_to_string(&a, &str);
+ if (r < 0)
+ return r;
+
+ return sysctl_write_ip_property(AF_INET6, link->ifname, "stable_secret", str);
+}
+
static int link_set_ipv4_accept_local(Link *link) {
assert(link);
@@ -282,6 +327,10 @@ int link_set_sysctl(Link *link) {
if (r < 0)
log_link_warning_errno(link, r, "Cannot set IPv6 MTU, ignoring: %m");
+ r = link_set_ipv6ll_stable_secret(link);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set stable secret address for IPv6 link local address: %m");
+
r = link_set_ipv4_accept_local(link);
if (r < 0)
log_link_warning_errno(link, r, "Cannot set IPv4 accept_local flag for interface, ignoring: %m");
diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network
index 195ed8f0cc8..f1305a04b2b 100644
--- a/test/fuzz/fuzz-network-parser/directives.network
+++ b/test/fuzz/fuzz-network-parser/directives.network
@@ -222,6 +222,7 @@ L2TP=
MACsec=
LinkLocalAddressing=
IPv6LinkLocalAddressGenerationMode=
+IPv6StableSecretAddress=
ConfigureWithoutCarrier=
NTP=
DHCP=
diff --git a/test/test-network/conf/25-link-local-addressing-yes.network b/test/test-network/conf/25-link-local-addressing-yes.network
index 8268da5aa86..f21d4d2328c 100644
--- a/test/test-network/conf/25-link-local-addressing-yes.network
+++ b/test/test-network/conf/25-link-local-addressing-yes.network
@@ -4,3 +4,4 @@ Name=test1
[Network]
LinkLocalAddressing=yes
IPv6AcceptRA=no
+IPv6StableSecretAddress=0123:4567:89ab:cdef:0123:4567:89ab:cdef
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index f55020db7c6..33090b565d8 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -2619,24 +2619,9 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities):
3: generate stable privacy addresses, using a random secret if unset
'''
- test1_addr_gen_mode = ''
- if os.path.exists(os.path.join(os.path.join(network_sysctl_ipv6_path, 'test1'), 'stable_secret')):
- with open(os.path.join(os.path.join(network_sysctl_ipv6_path, 'test1'), 'stable_secret')) as f:
- try:
- f.readline()
- except IOError:
- # if stable_secret is unset, then EIO is returned
- test1_addr_gen_mode = '0'
- else:
- test1_addr_gen_mode = '2'
- else:
- test1_addr_gen_mode = '0'
-
- if os.path.exists(os.path.join(os.path.join(network_sysctl_ipv6_path, 'test1'), 'addr_gen_mode')):
- self.assertEqual(read_ipv6_sysctl_attr('test1', 'addr_gen_mode'), test1_addr_gen_mode)
-
- if os.path.exists(os.path.join(os.path.join(network_sysctl_ipv6_path, 'dummy98'), 'addr_gen_mode')):
- self.assertEqual(read_ipv6_sysctl_attr('dummy98', 'addr_gen_mode'), '1')
+ self.assertEqual(read_ipv6_sysctl_attr('test1', 'stable_secret'), '0123:4567:89ab:cdef:0123:4567:89ab:cdef')
+ self.assertEqual(read_ipv6_sysctl_attr('test1', 'addr_gen_mode'), '2')
+ self.assertEqual(read_ipv6_sysctl_attr('dummy98', 'addr_gen_mode'), '1')
def test_link_local_addressing_remove_ipv6ll(self):
copy_unit_to_networkd_unit_path('26-link-local-addressing-ipv6.network', '12-dummy.netdev')