]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
network: add support for HSR netdev
authorBeniamino Galvani <b.galvani@gmail.com>
Mon, 31 Mar 2025 19:44:50 +0000 (21:44 +0200)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Fri, 4 Apr 2025 20:23:53 +0000 (05:23 +0900)
Add support for creating HSR/PRP interfaces. HSR (High-availability Seamless
Redundancy) and PRP (Parallel Redundancy Protocol) are two protocols that
provide seamless failover against failure of any single network component. They
are both implemented by the "hsr" kernel driver.

man/systemd.netdev.xml
src/libsystemd/sd-netlink/netlink-types-rtnl.c
src/network/meson.build
src/network/netdev/hsr.c [new file with mode: 0644]
src/network/netdev/hsr.h [new file with mode: 0644]
src/network/netdev/netdev-gperf.gperf
src/network/netdev/netdev.c
src/network/netdev/netdev.h
test/test-network/conf/25-hsr.netdev [new file with mode: 0644]
test/test-network/conf/25-hsr.network [new file with mode: 0644]
test/test-network/systemd-networkd-tests.py

index fe7342bfd0412f56b4cd83794904d128679d1b0a..9ea59a7763344f91921051a5bdd1780fa6d48381 100644 (file)
           <row><entry><varname>geneve</varname></entry>
           <entry>A GEneric NEtwork Virtualization Encapsulation (GENEVE) netdev driver.</entry></row>
 
+          <row><entry><varname>hsr</varname></entry>
+          <entry>A High-availability Seamless Redundancy (HSR) or Parallel Redundancy Protocol (PRP) interface. HSR and PRP are two protocols defined by the IEC 62439-3 standard, providing seamless failover against failure of any single network component.</entry></row>
+
           <row><entry><varname>l2tp</varname></entry>
           <entry>A Layer 2 Tunneling Protocol (L2TP) is a tunneling protocol used to support virtual private networks (VPNs) or as part of the delivery of services by ISPs. It does not provide any encryption or confidentiality by itself</entry></row>
 
     </variablelist>
   </refsect1>
 
+  <refsect1>
+    <title>[HSR] Section Options</title>
+
+    <para>The [HSR] section only applies for
+    netdevs of kind <literal>hsr</literal>, and accepts the
+    following keys:</para>
+
+    <variablelist class='network-directives'>
+      <varlistentry>
+        <term><varname>Ports=</varname></term>
+        <listitem>
+          <para>Specifies the underlying interfaces. This field is mandatory and must contain exactly two
+          interface names separated by space. This option can be specified multiples times, hence the two cases below have the same result:
+          <programlisting>Ports=eth1 eth2</programlisting>
+          <programlisting>Ports=eth1
+Ports=eth2</programlisting>
+          All the previous assignments are cleared when an empty string is specified.
+          </para>
+
+          <xi:include href="version-info.xml" xpointer="v258"/>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term><varname>Protocol=</varname></term>
+        <listitem>
+          <para>Specifies the protocol used by the interface. Takes one of <literal>hsr</literal> or
+          <literal>prp</literal>. Defaults to <literal>hsr</literal>.</para>
+
+          <para>Both protocols work by sending two copies of every outgoing frame, one for each of the two
+          ports. The destination node receives the two frames and and keeps only the first one. If a link
+          fails, only one of the two frames is received. HSR uses a ring topology where the two outgoing
+          frames are sent in opposite directions in the ring. PRP doesn't need a specific topology, but it
+          requires two completely redundant networks attached to the two ports.</para>
+
+          <xi:include href="version-info.xml" xpointer="v258"/>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term><varname>Supervision=</varname></term>
+        <listitem>
+          <para>Specifies the last byte of the destination MAC address of supervision frames. Takes a number
+          between 0 and 255. Defaults to 0. Supervision frames are used by the HSR and the PRP protocols to
+          monitor the integrity of the network and the presence of nodes. The first 5 bytes of the
+          destination MAC are always 01:15:4E:00:01 while the last byte is configurable.
+          </para>
+
+          <xi:include href="version-info.xml" xpointer="v258"/>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
   <refsect1>
     <title>[BareUDP] Section Options</title>
 
index d0d8bfe9291a69020e5cffd523a955208cc7bba0..57a38ecb4bcd28ec95596ae20f49fd24be116192 100644 (file)
@@ -239,6 +239,13 @@ static const NLAPolicy rtnl_link_info_data_gre_policies[] = {
         [IFLA_GRE_ERSPAN_HWID]      = BUILD_POLICY(U16),
 };
 
+static const NLAPolicy rtnl_link_info_data_hsr_policies[] = {
+        [IFLA_HSR_SLAVE1]           = BUILD_POLICY(U32),
+        [IFLA_HSR_SLAVE2]           = BUILD_POLICY(U32),
+        [IFLA_HSR_MULTICAST_SPEC]   = BUILD_POLICY(U8),
+        [IFLA_HSR_PROTOCOL]         = BUILD_POLICY(U8),
+};
+
 static const NLAPolicy rtnl_link_info_data_ipoib_policies[] = {
         [IFLA_IPOIB_PKEY]           = BUILD_POLICY(U16),
         [IFLA_IPOIB_MODE]           = BUILD_POLICY(U16),
@@ -413,8 +420,8 @@ static const NLAPolicySetUnionElement rtnl_link_info_data_policy_set_union_eleme
         BUILD_UNION_ELEMENT_BY_STRING("gretap",    rtnl_link_info_data_gre),
 /*
         BUILD_UNION_ELEMENT_BY_STRING("gtp",       rtnl_link_info_data_gtp),
-        BUILD_UNION_ELEMENT_BY_STRING("hsr",       rtnl_link_info_data_hsr),
 */
+        BUILD_UNION_ELEMENT_BY_STRING("hsr",       rtnl_link_info_data_hsr),
         BUILD_UNION_ELEMENT_BY_STRING("ip6erspan", rtnl_link_info_data_gre),
         BUILD_UNION_ELEMENT_BY_STRING("ip6gre",    rtnl_link_info_data_gre),
         BUILD_UNION_ELEMENT_BY_STRING("ip6gretap", rtnl_link_info_data_gre),
index 5bcce39738cf50ee622b05423cab1d302f6dbde9..c6352f79c483c1bcad114e09a3ad5bf9d7c94c31 100644 (file)
@@ -10,6 +10,7 @@ sources = files(
         'netdev/dummy.c',
         'netdev/fou-tunnel.c',
         'netdev/geneve.c',
+        'netdev/hsr.c',
         'netdev/ifb.c',
         'netdev/ipoib.c',
         'netdev/ipvlan.c',
diff --git a/src/network/netdev/hsr.c b/src/network/netdev/hsr.c
new file mode 100644 (file)
index 0000000..c3536a1
--- /dev/null
@@ -0,0 +1,127 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/* Make sure the net/if.h header is included before any linux/ one */
+#include <net/if.h>
+#include <linux/if_arp.h>
+#include <netinet/in.h>
+
+#include "hsr.h"
+#include "netlink-util.h"
+#include "networkd-manager.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+
+static const char * const hsr_protocol_table[_NETDEV_HSR_PROTOCOL_MAX] = {
+        [NETDEV_HSR_PROTOCOL_HSR] = "hsr",
+        [NETDEV_HSR_PROTOCOL_PRP] = "prp",
+};
+
+DEFINE_STRING_TABLE_LOOKUP_FROM_STRING(hsr_protocol, HsrProtocol);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_hsr_protocol, hsr_protocol, HsrProtocol);
+
+static int hsr_get_port_links(NetDev *netdev, Link **ret1, Link **ret2) {
+        Hsr *h = ASSERT_PTR(HSR(netdev));
+        Link *link1, *link2;
+        int r;
+
+        r = link_get_by_name(netdev->manager, h->ports[0], &link1);
+        if (r < 0)
+                return r;
+
+        r = link_get_by_name(netdev->manager, h->ports[1], &link2);
+        if (r < 0)
+                return r;
+
+        if (ret1)
+                *ret1 = link1;
+        if (ret2)
+                *ret2 = link2;
+
+        return 0;
+}
+
+static int netdev_hsr_fill_message_create(NetDev *netdev, Link *link, sd_netlink_message *m) {
+        Hsr *h = ASSERT_PTR(HSR(netdev));
+        Link *link1, *link2;
+        int r;
+
+        assert(m);
+
+        r = hsr_get_port_links(netdev, &link1, &link2);
+        if (r < 0)
+                return r;
+
+        if (link1->ifindex == link2->ifindex)
+                return log_netdev_warning_errno(
+                                netdev, SYNTHETIC_ERRNO(EINVAL), "the two HSR ports must be different");
+
+        r = sd_netlink_message_append_u32(m, IFLA_HSR_SLAVE1, link1->ifindex);
+        if (r < 0)
+                return r;
+
+        r = sd_netlink_message_append_u32(m, IFLA_HSR_SLAVE2, link2->ifindex);
+        if (r < 0)
+                return r;
+
+        r = sd_netlink_message_append_u8(m, IFLA_HSR_PROTOCOL, h->protocol);
+        if (r < 0)
+                return r;
+
+        r = sd_netlink_message_append_u8(m, IFLA_HSR_MULTICAST_SPEC, h->supervision);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int netdev_hsr_config_verify(NetDev *netdev, const char *filename) {
+        Hsr *h = ASSERT_PTR(HSR(netdev));
+
+        assert(filename);
+
+        if (strv_length(h->ports) != 2)
+                return log_netdev_warning_errno(
+                                netdev,
+                                SYNTHETIC_ERRNO(EINVAL),
+                                "HSR needs two ports set, ignoring \"%s\".",
+                                filename);
+
+        if (streq(h->ports[0], h->ports[1]))
+                return log_netdev_warning_errno(
+                                netdev,
+                                SYNTHETIC_ERRNO(EINVAL),
+                                "the two HSR ports must be different, ignoring \"%s\".",
+                                filename);
+
+        return 0;
+}
+
+static int netdev_hsr_is_ready_to_create(NetDev *netdev, Link *link) {
+        return hsr_get_port_links(netdev, NULL, NULL) >= 0;
+}
+
+static void netdev_hsr_done(NetDev *netdev) {
+        Hsr *h = ASSERT_PTR(HSR(netdev));
+
+        strv_free(h->ports);
+}
+
+static void netdev_hsr_init(NetDev *netdev) {
+        Hsr *h = ASSERT_PTR(HSR(netdev));
+
+        h->protocol = NETDEV_HSR_PROTOCOL_HSR;
+}
+
+const NetDevVTable hsr_vtable = {
+        .object_size = sizeof(Hsr),
+        .init = netdev_hsr_init,
+        .done = netdev_hsr_done,
+        .config_verify = netdev_hsr_config_verify,
+        .is_ready_to_create = netdev_hsr_is_ready_to_create,
+        .fill_message_create = netdev_hsr_fill_message_create,
+        .sections = NETDEV_COMMON_SECTIONS "HSR\0",
+        .create_type = NETDEV_CREATE_INDEPENDENT,
+        .iftype = ARPHRD_ETHER,
+        .generate_mac = true,
+};
diff --git a/src/network/netdev/hsr.h b/src/network/netdev/hsr.h
new file mode 100644 (file)
index 0000000..104a9f7
--- /dev/null
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct Hsr Hsr;
+
+#include <linux/if_link.h>
+
+#include "netdev.h"
+
+typedef enum HsrProtocol {
+        NETDEV_HSR_PROTOCOL_HSR = HSR_PROTOCOL_HSR,
+        NETDEV_HSR_PROTOCOL_PRP = HSR_PROTOCOL_PRP,
+        _NETDEV_HSR_PROTOCOL_MAX,
+        _NETDEV_HSR_PROTOCOL_INVALID = -EINVAL,
+} HsrProtocol;
+
+struct Hsr {
+        NetDev meta;
+
+        char **ports;
+        HsrProtocol protocol;
+        uint8_t supervision;
+};
+
+DEFINE_NETDEV_CAST(HSR, Hsr);
+extern const NetDevVTable hsr_vtable;
+
+HsrProtocol hsr_protocol_from_string(const char *d) _pure_;
+
+CONFIG_PARSER_PROTOTYPE(config_parse_hsr_protocol);
index 35016f24d166355b1fbfefb83b5fb3bfa34e0a3f..29cf6174b24d46d6923ba56a17fc631f51c70219 100644 (file)
@@ -12,6 +12,7 @@ _Pragma("GCC diagnostic ignored \"-Wzero-as-null-pointer-constant\"")
 #include "conf-parser.h"
 #include "fou-tunnel.h"
 #include "geneve.h"
+#include "hsr.h"
 #include "ipoib.h"
 #include "ipvlan.h"
 #include "l2tp-tunnel.h"
@@ -172,6 +173,9 @@ GENEVE.DestinationPort,                   config_parse_ip_port,
 GENEVE.IPDoNotFragment,                   config_parse_geneve_df,                    0,                             offsetof(Geneve, geneve_df)
 GENEVE.FlowLabel,                         config_parse_geneve_flow_label,            0,                             0
 GENEVE.InheritInnerProtocol,              config_parse_bool,                         0,                             offsetof(Geneve, inherit_inner_protocol)
+HSR.Ports,                                config_parse_ifnames,                      IFNAME_VALID_ALTERNATIVE,      offsetof(Hsr, ports)
+HSR.Protocol,                             config_parse_hsr_protocol,                 0,                             offsetof(Hsr, protocol)
+HSR.Supervision,                          config_parse_uint8,                        0,                             offsetof(Hsr, supervision)
 MACsec.Port,                              config_parse_macsec_port,                  0,                             0
 MACsec.Encrypt,                           config_parse_tristate,                     0,                             offsetof(MACsec, encrypt)
 MACsecReceiveChannel.Port,                config_parse_macsec_port,                  0,                             0
index c2986aafb5aa0eb2082f2b38baae0bd6ca288218..f31366de1c66d8b4d8ac3efbe7df0a0fee377560 100644 (file)
@@ -18,6 +18,7 @@
 #include "fd-util.h"
 #include "fou-tunnel.h"
 #include "geneve.h"
+#include "hsr.h"
 #include "ifb.h"
 #include "ipoib.h"
 #include "ipvlan.h"
@@ -65,6 +66,7 @@ const NetDevVTable * const netdev_vtable[_NETDEV_KIND_MAX] = {
         [NETDEV_KIND_GENEVE]    = &geneve_vtable,
         [NETDEV_KIND_GRE]       = &gre_vtable,
         [NETDEV_KIND_GRETAP]    = &gretap_vtable,
+        [NETDEV_KIND_HSR]       = &hsr_vtable,
         [NETDEV_KIND_IFB]       = &ifb_vtable,
         [NETDEV_KIND_IP6GRE]    = &ip6gre_vtable,
         [NETDEV_KIND_IP6GRETAP] = &ip6gretap_vtable,
@@ -106,6 +108,7 @@ static const char* const netdev_kind_table[_NETDEV_KIND_MAX] = {
         [NETDEV_KIND_GENEVE]    = "geneve",
         [NETDEV_KIND_GRE]       = "gre",
         [NETDEV_KIND_GRETAP]    = "gretap",
+        [NETDEV_KIND_HSR]       = "hsr",
         [NETDEV_KIND_IFB]       = "ifb",
         [NETDEV_KIND_IP6GRE]    = "ip6gre",
         [NETDEV_KIND_IP6GRETAP] = "ip6gretap",
index b330aa394d7ce6d7fa9b4e4fb738100a378907a4..6b3ed67291a65105d0c2ab04e9b2bae85fdbd009 100644 (file)
@@ -24,6 +24,7 @@
         "-Bridge\0"                               \
         "-FooOverUDP\0"                           \
         "-GENEVE\0"                               \
+        "-HSR\0"                                  \
         "-IPoIB\0"                                \
         "-IPVLAN\0"                               \
         "-IPVTAP\0"                               \
@@ -59,6 +60,7 @@ typedef enum NetDevKind {
         NETDEV_KIND_GENEVE,
         NETDEV_KIND_GRE,
         NETDEV_KIND_GRETAP,
+        NETDEV_KIND_HSR,
         NETDEV_KIND_IFB,
         NETDEV_KIND_IP6GRE,
         NETDEV_KIND_IP6GRETAP,
diff --git a/test/test-network/conf/25-hsr.netdev b/test/test-network/conf/25-hsr.netdev
new file mode 100644 (file)
index 0000000..ac76f9f
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[NetDev]
+Name=hsr99
+Kind=hsr
+
+[HSR]
+Ports=test1 dummy98
diff --git a/test/test-network/conf/25-hsr.network b/test/test-network/conf/25-hsr.network
new file mode 100644 (file)
index 0000000..d87f547
--- /dev/null
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=hsr99
+
+[Network]
+IPv6AcceptRA=no
index fe1c61b8fb7ab4774edd26e0fcbcd4725c4379eb..fc7a67c715466b440140fba4d7a2c9bff2a3c7a7 100755 (executable)
@@ -2057,6 +2057,39 @@ class NetworkdNetDevTests(unittest.TestCase, Utilities):
                 networkctl_reload()
                 self.wait_online('ipvlan99:degraded', 'test1:degraded')
 
+    @expectedFailureIfModuleIsNotAvailable('hsr')
+    def test_hsr(self):
+        first = True
+        for proto, supervision in [['hsr', 9], ['prp', 127]]:
+            if first:
+                first = False
+            else:
+                self.tearDown()
+
+            print(f'### test_hsr(proto={proto}, supervision={supervision})')
+            with self.subTest(proto=proto, supervision=supervision):
+                copy_network_unit('25-hsr.netdev', '25-hsr.network',
+                                  '11-dummy.netdev', '11-dummy.network',
+                                  '12-dummy.netdev', '12-dummy-no-address.network')
+                with open(os.path.join(network_unit_dir, '25-hsr.netdev'), mode='a', encoding='utf-8') as f:
+                    f.write('Protocol=' + proto + '\nSupervision=' + str(supervision))
+
+                start_networkd()
+                self.wait_online('hsr99:degraded')
+                self.networkctl_check_unit('hsr99', '25-hsr', '25-hsr')
+                self.networkctl_check_unit('test1', '11-dummy', '11-dummy')
+                self.networkctl_check_unit('dummy98', '12-dummy', '12-dummy-no-address')
+
+                output = check_output('ip -d link show hsr99')
+                print(output)
+                self.assertRegex(output, 'hsr slave1 test1 slave2 dummy98')
+                self.assertRegex(output, f'supervision 01:15:4e:00:01:{supervision:02x}')
+                self.assertRegex(output, 'proto ' + ('0' if proto == 'hsr' else '1') + ' ')
+
+                touch_network_unit('25-hsr.netdev')
+                networkctl_reload()
+                self.wait_online('hsr99:degraded')
+
     @expectedFailureIfModuleIsNotAvailable('ipvtap')
     def test_ipvtap(self):
         first = True