network: Support interface-bound ECMP routes in MultiPathRoute= (#39742)

MultiPathRoute= can now specify device-only nexthops without a gateway
address, e.g. MultiPathRoute=@wg0. This enables ECMP configurations over
interfaces that don't use gateway addresses, such as WireGuard tunnels.

The syntax is extended from "address[@device] [weight]" to
"[address]@device [weight]". The address is now optional, but at least
one of gateway or device must be specified. The @ symbol must still be
present for device-only routes, making the syntax unambiguous: @wg0
specifies a device, while a bare IP address specifies a gateway.

Device-only nexthops are only available for IPv4 routes. Device-only
multipath routes for IPv6 are not supported by the kernel's netlink
interface and will be rejected with a warning.

This change is fully backwards compatible. All existing configurations
continue to work unchanged, as they always included a gateway address.

Closes #39699.
This commit is contained in:
Yu Watanabe
2026-01-02 09:25:43 +09:00
committed by GitHub
5 changed files with 230 additions and 38 deletions
@@ -115,3 +115,8 @@ Peer=1.1.8.104/31
[Route]
Gateway=1.1.8.104
[Route]
Destination=192.168.20.0/24
MultiPathRoute=@test1 15
MultiPathRoute=@dummy98 25
@@ -4392,6 +4392,13 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities):
self.assertIn('via 2001:1234:5:8fff:ff:ff:ff:ff dev dummy98', output)
self.assertIn('via 2001:1234:5:9fff:ff:ff:ff:ff dev dummy98', output)
print('### ip route show 192.168.20.0/24')
output = check_output('ip route show 192.168.20.0/24')
print(output)
self.assertIn('192.168.20.0/24 proto static', output)
self.assertIn('nexthop dev test1 weight 15', output)
self.assertIn('nexthop dev dummy98 weight 25', output)
check_json(networkctl_json())
def _check_unreachable_routes_removed(self):