]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
IPv6 RA: Support the Retrans Timer field (IPv6 Conformance Test: v6LC.2.1.5)
authorMatt Muggeridge <Matt.Muggeridge2@hpe.com>
Mon, 22 Jan 2024 09:55:41 +0000 (19:55 +1000)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Mon, 22 Jan 2024 18:18:01 +0000 (03:18 +0900)
The RA's Retransmission Timer field was being ignored. This resolves the IPv6
Core Conformance test, v6LC.2.1.5 [1].

Retransmission Timer is a 32-bit unsigned integer. The time, in milliseconds,
between retransmitted Neighbor Solicitation messages. Used by the Address
Resolution and Neighbor Unreachability Detection (NUD) algorithm.

Support setting a default value for the neighbour retransmission timer value with:

    [Network]
    IPv6RetransmissionTimeSec=<int>

By default, upon receiving a Router Advertisement with the Retransmission Timer
field set to a non-zero value, it will update the kernel's retransmit timer value.
To disable this behaviour, configure the UseIPv6RetransmissionTime= under the
[IPv6AcceptRA] section.

    [IPv6AcceptRA]
    UseIPv6RetransmissionTime=<bool>

RFC4861: Neighbor Discovery in IPv6
  * Section 4.2 RA Message Format.
  * Section 6.3.4 Processing Received Router Advertisements

A Router Advertisement field (e.g., Cur Hop Limit, Reachable Time,
and Retrans Timer) may contain a value denoting that it is
unspecified. In such cases, the parameter should be ignored and the
host should continue using whatever value it is already using. In
particular, a host MUST NOT interpret the unspecified value as
meaning change back to the default value that was in use before the
first Router Advertisement was received.

The RetransTimer variable SHOULD be copied from the Retrans Timer
field, if the received value is non-zero.

References
[1] IPv6 Core Conformance Spec (PDF)

21 files changed:
man/systemd.network.xml
src/basic/sysctl-util.c
src/basic/sysctl-util.h
src/libsystemd-network/ndisc-router.c
src/libsystemd-network/ndisc-router.h
src/libsystemd-network/test-ndisc-rs.c
src/network/networkd-ndisc.c
src/network/networkd-network-gperf.gperf
src/network/networkd-network.c
src/network/networkd-network.h
src/network/networkd-sysctl.c
src/systemd/sd-ndisc.h
test/test-network/conf/25-dummy.netdev [new file with mode: 0644]
test/test-network/conf/25-dummy.network [new file with mode: 0644]
test/test-network/conf/25-ipv6-neigh-retrans-time-0s.network [new file with mode: 0644]
test/test-network/conf/25-ipv6-neigh-retrans-time-3s.network [new file with mode: 0644]
test/test-network/conf/25-ipv6-neigh-retrans-time-4s.network [new file with mode: 0644]
test/test-network/conf/25-ipv6-neigh-retrans-time-infinity.network [new file with mode: 0644]
test/test-network/conf/25-ipv6-neigh-retrans-time-invalid.network [new file with mode: 0644]
test/test-network/conf/25-ipv6-neigh-retrans-time-toobig.network [new file with mode: 0644]
test/test-network/systemd-networkd-tests.py

index ef4a0fd4305278ea578aa24839a906e54f183bea..c1b0eec1a72c784ed7fa3148e2f3e5b188d4f430 100644 (file)
@@ -882,6 +882,18 @@ Table=1234</programlisting></para>
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>IPv6RetransmissionTimeSec=</varname></term>
+        <listitem>
+          <para>Configures IPv6 Retransmission Time. The time between retransmitted Neighbor
+          Solicitation messages. Used by address resolution and the Neighbor Unreachability
+          Detection algorithm. A value of zero is ignored and the kernel's current value
+          will be used. Defaults to unset, and the kernel's current value will be used.</para>
+
+          <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>IPv4ReversePathFilter=</varname></term>
         <listitem>
@@ -3306,6 +3318,18 @@ Token=prefixstable:2002:da8:1::</programlisting></para>
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>UseRetransmissionTime=</varname></term>
+        <listitem>
+          <para>Takes a boolean. When true, the retransmission time received in the Router Advertisement will be set
+          on the interface receiving the advertisement. It is used as the time between retransmissions of Neighbor
+          Solicitation messages to a neighbor when resolving the address or when probing the reachability of a neighbor.
+          Defaults to true.</para>
+
+          <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>UseICMP6RateLimit=</varname></term>
         <listitem>
index b66a6622ae7c6f8258ee9c6c68ca4c8636447e60..9a1933f57924c1f9d3bce8ff632b48df7c655fca 100644 (file)
@@ -96,6 +96,26 @@ int sysctl_write_ip_property(int af, const char *ifname, const char *property, c
         return sysctl_write(p, value);
 }
 
+int sysctl_write_ip_neighbor_property(int af, const char *ifname, const char *property, const char *value) {
+        const char *p;
+
+        assert(property);
+        assert(value);
+        assert(ifname);
+
+        if (!IN_SET(af, AF_INET, AF_INET6))
+                return -EAFNOSUPPORT;
+
+        if (ifname) {
+                if (!ifname_valid_full(ifname, IFNAME_VALID_SPECIAL))
+                        return -EINVAL;
+                p = strjoina("net/", af_to_ipv4_ipv6(af), "/neigh/", ifname, "/", property);
+        } else
+                p = strjoina("net/", af_to_ipv4_ipv6(af), "/neigh/default/", property);
+
+        return sysctl_write(p, value);
+}
+
 int sysctl_read(const char *property, char **ret) {
         char *p;
         int r;
index 32364196f97a43c9875667309e961e49542c8e4b..7192e8c0b0a5e9d18a608ad18f76bc814d44caf3 100644 (file)
@@ -19,6 +19,13 @@ static inline int sysctl_write_ip_property_boolean(int af, const char *ifname, c
         return sysctl_write_ip_property(af, ifname, property, one_zero(value));
 }
 
+int sysctl_write_ip_neighbor_property(int af, const char *ifname, const char *property, const char *value);
+static inline int sysctl_write_ip_neighbor_property_uint32(int af, const char *ifname, const char *property, uint32_t value) {
+        char buf[DECIMAL_STR_MAX(uint32_t)];
+        xsprintf(buf, "%u", value);
+        return sysctl_write_ip_neighbor_property(af, ifname, property, buf);
+}
+
 #define DEFINE_SYSCTL_WRITE_IP_PROPERTY(name, type, format)           \
         static inline int sysctl_write_ip_property_##name(int af, const char *ifname, const char *property, type value) { \
                 char buf[DECIMAL_STR_MAX(type)];                        \
index 5162df799c3e89adde494da0eb593a1bdf2327d1..89681d00758b9a364d0bffe07cb4923dee77b30f 100644 (file)
@@ -144,6 +144,7 @@ int ndisc_router_parse(sd_ndisc *nd, sd_ndisc_router *rt) {
         rt->flags = a->nd_ra_flags_reserved; /* the first 8 bits */
         rt->lifetime_usec = be16_sec_to_usec(a->nd_ra_router_lifetime, /* max_as_infinity = */ false);
         rt->icmp6_ratelimit_usec = be32_msec_to_usec(a->nd_ra_retransmit, /* max_as_infinity = */ false);
+        rt->retransmission_time_usec = be32_msec_to_usec(a->nd_ra_retransmit, /* max_as_infinity = */ false);
 
         rt->preference = (rt->flags >> 3) & 3;
         if (!IN_SET(rt->preference, SD_NDISC_PREFERENCE_LOW, SD_NDISC_PREFERENCE_HIGH))
@@ -275,6 +276,14 @@ int sd_ndisc_router_get_hop_limit(sd_ndisc_router *rt, uint8_t *ret) {
         return 0;
 }
 
+int sd_ndisc_router_get_retransmission_time(sd_ndisc_router *rt, uint64_t *ret) {
+        assert_return(rt, -EINVAL);
+        assert_return(ret, -EINVAL);
+
+        *ret = rt->retransmission_time_usec;
+        return 0;
+}
+
 int sd_ndisc_router_get_icmp6_ratelimit(sd_ndisc_router *rt, uint64_t *ret) {
         assert_return(rt, -EINVAL);
         assert_return(ret, -EINVAL);
index 0a55e1ac57bdf8e34c379847bdf5eaf53c2ee20d..63d4f90ba909973c9c7ef814c75b444d1adf9fa4 100644 (file)
@@ -24,6 +24,7 @@ struct sd_ndisc_router {
         uint64_t flags;
         unsigned preference;
         uint64_t lifetime_usec;
+        usec_t retransmission_time_usec;
 
         uint8_t hop_limit;
         uint32_t mtu;
index d94cc1ceb74ad8672657c397ce1dbb69d4e8cad2..d0648b95fabf165b7ac8f8357ea0042d2f430bbf 100644 (file)
@@ -28,7 +28,7 @@ static sd_ndisc *test_timeout_nd;
 static void router_dump(sd_ndisc_router *rt) {
         struct in6_addr addr;
         uint8_t hop_limit;
-        usec_t t, lifetime;
+        usec_t t, lifetime, retrans_time;
         uint64_t flags;
         uint32_t mtu;
         unsigned preference;
@@ -65,6 +65,9 @@ static void router_dump(sd_ndisc_router *rt) {
         assert_se(sd_ndisc_router_get_lifetime_timestamp(rt, CLOCK_REALTIME, &t) >= 0);
         log_info("Lifetime: %s (%s)", FORMAT_TIMESPAN(lifetime, USEC_PER_SEC), FORMAT_TIMESTAMP(t));
 
+        assert_se(sd_ndisc_router_get_retransmission_time(rt, &retrans_time) >= 0);
+        log_info("Retransmission Time: %s", FORMAT_TIMESPAN(retrans_time, USEC_PER_SEC));
+
         if (sd_ndisc_router_get_mtu(rt, &mtu) < 0)
                 log_info("No MTU set");
         else
index 9f56fd23d4484bd64b6c2bd03154afc3fde0f856..ca8bbb2d5ef6766b1c6ddd8f6e4980fc25ef693b 100644 (file)
@@ -393,6 +393,42 @@ static int ndisc_router_process_icmp6_ratelimit(Link *link, sd_ndisc_router *rt)
         return 0;
 }
 
+static int ndisc_router_process_retransmission_time(Link *link, sd_ndisc_router *rt) {
+        usec_t retrans_time, msec;
+        int r;
+
+        assert(link);
+        assert(link->network);
+        assert(rt);
+
+        if (!link->network->ipv6_accept_ra_use_retransmission_time)
+                return 0;
+
+        r = sd_ndisc_router_get_retransmission_time(rt, &retrans_time);
+        if (r < 0) {
+                log_link_debug_errno(link, r, "Failed to get retransmission time from RA, ignoring: %m");
+                return 0;
+        }
+
+        /* 0 is the unspecified value and must not be set (see RFC4861, 6.3.4) */
+        if (!timestamp_is_set(retrans_time))
+                return 0;
+
+        msec = DIV_ROUND_UP(retrans_time, USEC_PER_MSEC);
+        if (msec <= 0 || msec > UINT32_MAX) {
+                log_link_debug(link, "Failed to get retransmission time from RA - out of range (%"PRIu64"), ignoring", msec);
+                return 0;
+        }
+
+        /* Set the retransmission time for Neigbor Solicitations. */
+        r = sysctl_write_ip_neighbor_property_uint32(AF_INET6, link->ifname, "retrans_time_ms", (uint32_t) msec);
+        if (r < 0)
+                log_link_warning_errno(
+                        link, r, "Failed to apply neighbor retransmission time (%"PRIu64"), ignoring: %m", msec);
+
+        return 0;
+}
+
 static int ndisc_router_process_autonomous_prefix(Link *link, sd_ndisc_router *rt) {
         usec_t lifetime_valid_usec, lifetime_preferred_usec;
         _cleanup_set_free_ Set *addresses = NULL;
@@ -1354,6 +1390,10 @@ static int ndisc_router_handler(Link *link, sd_ndisc_router *rt) {
         if (r < 0)
                 return r;
 
+        r = ndisc_router_process_retransmission_time(link, rt);
+        if (r < 0)
+                return r;
+
         r = ndisc_router_process_options(link, rt);
         if (r < 0)
                 return r;
index f0650a0c8869f8995544986bc366093579270e05..ce3745093808c0d24ca69faaa1e42aacf9097da4 100644 (file)
@@ -131,6 +131,7 @@ Network.IPv6AcceptRA,                        config_parse_tristate,
 Network.IPv6AcceptRouterAdvertisements,      config_parse_tristate,                                    0,                             offsetof(Network, ipv6_accept_ra)
 Network.IPv6DuplicateAddressDetection,       config_parse_int,                                         0,                             offsetof(Network, ipv6_dad_transmits)
 Network.IPv6HopLimit,                        config_parse_uint8,                                       0,                             offsetof(Network, ipv6_hop_limit)
+Network.IPv6RetransmissionTimeSec,           config_parse_sec,                                         0,                             offsetof(Network, ipv6_retransmission_time)
 Network.IPv6ProxyNDP,                        config_parse_tristate,                                    0,                             offsetof(Network, ipv6_proxy_ndp)
 Network.IPv6MTUBytes,                        config_parse_mtu,                                         AF_INET6,                      offsetof(Network, ipv6_mtu)
 Network.IPv4AcceptLocal,                     config_parse_tristate,                                    0,                             offsetof(Network, ipv4_accept_local)
@@ -297,6 +298,7 @@ IPv6AcceptRA.UseDNS,                         config_parse_bool,
 IPv6AcceptRA.UseDomains,                     config_parse_ipv6_accept_ra_use_domains,                  0,                             offsetof(Network, ipv6_accept_ra_use_domains)
 IPv6AcceptRA.UseMTU,                         config_parse_bool,                                        0,                             offsetof(Network, ipv6_accept_ra_use_mtu)
 IPv6AcceptRA.UseHopLimit,                    config_parse_bool,                                        0,                             offsetof(Network, ipv6_accept_ra_use_hop_limit)
+IPv6AcceptRA.UseRetransmissionTime,          config_parse_bool,                                        0,                             offsetof(Network, ipv6_accept_ra_use_retransmission_time)
 IPv6AcceptRA.UseICMP6RateLimit,              config_parse_bool,                                        0,                             offsetof(Network, ipv6_accept_ra_use_icmp6_ratelimit)
 IPv6AcceptRA.DHCPv6Client,                   config_parse_ipv6_accept_ra_start_dhcp6_client,           0,                             offsetof(Network, ipv6_accept_ra_start_dhcp6_client)
 IPv6AcceptRA.RouteTable,                     config_parse_dhcp_or_ra_route_table,                      AF_INET6,                      0
index 08c7da56997422a82a36ed9cc9f7b9f0b3880f21..16c679b34388e640106c9b2c05f1884f709c6826 100644 (file)
@@ -483,6 +483,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi
                 .ipv6_accept_ra_use_onlink_prefix = true,
                 .ipv6_accept_ra_use_mtu = true,
                 .ipv6_accept_ra_use_hop_limit = true,
+                .ipv6_accept_ra_use_retransmission_time = true,
                 .ipv6_accept_ra_use_icmp6_ratelimit = true,
                 .ipv6_accept_ra_route_table = RT_TABLE_MAIN,
                 .ipv6_accept_ra_route_metric_high = IPV6RA_ROUTE_METRIC_HIGH,
index 1d7a7da798214977de3a4d6391b504334732ba62..3ab115c3b9b52abdeb77c1562afc18f497186222 100644 (file)
@@ -324,6 +324,7 @@ struct Network {
         int ipv4_route_localnet;
         int ipv6_dad_transmits;
         uint8_t ipv6_hop_limit;
+        usec_t ipv6_retransmission_time;
         int proxy_arp;
         int proxy_arp_pvlan;
         uint32_t ipv6_mtu;
@@ -341,6 +342,7 @@ struct Network {
         bool ipv6_accept_ra_use_onlink_prefix;
         bool ipv6_accept_ra_use_mtu;
         bool ipv6_accept_ra_use_hop_limit;
+        bool ipv6_accept_ra_use_retransmission_time;
         bool ipv6_accept_ra_use_icmp6_ratelimit;
         bool ipv6_accept_ra_quickack;
         bool ipv6_accept_ra_use_captive_portal;
index 9d188c022e0bde093542580b6dddcdd50479e5d3..8fa0ede5c2aee0057f4ce06dc905ae1006bf4770 100644 (file)
@@ -179,6 +179,24 @@ static int link_set_ipv6_hop_limit(Link *link) {
         return sysctl_write_ip_property_int(AF_INET6, link->ifname, "hop_limit", link->network->ipv6_hop_limit);
 }
 
+static int link_set_ipv6_retransmission_time(Link *link) {
+        usec_t retrans_time_ms;
+
+        assert(link);
+
+        if (!link_is_configured_for_family(link, AF_INET6))
+                return 0;
+
+        if (!timestamp_is_set(link->network->ipv6_retransmission_time))
+                return 0;
+
+        retrans_time_ms = DIV_ROUND_UP(link->network->ipv6_retransmission_time, USEC_PER_MSEC);
+         if (retrans_time_ms <= 0 || retrans_time_ms > UINT32_MAX)
+                return 0;
+
+        return sysctl_write_ip_neighbor_property_uint32(AF_INET6, link->ifname, "retrans_time_ms", retrans_time_ms);
+}
+
 static int link_set_ipv6_proxy_ndp(Link *link) {
         bool v;
 
@@ -297,6 +315,10 @@ int link_set_sysctl(Link *link) {
         if (r < 0)
                 log_link_warning_errno(link, r, "Cannot set IPv6 hop limit for interface, ignoring: %m");
 
+        r = link_set_ipv6_retransmission_time(link);
+        if (r < 0)
+                log_link_warning_errno(link, r, "Cannot set IPv6 retransmission time for interface, ignoring: %m");
+
         r = link_set_ipv6_proxy_ndp(link);
         if (r < 0)
                 log_link_warning_errno(link, r, "Cannot set IPv6 proxy NDP, ignoring: %m");
index 3f93e3a40682849f673ae689b77fad030cba739a..a5ccd5f6449107585406e701f63833366bee28ed 100644 (file)
@@ -96,6 +96,7 @@ int sd_ndisc_router_get_flags(sd_ndisc_router *rt, uint64_t *ret);
 int sd_ndisc_router_get_preference(sd_ndisc_router *rt, unsigned *ret);
 int sd_ndisc_router_get_lifetime(sd_ndisc_router *rt, uint64_t *ret);
 int sd_ndisc_router_get_lifetime_timestamp(sd_ndisc_router *rt, clockid_t clock, uint64_t *ret);
+int sd_ndisc_router_get_retransmission_time(sd_ndisc_router *rt, uint64_t *ret);
 int sd_ndisc_router_get_mtu(sd_ndisc_router *rt, uint32_t *ret);
 
 /* Generic option access */
diff --git a/test/test-network/conf/25-dummy.netdev b/test/test-network/conf/25-dummy.netdev
new file mode 100644 (file)
index 0000000..d7cf7b4
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[NetDev]
+Name=test25
+Kind=dummy
diff --git a/test/test-network/conf/25-dummy.network b/test/test-network/conf/25-dummy.network
new file mode 100644 (file)
index 0000000..a6e93fd
--- /dev/null
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=test25
+
+[Network]
+IPv6AcceptRA=no
diff --git a/test/test-network/conf/25-ipv6-neigh-retrans-time-0s.network b/test/test-network/conf/25-ipv6-neigh-retrans-time-0s.network
new file mode 100644 (file)
index 0000000..04c7c49
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=test25
+
+[Network]
+IPv6AcceptRA=no
+IPv6RetransmissionTimeSec=0
diff --git a/test/test-network/conf/25-ipv6-neigh-retrans-time-3s.network b/test/test-network/conf/25-ipv6-neigh-retrans-time-3s.network
new file mode 100644 (file)
index 0000000..b4dbd06
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=test25
+
+[Network]
+IPv6AcceptRA=no
+IPv6RetransmissionTimeSec=3
diff --git a/test/test-network/conf/25-ipv6-neigh-retrans-time-4s.network b/test/test-network/conf/25-ipv6-neigh-retrans-time-4s.network
new file mode 100644 (file)
index 0000000..cbdf4f3
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=test25
+
+[Network]
+IPv6AcceptRA=no
+IPv6RetransmissionTimeSec=4
diff --git a/test/test-network/conf/25-ipv6-neigh-retrans-time-infinity.network b/test/test-network/conf/25-ipv6-neigh-retrans-time-infinity.network
new file mode 100644 (file)
index 0000000..085cb30
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=test25
+
+[Network]
+IPv6AcceptRA=no
+IPv6RetransmissionTimeSec=infinity
diff --git a/test/test-network/conf/25-ipv6-neigh-retrans-time-invalid.network b/test/test-network/conf/25-ipv6-neigh-retrans-time-invalid.network
new file mode 100644 (file)
index 0000000..8a0bf83
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=test25
+
+[Network]
+IPv6AcceptRA=no
+IPv6RetransmissionTimeSec=-2
diff --git a/test/test-network/conf/25-ipv6-neigh-retrans-time-toobig.network b/test/test-network/conf/25-ipv6-neigh-retrans-time-toobig.network
new file mode 100644 (file)
index 0000000..0976bae
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=test25
+
+[Network]
+IPv6AcceptRA=no
+IPv6RetransmissionTimeSec=999999999999999999
index 3bb0166f4eee6304f3c82efe4617f7d224f56c8c..064ca53193b47b33c2032b36c947c9a2231a70c8 100755 (executable)
@@ -4,6 +4,16 @@
 
 # These tests can be executed in the systemd mkosi image when booted in QEMU. After booting the QEMU VM,
 # simply run this file which can be found in the VM at /usr/lib/systemd/tests/testdata/test-network/systemd-networkd-tests.py.
+#
+# To run an individual test, specify it as a command line argument in the form
+# of <class>.<test_function>. E.g. the NetworkdMTUTests class has a test
+# function called test_ipv6_mtu().  To run just that test use:
+#
+#    sudo ./systemd-networkd-tests.py NetworkdMTUTests.test_ipv6_mtu
+#
+# Similarly, other indivdual tests can be run, eg.:
+#
+#    sudo ./systemd-networkd-tests.py NetworkdNetworkTests.test_ipv6_neigh_retrans_time
 
 import argparse
 import datetime
@@ -582,9 +592,16 @@ def read_ip_sysctl_attr(link, attribute, ipv):
     with open(os.path.join('/proc/sys/net', ipv, 'conf', link, attribute), encoding='utf-8') as f:
         return f.readline().strip()
 
+def read_ip_neigh_sysctl_attr(link, attribute, ipv):
+    with open(os.path.join('/proc/sys/net', ipv, 'neigh', link, attribute), encoding='utf-8') as f:
+        return f.readline().strip()
+
 def read_ipv6_sysctl_attr(link, attribute):
     return read_ip_sysctl_attr(link, attribute, 'ipv6')
 
+def read_ipv6_neigh_sysctl_attr(link, attribute):
+    return read_ip_neigh_sysctl_attr(link, attribute, 'ipv6')
+
 def read_ipv4_sysctl_attr(link, attribute):
     return read_ip_sysctl_attr(link, attribute, 'ipv4')
 
@@ -915,6 +932,9 @@ class Utilities():
     def check_ipv6_sysctl_attr(self, link, attribute, expected):
         self.assertEqual(read_ipv6_sysctl_attr(link, attribute), expected)
 
+    def check_ipv6_neigh_sysctl_attr(self, link, attribute, expected):
+        self.assertEqual(read_ipv6_neigh_sysctl_attr(link, attribute), expected)
+
     def wait_links(self, *links, timeout=20, fail_assert=True):
         def links_exist(*links):
             for link in links:
@@ -3505,6 +3525,56 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities):
         for i in range(1, 5):
             self.assertRegex(output, f'2607:5300:203:5215:{i}::1 *proxy')
 
+    def test_ipv6_neigh_retrans_time(self):
+        link='test25'
+        copy_network_unit('25-dummy.netdev', '25-dummy.network')
+        start_networkd()
+
+        self.wait_online([f'{link}:degraded'])
+        remove_network_unit('25-dummy.network')
+
+        # expect retrans_time_ms updated
+        copy_network_unit('25-ipv6-neigh-retrans-time-3s.network')
+        networkctl_reload()
+        self.wait_online([f'{link}:degraded'])
+        self.check_ipv6_neigh_sysctl_attr(link, 'retrans_time_ms', '3000')
+        remove_network_unit('25-ipv6-neigh-retrans-time-3s.network')
+
+        # expect retrans_time_ms unchanged
+        copy_network_unit('25-ipv6-neigh-retrans-time-0s.network')
+        networkctl_reload()
+        self.wait_online([f'{link}:degraded'])
+        self.check_ipv6_neigh_sysctl_attr(link, 'retrans_time_ms', '3000')
+        remove_network_unit('25-ipv6-neigh-retrans-time-0s.network')
+
+        # expect retrans_time_ms unchanged
+        copy_network_unit('25-ipv6-neigh-retrans-time-toobig.network')
+        networkctl_reload()
+        self.wait_online([f'{link}:degraded'])
+        self.check_ipv6_neigh_sysctl_attr(link, 'retrans_time_ms', '3000')
+        remove_network_unit('25-ipv6-neigh-retrans-time-toobig.network')
+
+        # expect retrans_time_ms unchanged
+        copy_network_unit('25-ipv6-neigh-retrans-time-infinity.network')
+        networkctl_reload()
+        self.wait_online([f'{link}:degraded'])
+        self.check_ipv6_neigh_sysctl_attr(link, 'retrans_time_ms', '3000')
+        remove_network_unit('25-ipv6-neigh-retrans-time-infinity.network')
+
+        # expect retrans_time_ms unchanged
+        copy_network_unit('25-ipv6-neigh-retrans-time-invalid.network')
+        networkctl_reload()
+        self.wait_online([f'{link}:degraded'])
+        self.check_ipv6_neigh_sysctl_attr(link, 'retrans_time_ms', '3000')
+        remove_network_unit('25-ipv6-neigh-retrans-time-invalid.network')
+
+        # expect retrans_time_ms updated
+        copy_network_unit('25-ipv6-neigh-retrans-time-4s.network')
+        networkctl_reload()
+        self.wait_online([f'{link}:degraded'])
+        self.check_ipv6_neigh_sysctl_attr(link, 'retrans_time_ms', '4000')
+        remove_network_unit('25-ipv6-neigh-retrans-time-4s.network')
+
     def test_neighbor(self):
         copy_network_unit('12-dummy.netdev', '25-neighbor-dummy.network', '25-neighbor-dummy.network.d/10-step1.conf',
                           '25-gre-tunnel-remote-any.netdev', '25-neighbor-ip.network',