new file mode 100644
@@ -0,0 +1,166 @@
+From ec4f646759d6625bbafcd4ce67224fb63b99a96c Mon Sep 17 00:00:00 2001
+From: Yu Watanabe <watanabe.yu+github@gmail.com>
+Date: Mon, 9 Mar 2026 10:57:06 +0900
+Subject: [PATCH] network: introduce link_is_up() helper function
+
+Upstream-Status: Backport [https://github.com/systemd/systemd/pull/41000/commits/ec4f646759d6625bbafcd4ce67224fb63b99a96c]
+Comment: Patch is refreshed based on the 255.22 codebase
+Did not pick changes from the below files as it is not matches to our code base.
+src/network/netdev/bond.c
+src/network/networkd-route.c
+
+Signed-off-by: Akash Hadke <Akash.Hadke@bmwtechworks.in>
+---
+ src/network/networkd-link.c | 21 +++++++++++----------
+ src/network/networkd-link.h | 1 +
+ src/network/networkd-nexthop.c | 4 ++--
+ src/network/networkd-route.c | 2 +-
+ src/network/networkd-setlink.c | 11 +++++------
+ 6 files changed, 21 insertions(+), 20 deletions(-)
+
+diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c
+index e49d5946aa67d..c77b02b8e0192 100644
+--- a/src/network/networkd-link.c
++++ b/src/network/networkd-link.c
+@@ -726,7 +726,6 @@ int link_ipv6ll_gained(Link *link) {
+
+ int link_handle_bound_to_list(Link *link) {
+ bool required_up = false;
+- bool link_is_up = false;
+ Link *l;
+
+ assert(link);
+@@ -737,18 +736,15 @@ int link_handle_bound_to_list(Link *link) {
+ if (hashmap_isempty(link->bound_to_links))
+ return 0;
+
+- if (link->flags & IFF_UP)
+- link_is_up = true;
+-
+ HASHMAP_FOREACH(l, link->bound_to_links)
+ if (link_has_carrier(l)) {
+ required_up = true;
+ break;
+ }
+
+- if (!required_up && link_is_up)
++ if (!required_up && link_is_up(link))
+ return link_request_to_bring_up_or_down(link, /* up = */ false);
+- if (required_up && !link_is_up)
++ if (required_up && !link_is_up(link))
+ return link_request_to_bring_up_or_down(link, /* up = */ true);
+
+ return 0;
+@@ -1736,7 +1732,7 @@ void link_update_operstate(Link *link, bool also_update_master) {
+ carrier_state = LINK_CARRIER_STATE_ENSLAVED;
+ else
+ carrier_state = LINK_CARRIER_STATE_CARRIER;
+- } else if (link->flags & IFF_UP)
++ } else if (link_is_up(link))
+ carrier_state = LINK_CARRIER_STATE_NO_CARRIER;
+ else
+ carrier_state = LINK_CARRIER_STATE_OFF;
+@@ -1867,6 +1863,11 @@ void link_update_operstate(Link *link, bool also_update_master) {
+ ? ((old & flag) ? (" -" string) : (" +" string)) \
+ : "")
+
++bool link_is_up(Link *link) {
++ assert(link);
++ return FLAGS_SET(link->flags, IFF_UP);
++}
++
+ static int link_update_flags(Link *link, sd_netlink_message *message) {
+ bool link_was_admin_up, had_carrier;
+ uint8_t operstate;
+@@ -1930,21 +1931,21 @@ static int link_update_flags(Link *link, sd_netlink_message *message) {
+ log_link_debug(link, "Unknown link flags lost, ignoring: %#.5x", unknown_flags_removed);
+ }
+
+- link_was_admin_up = link->flags & IFF_UP;
++ link_was_admin_up = link_is_up(link);
+ had_carrier = link_has_carrier(link);
+
+ link->flags = flags;
+ link->kernel_operstate = operstate;
+
+ link_update_operstate(link, true);
+-
+- if (!link_was_admin_up && (link->flags & IFF_UP)) {
++
++ if (!link_was_admin_up && link_is_up(link)) {
+ log_link_info(link, "Link UP");
+
+ r = link_admin_state_up(link);
+ if (r < 0)
+ return r;
+- } else if (link_was_admin_up && !(link->flags & IFF_UP)) {
++ } else if (link_was_admin_up && !link_is_up(link)) {
+ log_link_info(link, "Link DOWN");
+
+ r = link_admin_state_down(link);
+diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h
+index c5b9421bc0b2b..9c641fad39bdb 100644
+--- a/src/network/networkd-link.h
++++ b/src/network/networkd-link.h
+@@ -233,6 +233,7 @@ static inline bool link_has_carrier(Link *link) {
+ assert(link);
+ return netif_has_carrier(link->kernel_operstate, link->flags);
+ }
++bool link_is_up(Link *link);
+
+ bool link_ipv6_enabled(Link *link);
+ int link_ipv6ll_gained(Link *link);
+diff --git a/src/network/networkd-nexthop.c b/src/network/networkd-nexthop.c
+index 5e5f0d0e6858b..23941527cfdcf 100644
+--- a/src/network/networkd-nexthop.c
++++ b/src/network/networkd-nexthop.c
+@@ -526,7 +526,7 @@ static bool nexthop_is_ready_to_configur
+ * kernel. */
+ if (link->set_flags_messages > 0)
+ return false;
+- if (!FLAGS_SET(link->flags, IFF_UP))
++ if (!link_is_up(link))
+ return false;
+ }
+
+diff --git a/src/network/networkd-setlink.c b/src/network/networkd-setlink.c
+index 7069b101f9f55..f5a43788ef792 100644
+--- a/src/network/networkd-setlink.c
++++ b/src/network/networkd-setlink.c
+@@ -486,7 +486,7 @@ static int link_is_ready_to_set_link(Lin
+ case REQUEST_TYPE_SET_LINK_CAN:
+ /* Do not check link->set_flags_messages here, as it is ok even if link->flags
+ * is outdated, and checking the counter causes a deadlock. */
+- if (FLAGS_SET(link->flags, IFF_UP)) {
++ if (link_is_up(link)) {
+ /* The CAN interface must be down to configure bitrate, etc... */
+ r = link_down_now(link);
+ if (r < 0)
+@@ -550,7 +550,7 @@ static int link_is_ready_to_set_link(Lin
+
+ /* Do not check link->set_flags_messages here, as it is ok even if link->flags is outdated,
+ * and checking the counter causes a deadlock. */
+- if (link->network->bond && FLAGS_SET(link->flags, IFF_UP)) {
++ if (link->network->bond && link_is_up(link)) {
+ /* link must be down when joining to bond master. */
+ r = link_down_now(link);
+ if (r < 0)
+@@ -649,8 +649,7 @@ int link_request_to_set_addrgen_mode(Lin
+ * link goes down. Hence, we need to reset the interface. However, setting the mode by sysctl
+ * does not need that. Let's use the sysctl interface when the link is already up.
+ * See also issue #22424. */
+- if (mode != IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE &&
+- FLAGS_SET(link->flags, IFF_UP)) {
++ if (mode != IPV6_LINK_LOCAL_ADDRESSS_GEN_MODE_NONE && link_is_up(link)) {
+ r = link_set_ipv6ll_addrgen_mode(link, mode);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Cannot set IPv6 address generation mode, ignoring: %m");
+@@ -1142,7 +1141,7 @@ static bool link_is_ready_to_bring_up_or
+ if (link_get_by_index(link->manager, link->dsa_master_ifindex, &master) < 0)
+ return false;
+
+- if (!FLAGS_SET(master->flags, IFF_UP))
++ if (!link_is_up(master))
+ return false;
+ }
+
new file mode 100644
@@ -0,0 +1,72 @@
+From 43989de6427954f0755e2667bda1cf3839d27964 Mon Sep 17 00:00:00 2001
+From: Yu Watanabe <watanabe.yu+github@gmail.com>
+Date: Mon, 9 Mar 2026 11:30:54 +0900
+Subject: [PATCH] network: check if gateway is ready only when the nexthop is
+ bound to link
+
+Currently, we support three types of nexthop:
+1. simple nexthop, which is bound to link, may have specific gateway
+ address,
+2. blackhole nexthop, which is global configuration and is not bound to
+ any links,
+3. group nexthop, which is also global configuration and is not bound to
+ any links.
+
+Thus, gateway_is_ready() is only necessary to call for simple nexthop
+case. Let's make the logic simpler.
+
+Upstream-Status: Backport [https://github.com/systemd/systemd/commit/43989de6427954f0755e2667bda1cf3839d27964]
+Comment: Patch is refreshed based on 255.22 codebase
+
+Signed-off-by: Akash Hadke <Akash.Hadke@bmwtechworks.in>
+---
+ src/network/networkd-nexthop.c | 22 +++++++++++++++++-----
+ 1 file changed, 17 insertions(+), 5 deletions(-)
+
+diff --git a/src/network/networkd-nexthop.c b/src/network/networkd-nexthop.c
+index 23941527cfdcf..81eb85b4a06c1 100644
+--- a/src/network/networkd-nexthop.c
++++ b/src/network/networkd-nexthop.c
+@@ -520,17 +520,29 @@ static bool nexthop_is_ready_to_configur
+ if (!link_is_ready_to_configure(link, false))
+ return false;
+
++ /* Currently, we support the following three types of nexthops:
++ * 1. Simple nexthop - bound to the link, requires the underlying link is up.
++ * 2. Blackhole nexthop - not bound to the link.
++ * 3. Group nexthop - not bound to the link, but all group members must be configured first.
++ *
++ * Note, the kernel also supports fdb nexthop, but currently we do not support it. Note, fdb nexthop
++ * does not require IFF_UP. See rtm_to_nh_config() in net/ipv4/nexthop.c of kernel. */
++
++ /* Simple nexthop */
+ if (nexthop_owned_by_link(nexthop)) {
+- /* TODO: fdb nexthop does not require IFF_UP. The conditions below needs to be updated
+- * when fdb nexthop support is added. See rtm_to_nh_config() in net/ipv4/nexthop.c of
+- * kernel. */
+ if (link->set_flags_messages > 0)
+ return false;
+ if (!link_is_up(link))
+ return false;
++
++ return gateway_is_ready(link, FLAGS_SET(nexthop->flags, RTNH_F_ONLINK), nexthop->family, &nexthop->gw);
+ }
+
+- /* All group members must be configured first. */
++ /* Blackhole nexthop */
++ if (nexthop->blackhole)
++ return true;
++
++ /* Group nexthop */
+ HASHMAP_FOREACH(nhg, nexthop->group) {
+ NextHop *g;
+
+@@ -552,7 +564,7 @@ static bool nexthop_is_ready_to_configur
+ }
+ }
+
+- return gateway_is_ready(link, FLAGS_SET(nexthop->flags, RTNH_F_ONLINK), nexthop->family, &nexthop->gw);
++ return true;
+ }
+
+ static int nexthop_process_request(Request *req, Link *link, NextHop *nexthop) {
new file mode 100644
@@ -0,0 +1,57 @@
+From ed54648b47779d2ba24ceffd16b7b63ec0845043 Mon Sep 17 00:00:00 2001
+From: Yu Watanabe <watanabe.yu+github@gmail.com>
+Date: Mon, 9 Mar 2026 11:46:01 +0900
+Subject: [PATCH] network: route bound to a link requires the link is up
+
+We checked if the link is up only when configuring (explicit) nexthop,
+but we did not checked that when configuring route which has (implicit)
+nexthop.
+
+Let's move the checks from nexthop_is_ready_to_configure() to
+gateway_is_ready(), which is called for both implicit and explict
+nexthops.
+
+Fixes #40106.
+
+Upstream-Status: Backport [https://github.com/systemd/systemd/commit/ed54648b47779d2ba24ceffd16b7b63ec0845043]
+Comment: Patch is refreshed based on 255.22 codebase
+
+Signed-off-by: Akash Hadke <Akash.Hadke@bmwtechworks.in>
+---
+ src/network/networkd-nexthop.c | 5 -----
+ src/network/networkd-route-util.c | 6 ++++++
+ 2 files changed, 6 insertions(+), 5 deletions(-)
+
+diff --git a/src/network/networkd-nexthop.c b/src/network/networkd-nexthop.c
+index 81eb85b4a06c1..0e453e159748a 100644
+--- a/src/network/networkd-nexthop.c
++++ b/src/network/networkd-nexthop.c
+@@ -530,11 +530,6 @@ static bool nexthop_is_ready_to_configur
+
+ /* Simple nexthop */
+ if (nexthop_owned_by_link(nexthop)) {
+- if (link->set_flags_messages > 0)
+- return false;
+- if (!link_is_up(link))
+- return false;
+-
+ return gateway_is_ready(link, FLAGS_SET(nexthop->flags, RTNH_F_ONLINK), nexthop->family, &nexthop->gw);
+ }
+
+diff --git a/src/network/networkd-route-util.c b/src/network/networkd-route-util.c
+index 37ace2d335c60..3a7156fc4129f 100644
+--- a/src/network/networkd-route-util.c
++++ b/src/network/networkd-route-util.c
+@@ -128,6 +128,12 @@ bool gateway_is_ready(Link *link, bool o
+ assert(link);
+ assert(link->manager);
+
++ if (link->set_flags_messages > 0)
++ return false;
++
++ if (!link_is_up(link))
++ return false;
++
+ if (onlink)
+ return true;
+
new file mode 100644
@@ -0,0 +1,117 @@
+From f47d180b855dd4eb0a11d44d001c99f475dda56b Mon Sep 17 00:00:00 2001
+From: Yu Watanabe <watanabe.yu+github@gmail.com>
+Date: Mon, 9 Mar 2026 12:31:41 +0900
+Subject: [PATCH] test-network: add test case for issue #40106
+
+Upstream-Status: Backport [https://github.com/systemd/systemd/commit/f47d180b855dd4eb0a11d44d001c99f475dda56b]
+
+Signed-off-by: Akash Hadke <Akash.Hadke@bmwtechworks.in>
+---
+ .../25-route-static-issue-40106-dummy.network | 7 ++++
+ .../25-route-static-issue-40106-vlan.netdev | 7 ++++
+ .../25-route-static-issue-40106-vlan.network | 15 +++++++
+ test/test-network/systemd-networkd-tests.py | 41 +++++++++++++++++++
+ 4 files changed, 70 insertions(+)
+ create mode 100644 test/test-network/conf/25-route-static-issue-40106-dummy.network
+ create mode 100644 test/test-network/conf/25-route-static-issue-40106-vlan.netdev
+ create mode 100644 test/test-network/conf/25-route-static-issue-40106-vlan.network
+
+diff --git a/test/test-network/conf/25-route-static-issue-40106-dummy.network b/test/test-network/conf/25-route-static-issue-40106-dummy.network
+new file mode 100644
+index 0000000000000..08553b97f5f19
+--- /dev/null
++++ b/test/test-network/conf/25-route-static-issue-40106-dummy.network
+@@ -0,0 +1,7 @@
++# SPDX-License-Identifier: LGPL-2.1-or-later
++[Match]
++Name=dummy99
++
++[Network]
++ConfigureWithoutCarrier=true
++VLAN=vlan99
+diff --git a/test/test-network/conf/25-route-static-issue-40106-vlan.netdev b/test/test-network/conf/25-route-static-issue-40106-vlan.netdev
+new file mode 100644
+index 0000000000000..c40e7b755aefa
+--- /dev/null
++++ b/test/test-network/conf/25-route-static-issue-40106-vlan.netdev
+@@ -0,0 +1,7 @@
++# SPDX-License-Identifier: LGPL-2.1-or-later
++[NetDev]
++Kind=vlan
++Name=vlan99
++
++[VLAN]
++Id=99
+diff --git a/test/test-network/conf/25-route-static-issue-40106-vlan.network b/test/test-network/conf/25-route-static-issue-40106-vlan.network
+new file mode 100644
+index 0000000000000..442cf480fbc51
+--- /dev/null
++++ b/test/test-network/conf/25-route-static-issue-40106-vlan.network
+@@ -0,0 +1,15 @@
++# SPDX-License-Identifier: LGPL-2.1-or-later
++[Match]
++Name=vlan99
++
++[Network]
++ConfigureWithoutCarrier=true
++BindCarrier=dummy99
++
++[Address]
++Address=192.0.2.1/24
++
++[Route]
++Destination=198.51.100.1
++Type=multicast
++Scope=link
+diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
+index bbbab8e3fe636..6f2fe1b9eb65c 100755
+--- a/test/test-network/systemd-networkd-tests.py
++++ b/test/test-network/systemd-networkd-tests.py
+@@ -4603,6 +4603,47 @@ def test_route_via_ipv6(self):
+ self.assertRegex(output, '149.10.124.48/28 proto kernel scope link src 149.10.124.58')
+ self.assertRegex(output, '149.10.124.66 via inet6 2001:1234:5:8fff:ff:ff:ff:ff proto static')
+
++ def test_route_static_issue_40106(self):
++ check_output('ip link add dummy99 type dummy')
++ check_output('ip link set dummy99 up carrier off')
++ copy_network_unit(
++ '25-route-static-issue-40106-dummy.network',
++ '25-route-static-issue-40106-vlan.netdev',
++ '25-route-static-issue-40106-vlan.network',
++ )
++ start_networkd()
++ self.wait_online('dummy99:no-carrier')
++ self.wait_operstate('vlan99', operstate='off', setup_state='configuring')
++
++ # address can be configured even when the interface is down.
++ self.wait_address('vlan99', '192.0.2.1/24', ipv='-4', timeout_sec=10)
++ print('### ip -4 address show dev vlan99')
++ output = check_output('ip -4 address show dev vlan99')
++ print(output)
++ self.assertIn('inet 192.0.2.1/24 brd 192.0.2.255 scope global vlan99', output)
++
++ # route cannot be configured when the interface is down.
++ print('### ip -4 route show dev vlan99')
++ output = check_output('ip -4 route show dev vlan99')
++ print(output)
++ self.assertEqual(output, '')
++
++ # When cable is connected, the vlan becomes up by BindCarrier=, then
++ # the pending route is also configured.
++ check_output('ip link set dummy99 carrier on')
++ self.wait_online('dummy99:degraded', 'vlan99:routable')
++
++ print('### ip -4 address show dev vlan99')
++ output = check_output('ip -4 address show dev vlan99')
++ print(output)
++ self.assertIn('inet 192.0.2.1/24 brd 192.0.2.255 scope global vlan99', output)
++
++ print('### ip -4 route show dev vlan99')
++ output = check_output('ip -4 route show dev vlan99')
++ print(output)
++ self.assertIn('192.0.2.0/24 proto kernel scope link src 192.0.2.1', output)
++ self.assertIn('multicast 198.51.100.1 proto static scope link', output)
++
+ @expectedFailureIfModuleIsNotAvailable('tcp_dctcp')
+ def test_route_congctl(self):
+ copy_network_unit('25-route-congctl.network', '12-dummy.netdev')
@@ -37,6 +37,10 @@ SRC_URI += " \
file://CVE-2026-29111-02.patch \
file://CVE-2026-29111-03.patch \
file://CVE-2026-29111-04.patch \
+ file://0023-network-introduce-link_is_up-helper-function.patch \
+ file://0024-network-check-if-gateway-is-ready-only-when-the-nexthop-is-bound-to-link.patch \
+ file://0025-network-route-bound-to-a-link-requires-the-link-is-up.patch \
+ file://0026-test-network-add-test-case-for-issue-40106.patch \
"
# patches needed by musl