diff --git a/man/systemd.network.xml b/man/systemd.network.xml index ffed2ee066e..2ee8a6ab487 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2518,8 +2518,12 @@ Token=prefixstable:2002:da8:1:: RouteMetric= - Set the routing metric for the routes received in the Router Advertisement. Takes an - unsigned integer in the range 0…4294967295. Defaults to 1024. + Set the routing metric for the routes received in the Router Advertisement. Takes an unsigned + integer in the range 0…4294967295, or three unsigned integer separated with :, + in that case the first one is used when the router preference is high, the second is for medium + preference, and the last is for low preference + (high:medium:low). + Defaults to 512:1024:2048. diff --git a/src/network/networkd-dhcp-common.c b/src/network/networkd-dhcp-common.c index 7b0b2dea857..ca9a825e7b3 100644 --- a/src/network/networkd-dhcp-common.c +++ b/src/network/networkd-dhcp-common.c @@ -304,7 +304,7 @@ int config_parse_dhcp( return 0; } -int config_parse_dhcp_or_ra_route_metric( +int config_parse_dhcp_route_metric( const char* unit, const char *filename, unsigned line, @@ -322,7 +322,7 @@ int config_parse_dhcp_or_ra_route_metric( assert(filename); assert(lvalue); - assert(IN_SET(ltype, AF_UNSPEC, AF_INET, AF_INET6)); + assert(IN_SET(ltype, AF_UNSPEC, AF_INET)); assert(rvalue); assert(data); @@ -338,16 +338,15 @@ int config_parse_dhcp_or_ra_route_metric( network->dhcp_route_metric = metric; network->dhcp_route_metric_set = true; break; - case AF_INET6: - network->ipv6_accept_ra_route_metric = metric; - network->ipv6_accept_ra_route_metric_set = true; - break; case AF_UNSPEC: /* For backward compatibility. */ if (!network->dhcp_route_metric_set) network->dhcp_route_metric = metric; - if (!network->ipv6_accept_ra_route_metric_set) - network->ipv6_accept_ra_route_metric = metric; + if (!network->ipv6_accept_ra_route_metric_set) { + network->ipv6_accept_ra_route_metric_high = metric; + network->ipv6_accept_ra_route_metric_medium = metric; + network->ipv6_accept_ra_route_metric_low = metric; + } break; default: assert_not_reached(); @@ -356,6 +355,64 @@ int config_parse_dhcp_or_ra_route_metric( return 0; } +int config_parse_ipv6_accept_ra_route_metric( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Network *network = ASSERT_PTR(userdata); + uint32_t metric_high, metric_medium, metric_low; + int r, s, t; + + assert(filename); + assert(rvalue); + + if (safe_atou32(rvalue, &metric_low) >= 0) + metric_high = metric_medium = metric_low; + else { + _cleanup_free_ char *high = NULL, *medium = NULL, *low = NULL; + const char *p = rvalue; + + r = extract_many_words(&p, ":", EXTRACT_DONT_COALESCE_SEPARATORS, &high, &medium, &low, NULL); + if (r == -ENOMEM) + return log_oom(); + if (r != 3 || !isempty(p)) { + log_syntax(unit, LOG_WARNING, filename, line, r < 0 ? r : 0, + "Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue); + return 0; + } + + r = safe_atou32(high, &metric_high); + s = safe_atou32(medium, &metric_medium); + t = safe_atou32(low, &metric_low); + if (r < 0 || s < 0 || t < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r < 0 ? r : s < 0 ? s : t, + "Failed to parse RouteTable=%s, ignoring assignment: %m", rvalue); + return 0; + } + + if (metric_high >= metric_medium || metric_medium >= metric_low) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid RouteTable=%s, ignoring assignment: %m", rvalue); + return 0; + } + } + + network->ipv6_accept_ra_route_metric_high = metric_high; + network->ipv6_accept_ra_route_metric_medium = metric_medium; + network->ipv6_accept_ra_route_metric_low = metric_low; + network->ipv6_accept_ra_route_metric_set = true; + + return 0; +} + int config_parse_dhcp_use_dns( const char* unit, const char *filename, diff --git a/src/network/networkd-dhcp-common.h b/src/network/networkd-dhcp-common.h index c19bc104075..6f57b78a7f3 100644 --- a/src/network/networkd-dhcp-common.h +++ b/src/network/networkd-dhcp-common.h @@ -15,6 +15,9 @@ #define UPLINK_INDEX_SELF -2 /* the interface itself is uplink */ #define DHCP_ROUTE_METRIC 1024 +#define IPV6RA_ROUTE_METRIC_HIGH 512 +#define IPV6RA_ROUTE_METRIC_MEDIUM 1024 +#define IPV6RA_ROUTE_METRIC_LOW 2048 #define DHCP6PD_ROUTE_METRIC 256 typedef struct Link Link; @@ -89,7 +92,8 @@ const char *dhcp_option_data_type_to_string(DHCPOptionDataType d) _const_; DHCPOptionDataType dhcp_option_data_type_from_string(const char *d) _pure_; CONFIG_PARSER_PROTOTYPE(config_parse_dhcp); -CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_or_ra_route_metric); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_route_metric); +CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_accept_ra_route_metric); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_dns); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_domains); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_use_ntp); diff --git a/src/network/networkd-ndisc.c b/src/network/networkd-ndisc.c index f31735b6ec6..ce7dff222bb 100644 --- a/src/network/networkd-ndisc.c +++ b/src/network/networkd-ndisc.c @@ -143,6 +143,28 @@ static int ndisc_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Request return 1; } +static void ndisc_set_route_priority(Link *link, Route *route) { + assert(link); + assert(route); + + if (route->priority_set) + return; /* explicitly configured. */ + + switch (route->pref) { + case SD_NDISC_PREFERENCE_LOW: + route->priority = link->network->ipv6_accept_ra_route_metric_low; + break; + case SD_NDISC_PREFERENCE_MEDIUM: + route->priority = link->network->ipv6_accept_ra_route_metric_medium; + break; + case SD_NDISC_PREFERENCE_HIGH: + route->priority = link->network->ipv6_accept_ra_route_metric_high; + break; + default: + assert_not_reached(); + } +} + static int ndisc_request_route(Route *in, Link *link, sd_ndisc_router *rt) { _cleanup_(route_freep) Route *route = in; struct in6_addr router; @@ -160,8 +182,7 @@ static int ndisc_request_route(Route *in, Link *link, sd_ndisc_router *rt) { route->provider.in6 = router; if (!route->table_set) route->table = link_get_ipv6_accept_ra_route_table(link); - if (!route->priority_set) - route->priority = link->network->ipv6_accept_ra_route_metric; + ndisc_set_route_priority(link, route); if (!route->protocol_set) route->protocol = RTPROT_RA; diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index aec54528498..70dead97ab1 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -234,7 +234,7 @@ DHCPv4.UserClass, config_parse_dhcp_user_or_vendor_cl DHCPv4.IAID, config_parse_iaid, AF_INET, 0 DHCPv4.DUIDType, config_parse_network_duid_type, 0, 0 DHCPv4.DUIDRawData, config_parse_network_duid_rawdata, 0, 0 -DHCPv4.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_INET, 0 +DHCPv4.RouteMetric, config_parse_dhcp_route_metric, AF_INET, 0 DHCPv4.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET, 0 DHCPv4.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone) DHCPv4.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port) @@ -277,7 +277,7 @@ IPv6AcceptRA.UseDomains, config_parse_ipv6_accept_ra_use_dom IPv6AcceptRA.UseMTU, config_parse_bool, 0, offsetof(Network, ipv6_accept_ra_use_mtu) 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 -IPv6AcceptRA.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_INET6, 0 +IPv6AcceptRA.RouteMetric, config_parse_ipv6_accept_ra_route_metric, 0, 0 IPv6AcceptRA.RouterAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_router) IPv6AcceptRA.RouterDenyList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_deny_listed_router) IPv6AcceptRA.PrefixAllowList, config_parse_in_addr_prefixes, AF_INET6, offsetof(Network, ndisc_allow_listed_prefix) @@ -559,7 +559,7 @@ DHCP.UserClass, config_parse_dhcp_user_or_vendor_cl DHCP.IAID, config_parse_iaid, AF_INET, 0 DHCP.DUIDType, config_parse_network_duid_type, 0, 0 DHCP.DUIDRawData, config_parse_network_duid_rawdata, 0, 0 -DHCP.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_UNSPEC, 0 +DHCP.RouteMetric, config_parse_dhcp_route_metric, AF_UNSPEC, 0 DHCP.RouteTable, config_parse_dhcp_or_ra_route_table, AF_INET, 0 DHCP.UseTimezone, config_parse_bool, 0, offsetof(Network, dhcp_use_timezone) DHCP.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port) @@ -567,7 +567,7 @@ DHCP.RapidCommit, config_parse_bool, DHCP.ForceDHCPv6PDOtherInformation, config_parse_warn_compat, DISABLED_LEGACY, 0 DHCPv4.UseDomainName, config_parse_dhcp_use_domains, AF_INET, 0 DHCPv4.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical) -DHCPv6.RouteMetric, config_parse_dhcp_or_ra_route_metric, AF_INET6, 0 +DHCPv6.RouteMetric, config_parse_ipv6_accept_ra_route_metric, AF_INET6, 0 DHCPv6.ForceDHCPv6PDOtherInformation, config_parse_warn_compat, DISABLED_LEGACY, 0 DHCPv6PrefixDelegation.SubnetId, config_parse_dhcp_pd_subnet_id, 0, offsetof(Network, dhcp_pd_subnet_id) DHCPv6PrefixDelegation.Announce, config_parse_bool, 0, offsetof(Network, dhcp_pd_announce) diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index a6c5b442389..41a0766f078 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -481,7 +481,9 @@ 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_route_table = RT_TABLE_MAIN, - .ipv6_accept_ra_route_metric = DHCP_ROUTE_METRIC, + .ipv6_accept_ra_route_metric_high = IPV6RA_ROUTE_METRIC_HIGH, + .ipv6_accept_ra_route_metric_medium = IPV6RA_ROUTE_METRIC_MEDIUM, + .ipv6_accept_ra_route_metric_low = IPV6RA_ROUTE_METRIC_LOW, .ipv6_accept_ra_start_dhcp6_client = IPV6_ACCEPT_RA_START_DHCP6_CLIENT_YES, .can_termination = -1, diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index a2743f97434..79e8ceb06b4 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -315,7 +315,9 @@ struct Network { IPv6AcceptRAStartDHCP6Client ipv6_accept_ra_start_dhcp6_client; uint32_t ipv6_accept_ra_route_table; bool ipv6_accept_ra_route_table_set; - uint32_t ipv6_accept_ra_route_metric; + uint32_t ipv6_accept_ra_route_metric_high; + uint32_t ipv6_accept_ra_route_metric_medium; + uint32_t ipv6_accept_ra_route_metric_low; bool ipv6_accept_ra_route_metric_set; Set *ndisc_deny_listed_router; Set *ndisc_allow_listed_router; diff --git a/test/test-network/conf/25-veth-bridge.network b/test/test-network/conf/25-veth-bridge.network new file mode 100644 index 00000000000..b2f163412a9 --- /dev/null +++ b/test/test-network/conf/25-veth-bridge.network @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=client-p +Name=router-high-p +Name=router-low-p + +[Network] +Bridge=bridge99 +IPv6AcceptRA=no +IPv6SendRA=yes diff --git a/test/test-network/conf/25-veth-client.netdev b/test/test-network/conf/25-veth-client.netdev new file mode 100644 index 00000000000..511c1e59ce8 --- /dev/null +++ b/test/test-network/conf/25-veth-client.netdev @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[NetDev] +Name=client +Kind=veth +MACAddress=12:34:56:78:9a:00 + +[Peer] +Name=client-p +MACAddress=12:34:56:78:9b:00 diff --git a/test/test-network/conf/25-veth-client.network b/test/test-network/conf/25-veth-client.network new file mode 100644 index 00000000000..152fb4d4c19 --- /dev/null +++ b/test/test-network/conf/25-veth-client.network @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=client + +[Network] +IPv6AcceptRA=yes + +[IPv6AcceptRA] +UseDNS=no +UseDomains=no diff --git a/test/test-network/conf/25-veth-router-high.netdev b/test/test-network/conf/25-veth-router-high.netdev new file mode 100644 index 00000000000..845b4a9ce1d --- /dev/null +++ b/test/test-network/conf/25-veth-router-high.netdev @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[NetDev] +Name=router-high +Kind=veth +MACAddress=12:34:56:78:9a:99 + +[Peer] +Name=router-high-p +MACAddress=12:34:56:78:9b:99 diff --git a/test/test-network/conf/25-veth-router-high.network b/test/test-network/conf/25-veth-router-high.network new file mode 100644 index 00000000000..c6c2df231e6 --- /dev/null +++ b/test/test-network/conf/25-veth-router-high.network @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=router-high + +[Network] +IPv6AcceptRA=no +IPv6SendRA=yes + +[IPv6SendRA] +RouterPreference=high +EmitDNS=no +EmitDomains=no + +[IPv6Prefix] +Prefix=2002:da8:1:99::/64 +PreferredLifetimeSec=1000s +ValidLifetimeSec=2100s diff --git a/test/test-network/conf/25-veth-router-low.netdev b/test/test-network/conf/25-veth-router-low.netdev new file mode 100644 index 00000000000..2842f4f98a8 --- /dev/null +++ b/test/test-network/conf/25-veth-router-low.netdev @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[NetDev] +Name=router-low +Kind=veth +MACAddress=12:34:56:78:9a:98 + +[Peer] +Name=router-low-p +MACAddress=12:34:56:78:9b:98 diff --git a/test/test-network/conf/25-veth-router-low.network b/test/test-network/conf/25-veth-router-low.network new file mode 100644 index 00000000000..d2abc1520eb --- /dev/null +++ b/test/test-network/conf/25-veth-router-low.network @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=router-low + +[Network] +IPv6AcceptRA=no +IPv6SendRA=yes + +[IPv6SendRA] +RouterPreference=low +EmitDNS=no +EmitDomains=no + +[IPv6Prefix] +Prefix=2002:da8:1:98::/64 +PreferredLifetimeSec=1000s +ValidLifetimeSec=2100s diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 87710ef3fb6..de339894afe 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -907,6 +907,16 @@ class Utilities(): self.assertNotRegex(output, address_regex) + def wait_route(self, link, route_regex, table='main', ipv='', timeout_sec=100): + for i in range(timeout_sec): + if i > 0: + time.sleep(1) + output = check_output(f'ip {ipv} route show dev {link} table {table}') + if re.search(route_regex, output): + break + + self.assertRegex(output, route_regex) + def check_netlabel(self, interface, address, label='system_u:object_r:root_t:s0'): if not shutil.which('selinuxenabled'): print('## Checking NetLabel skipped: selinuxenabled command not found.') @@ -4338,6 +4348,55 @@ class NetworkdRATests(unittest.TestCase, Utilities): self.assertIn('2002:da8:1:0:b47e:7975:fc7a:7d6e', output) self.assertIn('2002:da8:2:0:f689:561a:8eda:7443', output) + def test_router_preference(self): + copy_network_unit('25-veth-client.netdev', + '25-veth-router-high.netdev', + '25-veth-router-low.netdev', + '26-bridge.netdev', + '25-veth-bridge.network', + '25-veth-client.network', + '25-veth-router-high.network', + '25-veth-router-low.network', + '25-bridge99.network') + start_networkd() + self.wait_online(['client-p:enslaved', + 'router-high:degraded', 'router-high-p:enslaved', + 'router-low:degraded', 'router-low-p:enslaved', + 'bridge99:routable']) + + networkctl_reconfigure('client') + self.wait_online(['client:routable']) + + self.wait_address('client', '2002:da8:1:99:1034:56ff:fe78:9a00/64', ipv='-6', timeout_sec=10) + self.wait_address('client', '2002:da8:1:98:1034:56ff:fe78:9a00/64', ipv='-6', timeout_sec=10) + self.wait_route('client', 'default via fe80::1034:56ff:fe78:9a99 proto ra metric 512', ipv='-6', timeout_sec=10) + self.wait_route('client', 'default via fe80::1034:56ff:fe78:9a98 proto ra metric 2048', ipv='-6', timeout_sec=10) + + output = check_output('ip -6 route show dev client default via fe80::1034:56ff:fe78:9a99') + print(output) + self.assertIn('pref high', output) + output = check_output('ip -6 route show dev client default via fe80::1034:56ff:fe78:9a98') + print(output) + self.assertIn('pref low', output) + + with open(os.path.join(network_unit_dir, '25-veth-client.network'), mode='a', encoding='utf-8') as f: + f.write('\n[Link]\nMACAddress=12:34:56:78:9a:01\n[IPv6AcceptRA]\nRouteMetric=100:200:300\n') + + networkctl_reload() + self.wait_online(['client:routable']) + + self.wait_address('client', '2002:da8:1:99:1034:56ff:fe78:9a01/64', ipv='-6', timeout_sec=10) + self.wait_address('client', '2002:da8:1:98:1034:56ff:fe78:9a01/64', ipv='-6', timeout_sec=10) + self.wait_route('client', 'default via fe80::1034:56ff:fe78:9a99 proto ra metric 100', ipv='-6', timeout_sec=10) + self.wait_route('client', 'default via fe80::1034:56ff:fe78:9a98 proto ra metric 300', ipv='-6', timeout_sec=10) + + output = check_output('ip -6 route show dev client default via fe80::1034:56ff:fe78:9a99') + print(output) + self.assertIn('pref high', output) + output = check_output('ip -6 route show dev client default via fe80::1034:56ff:fe78:9a98') + print(output) + self.assertIn('pref low', output) + class NetworkdDHCPServerTests(unittest.TestCase, Utilities): def setUp(self):