diff --git a/man/systemd.netdev.xml b/man/systemd.netdev.xml index fe7342bfd0..9ea59a7763 100644 --- a/man/systemd.netdev.xml +++ b/man/systemd.netdev.xml @@ -164,6 +164,9 @@ geneve A GEneric NEtwork Virtualization Encapsulation (GENEVE) netdev driver. + hsr + 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. + l2tp 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 @@ -1027,6 +1030,58 @@ + + [HSR] Section Options + + The [HSR] section only applies for + netdevs of kind hsr, and accepts the + following keys: + + + + Ports= + + 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: + Ports=eth1 eth2 + Ports=eth1 +Ports=eth2 + All the previous assignments are cleared when an empty string is specified. + + + + + + + Protocol= + + Specifies the protocol used by the interface. Takes one of hsr or + prp. Defaults to hsr. + + 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. + + + + + + Supervision= + + 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. + + + + + + + + [BareUDP] Section Options diff --git a/src/libsystemd/sd-netlink/netlink-types-rtnl.c b/src/libsystemd/sd-netlink/netlink-types-rtnl.c index d0d8bfe929..57a38ecb4b 100644 --- a/src/libsystemd/sd-netlink/netlink-types-rtnl.c +++ b/src/libsystemd/sd-netlink/netlink-types-rtnl.c @@ -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), diff --git a/src/network/meson.build b/src/network/meson.build index 5bcce39738..c6352f79c4 100644 --- a/src/network/meson.build +++ b/src/network/meson.build @@ -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 index 0000000000..c3536a1faa --- /dev/null +++ b/src/network/netdev/hsr.c @@ -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 +#include +#include + +#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 index 0000000000..104a9f7aa8 --- /dev/null +++ b/src/network/netdev/hsr.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct Hsr Hsr; + +#include + +#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); diff --git a/src/network/netdev/netdev-gperf.gperf b/src/network/netdev/netdev-gperf.gperf index 35016f24d1..29cf6174b2 100644 --- a/src/network/netdev/netdev-gperf.gperf +++ b/src/network/netdev/netdev-gperf.gperf @@ -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 diff --git a/src/network/netdev/netdev.c b/src/network/netdev/netdev.c index c2986aafb5..f31366de1c 100644 --- a/src/network/netdev/netdev.c +++ b/src/network/netdev/netdev.c @@ -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", diff --git a/src/network/netdev/netdev.h b/src/network/netdev/netdev.h index b330aa394d..6b3ed67291 100644 --- a/src/network/netdev/netdev.h +++ b/src/network/netdev/netdev.h @@ -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 index 0000000000..ac76f9fe79 --- /dev/null +++ b/test/test-network/conf/25-hsr.netdev @@ -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 index 0000000000..d87f547ef1 --- /dev/null +++ b/test/test-network/conf/25-hsr.network @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=hsr99 + +[Network] +IPv6AcceptRA=no diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index fe1c61b8fb..fc7a67c715 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -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