diff --git a/src/network/networkd-route.c b/src/network/networkd-route.c index 11fa624d14..67c5d5ae8e 100644 --- a/src/network/networkd-route.c +++ b/src/network/networkd-route.c @@ -1538,15 +1538,22 @@ int link_drop_routes(Link *link, bool only_static) { if (!link_should_mark_config(link, only_static, route->source, route->protocol)) continue; - /* When we also mark foreign routes, do not mark routes assigned to other interfaces. - * Otherwise, routes assigned to unmanaged interfaces will be dropped. - * Note, route_get_link() does not provide assigned link for routes with an unreachable type - * or IPv4 multipath routes. So, the current implementation does not support managing such - * routes by other daemon or so, unless ManageForeignRoutes=no. */ - if (!only_static) { - Link *route_link; + Link *route_link = NULL; + if (route_get_link(link->manager, route, &route_link) >= 0 && route_link != link) { + /* When we also mark foreign routes, do not mark routes assigned to other interfaces. + * Otherwise, routes assigned to unmanaged interfaces will be dropped. + * Note, route_get_link() does not provide assigned link for routes with an + * unreachable type or IPv4 multipath routes. So, the current implementation does not + * support managing such routes by other daemon or so, unless ManageForeignRoutes=no. */ + if (!only_static) + continue; - if (route_get_link(link->manager, route, &route_link) >= 0 && route_link != link) + /* When we mark only static routes, do not mark routes assigned to links that we do + * not know the assignment of .network files to the interfaces. Otherwise, if an + * interface is in the pending state, even if the .network file to be assigned to the + * interface has KeepConfiguration=yes, routes on the interface will be removed. + * This is especially important when systemd-networkd is restarted. */ + if (!IN_SET(route_link->state, LINK_STATE_UNMANAGED, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED)) continue; } diff --git a/test/test-network/conf/85-static-ipv6.network b/test/test-network/conf/85-static-ipv6.network new file mode 100644 index 0000000000..50322b11e4 --- /dev/null +++ b/test/test-network/conf/85-static-ipv6.network @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=dummy98 + +[Network] +DHCP=no +IPv6AcceptRA=no +KeepConfiguration=yes + +[Address] +Address=2001:db8:0:f101::15/64 +PreferredLifetime=forever + +[Address] +Address=2001:db8:1:f101::15/64 +PreferredLifetime=0 + +[Route] +Gateway=fe80::f0ca:cc1a +Source=::/0 +Destination=::/0 +Metric=1 diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 60759610cc..872fc489a7 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -32,6 +32,7 @@ import socket import struct import subprocess import sys +import tempfile import time import unittest @@ -4952,6 +4953,62 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): self.assertRegex(output, 'inet 10.1.2.3/16 scope global dummy98') self.assertNotRegex(output, 'inet 10.2.3.4/16 scope global dynamic dummy98') + def check_keep_configuration_on_restart(self): + self.wait_online('dummy98:routable') + self.wait_online('unmanaged0:routable', setup_state='unmanaged') + + print('### ip -6 address show dev dummy98') + output = check_output('ip -6 address show dev dummy98') + print(output) + self.assertIn('inet6 2001:db8:0:f101::15/64 scope global', output) + self.assertIn('inet6 2001:db8:1:f101::15/64 scope global deprecated', output) + + print('### ip -6 address show dev unmanaged0') + output = check_output('ip -6 address show dev unmanaged0') + print(output) + self.assertIn('inet6 2001:db8:9999:f101::15/64 scope global', output) + + print('### ip -6 route show default dev dummy98') + output = check_output('ip -6 route show default dev dummy98') + print(output) + self.assertIn('default via fe80::f0ca:cc1a proto static metric 1 pref medium', output) + + def test_keep_configuration_on_restart(self): + # Add an unmanaged interface with an up address + call('ip link add unmanaged0 type dummy') + call('ip link set unmanaged0 up') + call('ip -6 addr add 2001:db8:9999:f101::15/64 dev unmanaged0') + + copy_network_unit('12-dummy.netdev', '85-static-ipv6.network') + start_networkd() + self.check_keep_configuration_on_restart() + + # Start `ip monitor` with output to a temporary file + with tempfile.TemporaryFile(mode='r+', prefix='ip_monitor') as logfile: + process = subprocess.Popen(['ip', 'monitor', 'dev', 'dummy98'], stdout=logfile, text=True) + restart_networkd() + self.check_keep_configuration_on_restart() + + process.send_signal(signal.SIGTERM) + process.wait() + + print('### ip monitor dev dummy98 BEGIN') + + # Read the `ip monitor` output looking for network changes + logfile.seek(0) + for line in logfile: + print(line, end="") + # Check if a link went down + self.assertNotRegex(line, 'unmanaged0: .* state DOWN') + self.assertNotRegex(line, 'dummy98: .* state DOWN') + # Check if an address was removed + self.assertNotRegex(line, '^Deleted .* 2001:db8:') + self.assertNotRegex(line, '^Deleted 2001:db8:.*/64') + # Check if the default route was removed + self.assertNotRegex(line, '^Deleted default via fe80::f0ca:cc1a') + + print('### ip monitor dev dummy98 END') + def check_nexthop(self, manage_foreign_nexthops, first): self.wait_online('veth99:routable', 'veth-peer:routable', 'dummy98:routable')