diff --git a/man/systemd.netdev.xml b/man/systemd.netdev.xml index bd493a8c53..e4e7e611e7 100644 --- a/man/systemd.netdev.xml +++ b/man/systemd.netdev.xml @@ -1569,6 +1569,29 @@ Sets a firewall mark on outgoing WireGuard packets from this interface. Takes a number between 1 and 4294967295. + + RouteTable= + + The table identifier for the routes to the addresses specified in the + AllowedIPs=. Takes the special value off, one of the + predefined names default, main, and + local, names defined in RouteTable= in + networkd.conf5, + or a number in the range 1…4294967295. When off the routes to the + addresses specified in the AllowedIPs= setting will not be configured. + Defaults to main. This setting will be ignored when the same setting is + specified in the [WireGuardPeer] section. + + + + RouteMetric= + + The priority of the routes to the addresses specified in the + AllowedIPs=. Takes an integer in the range 0…4294967295. Defaults to 0 + for IPv4 addresses, and 1024 for IPv6 addresses. This setting will be ignored when the same + setting is specified in the [WireGuardPeer] section. + + @@ -1653,6 +1676,27 @@ Most users will not need this. + + RouteTable= + + The table identifier for the routes to the addresses specified in the + AllowedIPs=. Takes the special value off, one of the + predefined names default, main, and + local, names defined in RouteTable= in + networkd.conf5, + or a number in the range 1…4294967295. Defaults to unset, and the value specified in the + same setting in the [WireGuard] section will be used. + + + + RouteMetric= + + The priority of the routes to the addresses specified in the + AllowedIPs=. Takes an integer in the range 0…4294967295. Defaults to + unset, and the value specified in the same setting in the [WireGuard] section will be used. + + + diff --git a/src/network/netdev/netdev-gperf.gperf b/src/network/netdev/netdev-gperf.gperf index 316592005f..37a0d9fa5d 100644 --- a/src/network/netdev/netdev-gperf.gperf +++ b/src/network/netdev/netdev-gperf.gperf @@ -229,12 +229,16 @@ WireGuard.FwMark, config_parse_unsigned, WireGuard.ListenPort, config_parse_wireguard_listen_port, 0, offsetof(Wireguard, port) WireGuard.PrivateKey, config_parse_wireguard_private_key, 0, 0 WireGuard.PrivateKeyFile, config_parse_wireguard_private_key_file, 0, 0 +WireGuard.RouteTable, config_parse_wireguard_route_table, 0, offsetof(Wireguard, route_table) +WireGuard.RouteMetric, config_parse_wireguard_route_priority, 0, offsetof(Wireguard, route_priority) WireGuardPeer.AllowedIPs, config_parse_wireguard_allowed_ips, 0, 0 WireGuardPeer.Endpoint, config_parse_wireguard_endpoint, 0, 0 WireGuardPeer.PublicKey, config_parse_wireguard_peer_key, 0, 0 WireGuardPeer.PresharedKey, config_parse_wireguard_peer_key, 0, 0 WireGuardPeer.PresharedKeyFile, config_parse_wireguard_preshared_key_file, 0, 0 WireGuardPeer.PersistentKeepalive, config_parse_wireguard_keepalive, 0, 0 +WireGuardPeer.RouteTable, config_parse_wireguard_peer_route_table, 0, 0 +WireGuardPeer.RouteMetric, config_parse_wireguard_peer_route_priority,0, 0 Xfrm.InterfaceId, config_parse_uint32, 0, offsetof(Xfrm, if_id) Xfrm.Independent, config_parse_bool, 0, offsetof(Xfrm, independent) BatmanAdvanced.Aggregation, config_parse_bool, 0, offsetof(BatmanAdvanced, aggregation) diff --git a/src/network/netdev/wireguard.c b/src/network/netdev/wireguard.c index 3734fa51c5..f254b05f86 100644 --- a/src/network/netdev/wireguard.c +++ b/src/network/netdev/wireguard.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "sd-resolve.h" @@ -18,6 +19,8 @@ #include "memory-util.h" #include "netlink-util.h" #include "networkd-manager.h" +#include "networkd-route-util.h" +#include "networkd-route.h" #include "networkd-util.h" #include "parse-util.h" #include "path-util.h" @@ -696,7 +699,7 @@ int config_parse_wireguard_allowed_ips( *ipmask = (WireguardIPmask) { .family = family, - .ip.in6 = addr.in6, + .ip = addr, .cidr = prefixlen, }; @@ -827,6 +830,186 @@ int config_parse_wireguard_keepalive( return 0; } +int config_parse_wireguard_route_table( + 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) { + + NetDev *netdev = userdata; + uint32_t *table = data; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + assert(userdata); + + if (isempty(rvalue)) { + *table = RT_TABLE_MAIN; + return 0; + } + + if (streq(rvalue, "off")) { + *table = 0; + return 0; + } + + r = manager_get_route_table_from_string(netdev->manager, rvalue, table); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + return 0; +} + +int config_parse_wireguard_peer_route_table( + 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) { + + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + NetDev *netdev = userdata; + Wireguard *w; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(netdev); + assert(netdev->manager); + + w = WIREGUARD(netdev); + assert(w); + + r = wireguard_peer_new_static(w, filename, section_line, &peer); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + peer->route_table_set = false; /* Use the table specified in [WireGuard] section. */ + TAKE_PTR(peer); + return 0; + } + + if (streq(rvalue, "off")) { + peer->route_table = 0; /* Disabled. */ + peer->route_table_set = true; + TAKE_PTR(peer); + return 0; + } + + r = manager_get_route_table_from_string(netdev->manager, rvalue, &peer->route_table); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse %s=, ignoring assignment: %s", + lvalue, rvalue); + return 0; + } + + peer->route_table_set = true; + TAKE_PTR(peer); + return 0; +} + +int config_parse_wireguard_route_priority( + 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) { + + uint32_t *priority = data; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + *priority = 0; + return 0; + } + + r = safe_atou32(rvalue, priority); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + return 0; +} + +int config_parse_wireguard_peer_route_priority( + 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) { + + _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL; + Wireguard *w; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(userdata); + + w = WIREGUARD(userdata); + assert(w); + + r = wireguard_peer_new_static(w, filename, section_line, &peer); + if (r < 0) + return log_oom(); + + if (isempty(rvalue)) { + peer->route_priority_set = false; /* Use the priority specified in [WireGuard] section. */ + TAKE_PTR(peer); + return 0; + } + + r = safe_atou32(rvalue, &peer->route_priority); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Could not parse route priority \"%s\", ignoring assignment: %m", rvalue); + return 0; + } + + peer->route_priority_set = true; + TAKE_PTR(peer); + return 0; +} + static void wireguard_init(NetDev *netdev) { Wireguard *w; @@ -835,6 +1018,7 @@ static void wireguard_init(NetDev *netdev) { assert(w); w->flags = WGDEVICE_F_REPLACE_PEERS; + w->route_table = RT_TABLE_MAIN; } static void wireguard_done(NetDev *netdev) { @@ -852,6 +1036,8 @@ static void wireguard_done(NetDev *netdev) { hashmap_free_with_destructor(w->peers_by_section, wireguard_peer_free); set_free(w->peers_with_unresolved_endpoint); set_free(w->peers_with_failed_endpoint); + + set_free(w->routes); } static int wireguard_read_key_file(const char *filename, uint8_t dest[static WG_KEY_LEN]) { @@ -924,9 +1110,40 @@ static int wireguard_verify(NetDev *netdev, const char *filename) { "%s: Missing PrivateKey= or PrivateKeyFile=, " "Ignoring network device.", filename); - LIST_FOREACH_SAFE(peers, peer, peer_next, w->peers) - if (wireguard_peer_verify(peer) < 0) + LIST_FOREACH_SAFE(peers, peer, peer_next, w->peers) { + WireguardIPmask *ipmask; + + if (wireguard_peer_verify(peer) < 0) { wireguard_peer_free(peer); + continue; + } + + if ((peer->route_table_set ? peer->route_table : w->route_table) == 0) + continue; + + LIST_FOREACH(ipmasks, ipmask, peer->ipmasks) { + _cleanup_(route_freep) Route *route = NULL; + + r = route_new(&route); + if (r < 0) + return log_oom(); + + route->family = ipmask->family; + route->dst = ipmask->ip; + route->dst_prefixlen = ipmask->cidr; + route->scope = RT_SCOPE_UNIVERSE; + route->protocol = RTPROT_STATIC; + route->table = peer->route_table_set ? peer->route_table : w->route_table; + route->priority = peer->route_priority_set ? peer->route_priority : w->route_priority; + if (route->priority == 0 && route->family == AF_INET6) + route->priority = IP6_RT_PRIO_USER; + route->source = NETWORK_CONFIG_SOURCE_STATIC; + + r = set_ensure_consume(&w->routes, &route_hash_ops, TAKE_PTR(route)); + if (r < 0) + return log_oom(); + } + } return 0; } diff --git a/src/network/netdev/wireguard.h b/src/network/netdev/wireguard.h index b9b5ae9871..5d4b6da45e 100644 --- a/src/network/netdev/wireguard.h +++ b/src/network/netdev/wireguard.h @@ -33,6 +33,11 @@ typedef struct WireguardPeer { char *endpoint_host; char *endpoint_port; + uint32_t route_table; + uint32_t route_priority; + bool route_table_set; + bool route_priority_set; + LIST_HEAD(WireguardIPmask, ipmasks); LIST_FIELDS(struct WireguardPeer, peers); } WireguardPeer; @@ -55,6 +60,10 @@ struct Wireguard { unsigned n_retries; sd_event_source *resolve_retry_event_source; + + Set *routes; + uint32_t route_table; + uint32_t route_priority; }; DEFINE_NETDEV_CAST(WIREGUARD, Wireguard); @@ -68,3 +77,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_private_key); CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_private_key_file); CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_preshared_key_file); CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_keepalive); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_route_table); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_route_table); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_route_priority); +CONFIG_PARSER_PROTOTYPE(config_parse_wireguard_peer_route_priority); diff --git a/src/network/networkd-route.c b/src/network/networkd-route.c index 623b962c23..20d6aa47f6 100644 --- a/src/network/networkd-route.c +++ b/src/network/networkd-route.c @@ -19,6 +19,7 @@ #include "string-util.h" #include "strv.h" #include "vrf.h" +#include "wireguard.h" int route_new(Route **ret) { _cleanup_(route_freep) Route *route = NULL; @@ -242,7 +243,7 @@ int route_compare_func(const Route *a, const Route *b) { } } -DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( +DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR( route_hash_ops, Route, route_hash_func, @@ -673,7 +674,7 @@ static int route_set_netlink_message(const Route *route, sd_netlink_message *req return log_link_error_errno(link, r, "Could not set route table: %m"); /* Table attribute to allow more than 256. */ - r = sd_netlink_message_append_data(req, RTA_TABLE, &route->table, sizeof(route->table)); + r = sd_netlink_message_append_u32(req, RTA_TABLE, route->table); if (r < 0) return log_link_error_errno(link, r, "Could not append RTA_TABLE attribute: %m"); } @@ -865,6 +866,28 @@ static bool route_by_kernel(const Route *route) { return false; } +static void link_unmark_wireguard_routes(Link *link) { + Route *route, *existing; + NetDev *netdev; + Wireguard *w; + + assert(link); + + if (!streq_ptr(link->kind, "wireguard")) + return; + + if (netdev_get(link->manager, link->ifname, &netdev) < 0) + return; + + w = WIREGUARD(netdev); + if (!w) + return; + + SET_FOREACH(route, w->routes) + if (route_get(NULL, link, route, &existing) >= 0) + route_unmark(existing); +} + int link_drop_foreign_routes(Link *link) { Route *route; int k, r; @@ -914,6 +937,8 @@ int link_drop_foreign_routes(Link *link) { route_unmark(existing); } + link_unmark_wireguard_routes(link); + r = 0; SET_FOREACH(route, link->routes) { if (!route_is_marked(route)) @@ -1342,6 +1367,36 @@ static int link_request_static_route(Link *link, Route *route) { &link->static_route_messages, static_route_handler, NULL); } +static int link_request_wireguard_routes(Link *link, bool only_ipv4) { + NetDev *netdev; + Wireguard *w; + Route *route; + int r; + + assert(link); + + if (!streq_ptr(link->kind, "wireguard")) + return 0; + + if (netdev_get(link->manager, link->ifname, &netdev) < 0) + return 0; + + w = WIREGUARD(netdev); + if (!w) + return 0; + + SET_FOREACH(route, w->routes) { + if (only_ipv4 && route->family != AF_INET) + continue; + + r = link_request_static_route(link, route); + if (r < 0) + return r; + } + + return 0; +} + int link_request_static_routes(Link *link, bool only_ipv4) { Route *route; int r; @@ -1363,6 +1418,10 @@ int link_request_static_routes(Link *link, bool only_ipv4) { return r; } + r = link_request_wireguard_routes(link, only_ipv4); + if (r < 0) + return r; + if (link->static_route_messages == 0) { link->static_routes_configured = true; link_check_ready(link); @@ -1723,12 +1782,16 @@ int manager_rtnl_process_route(sd_netlink *rtnl, sd_netlink_message *message, Ma return 0; } - r = sd_rtnl_message_route_get_table(message, &table); + r = sd_netlink_message_read_u32(message, RTA_TABLE, &tmp->table); + if (r == -ENODATA) { + r = sd_rtnl_message_route_get_table(message, &table); + if (r >= 0) + tmp->table = table; + } if (r < 0) { log_link_warning_errno(link, r, "rtnl: received route message with invalid table, ignoring: %m"); return 0; } - tmp->table = table; r = sd_netlink_message_read_u32(message, RTA_PRIORITY, &tmp->priority); if (r < 0 && r != -ENODATA) { diff --git a/src/network/networkd-route.h b/src/network/networkd-route.h index 6bc19fd7d8..a3194de1ae 100644 --- a/src/network/networkd-route.h +++ b/src/network/networkd-route.h @@ -70,6 +70,7 @@ typedef struct Route { void route_hash_func(const Route *route, struct siphash *state); int route_compare_func(const Route *a, const Route *b); +extern const struct hash_ops route_hash_ops; int route_new(Route **ret); Route *route_free(Route *route); diff --git a/test/fuzz/fuzz-netdev-parser/directives.netdev b/test/fuzz/fuzz-netdev-parser/directives.netdev index c7c3682eab..e34d16af11 100644 --- a/test/fuzz/fuzz-netdev-parser/directives.netdev +++ b/test/fuzz/fuzz-netdev-parser/directives.netdev @@ -17,6 +17,8 @@ PrivateKey= PrivateKeyFile= FwMark= FirewallMark= +RouteTable= +RouteMetric= [MACVTAP] Mode= SourceMACAddress= @@ -67,6 +69,8 @@ PresharedKeyFile= PersistentKeepalive= PublicKey= AllowedIPs= +RouteTable= +RouteMetric= [Tunnel] FooOverUDP= IPv6FlowLabel= diff --git a/test/test-network/conf/25-wireguard-23-peers.netdev b/test/test-network/conf/25-wireguard-23-peers.netdev index 90ea1b9450..86f1440158 100644 --- a/test/test-network/conf/25-wireguard-23-peers.netdev +++ b/test/test-network/conf/25-wireguard-23-peers.netdev @@ -11,6 +11,8 @@ ListenPort=4500 # The key below should be overridden by PrivateKeyFile= PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= PrivateKeyFile=/run/systemd/network/25-wireguard-private-key.txt +RouteTable=1234 +RouteMetric=123 # peer 1 [WireGuardPeer] diff --git a/test/test-network/conf/25-wireguard-23-peers.network b/test/test-network/conf/25-wireguard-23-peers.network index 33c794c267..6f4c2d030c 100644 --- a/test/test-network/conf/25-wireguard-23-peers.network +++ b/test/test-network/conf/25-wireguard-23-peers.network @@ -6,95 +6,3 @@ Name=wg98 Address=192.168.123.123/24 Address=fd8d:4d6d:3ccb:0500::1/64 IPv6AcceptRA=no - -# nat64 via 1 -[Route] -Destination = fd8d:4d6d:3ccb:0c79:2339:edce::/96 - -# nat64 via 2 -[Route] -Destination = fd8d:4d6d:3ccb:a072:80da:de4f::/96 - -# nat64 via 3 -[Route] -Destination = fd8d:4d6d:3ccb:f349:c4f0:10c1::/96 - -# nat64 via 4 -[Route] -Destination = fd8d:4d6d:3ccb:b684:4f81:2e3e::/96 - -# nat64 via 5 -[Route] -Destination = fd8d:4d6d:3ccb:c624:6bf7:4c09::/96 - -# nat64 via 6 -[Route] -Destination = fd8d:4d6d:3ccb:9c11:d820:2e96::/96 - -# nat64 via 7 -[Route] -Destination = fd8d:4d6d:3ccb:bad5:495d:8e9c::/96 - -# nat64 via 8 -[Route] -Destination = fd8d:4d6d:3ccb:1e54:1415:35d0::/96 - -# nat64 via 9 -[Route] -Destination = fd8d:4d6d:3ccb:1dbf:ca8a:32d3::/96 - -# nat64 via 10 -[Route] -Destination = fd8d:4d6d:3ccb:dcdd:d33b:90c9::/96 - -# nat64 via 11 -[Route] -Destination = fd8d:4d6d:3ccb:6f2e:6888:c6fd::/96 - -# nat64 via 12 -[Route] -Destination = fd8d:4d6d:3ccb:d4f9:05dc:9296::/96 - -# nat64 via 13 -[Route] -Destination = fd8d:4d6d:3ccb:b39c:9cdc:755a::/96 - -# nat64 via 14 -[Route] -Destination = fd8d:4d6d:3ccb:bfe5:c3c3:5d77::/96 - -# nat64 via 15 -[Route] -Destination = fd8d:4d6d:3ccb:900c:d437:ec27::/96 - -# nat64 via 16 -[Route] -Destination = fd8d:4d6d:3ccb:270d:b5dd:4a3f::/96 - -# nat64 via 17 -[Route] -Destination = fd8d:4d6d:3ccb:e2e1:ae15:103f::/96 - -# nat64 via 18 -[Route] -Destination = fd8d:4d6d:3ccb:5660:679d:3532::/96 - -# nat64 via 19 -[Route] -Destination = fd8d:4d6d:3ccb:6825:573f:30f3::/96 - -# nat64 via 20 -[Route] -Destination = fd8d:4d6d:3ccb:a94b:cd6a:a32d::/96 - -# nat64 via 21 -[Route] -Destination = fd8d:4d6d:3ccb:8d4d:0bab:7280::/96 - -# nat64 via 22 -[Route] -Destination = fd8d:4d6d:3ccb:a3f3:df38:19b0::/96 - -# nat64 via 23 -[Route] -Destination = fd8d:4d6d:3ccb:9742:9931:5217::/96 diff --git a/test/test-network/conf/25-wireguard.netdev b/test/test-network/conf/25-wireguard.netdev index 075f2bd6a8..16f63d00bd 100644 --- a/test/test-network/conf/25-wireguard.netdev +++ b/test/test-network/conf/25-wireguard.netdev @@ -6,6 +6,9 @@ Kind=wireguard [WireGuard] ListenPort=51820 FwMark=1234 +# settings below are ignored. +RouteTable=off +RouteMetric=456 [WireGuardPeer] PublicKey=RDf+LSpeEre7YEIKaxg+wbpsNV7du+ktR99uBEtIiCA= @@ -14,3 +17,5 @@ AllowedIPs=fd31:bf08:57cb::/48,192.168.26.0/24 Endpoint=192.168.27.3:51820 PresharedKey=IIWIV17wutHv7t4cR6pOT91z6NSz/T8Arh0yaywhw3M= PersistentKeepalive=20 +RouteTable=1234 +RouteMetric=123 diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 4d4f792707..2c70a05719 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -1383,6 +1383,63 @@ class NetworkdNetDevTests(unittest.TestCase, Utilities): print(output) self.assertIn('inet6 fd8d:4d6d:3ccb:500::1/64 scope global', output) + output = check_output('ip -4 route show dev wg99 table 1234') + print(output) + self.assertIn('192.168.26.0/24 proto static metric 123', output) + + output = check_output('ip -6 route show dev wg99 table 1234') + print(output) + self.assertIn('fd31:bf08:57cb::/48 proto static metric 123 pref medium', output) + + output = check_output('ip -6 route show dev wg98 table 1234') + print(output) + self.assertIn('fd8d:4d6d:3ccb:500:c79:2339:edce:ece1 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:1dbf:ca8a:32d3:dd81 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:1e54:1415:35d0:a47c proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:270d:b5dd:4a3f:8909 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:5660:679d:3532:94d8 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:6825:573f:30f3:9472 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:6f2e:6888:c6fd:dfb9 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:8d4d:bab:7280:a09a proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:900c:d437:ec27:8822 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:9742:9931:5217:18d5 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:9c11:d820:2e96:9be0 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:a072:80da:de4f:add1 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:a3f3:df38:19b0:721 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:a94b:cd6a:a32d:90e6 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:b39c:9cdc:755a:ead3 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:b684:4f81:2e3e:132e proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:bad5:495d:8e9c:3427 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:bfe5:c3c3:5d77:fcb proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:c624:6bf7:4c09:3b59 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:d4f9:5dc:9296:a1a proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:dcdd:d33b:90c9:6088 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:e2e1:ae15:103f:f376 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:500:f349:c4f0:10c1:6b4 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:c79:2339:edce::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:1dbf:ca8a:32d3::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:1e54:1415:35d0::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:270d:b5dd:4a3f::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:5660:679d:3532::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:6825:573f:30f3::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:6f2e:6888:c6fd::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:8d4d:bab:7280::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:900c:d437:ec27::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:9742:9931:5217::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:9c11:d820:2e96::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:a072:80da:de4f::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:a3f3:df38:19b0::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:a94b:cd6a:a32d::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:b39c:9cdc:755a::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:b684:4f81:2e3e::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:bad5:495d:8e9c::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:bfe5:c3c3:5d77::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:c624:6bf7:4c09::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:d4f9:5dc:9296::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:dcdd:d33b:90c9::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:e2e1:ae15:103f::/96 proto static metric 123 pref medium', output) + self.assertIn('fd8d:4d6d:3ccb:f349:c4f0:10c1::/96 proto static metric 123 pref medium', output) + if shutil.which('wg'): call('wg')