network: add support for HSR netdev

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.
This commit is contained in:
Beniamino Galvani
2025-03-31 21:44:50 +02:00
committed by Yu Watanabe
parent c1b0e39ffd
commit f7996e2a33
11 changed files with 276 additions and 1 deletions

View File

@@ -164,6 +164,9 @@
<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>
@@ -1027,6 +1030,58 @@
</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>

View 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),

View 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',

127
src/network/netdev/hsr.c Normal file
View File

@@ -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,
};

30
src/network/netdev/hsr.h Normal file
View File

@@ -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);

View 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

View 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",

View 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,

View File

@@ -0,0 +1,7 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
[NetDev]
Name=hsr99
Kind=hsr
[HSR]
Ports=test1 dummy98

View File

@@ -0,0 +1,6 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
[Match]
Name=hsr99
[Network]
IPv6AcceptRA=no

View File

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