From f941960779cb39759a7de41607e5125638cef59c Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Wed, 19 Aug 2026 14:35:44 -0700 Subject: [PATCH 1/6] implement update route as add --- src/linux/netlinkutil/RoutingTable.cpp | 10 ++++++- test/windows/NetworkTests.cpp | 37 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/linux/netlinkutil/RoutingTable.cpp b/src/linux/netlinkutil/RoutingTable.cpp index 8deb15f691..43e52b8f2e 100644 --- a/src/linux/netlinkutil/RoutingTable.cpp +++ b/src/linux/netlinkutil/RoutingTable.cpp @@ -92,7 +92,15 @@ void RoutingTable::ModifyRouteImpl(const Route& route, Operation action) int operation = 0; if (action == Update) { - flags = NLM_F_CREATE | NLM_F_REPLACE; + // Intentionally omit NLM_F_REPLACE. NLM_F_REPLACE matches an existing route by + // (destination prefix, tos, metric) alone - ignoring the next hop / output interface - + // so replacing a route can silently overwrite a *different* interface's route that happens + // to share the same key (e.g. two interfaces with a default route at the same metric), + // deleting it. An Update is send by the WSL service to mean "ensure this route exists", + // so a create is sufficient. + // Note: The EEXIST error is already treated as non-fatal. + // flags = NLM_F_CREATE; + flags = NLM_F_CREATE; operation = RTM_NEWROUTE; } else if (action == Create) diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index b06aae02b9..7d7e4439fd 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -367,6 +367,43 @@ class NetworkTests VERIFY_ARE_EQUAL(v6State.DefaultRoute->Device, L"eth0"); } + // Adds a dummy interface with a default route over it, then updates the default route on eth0 to have the same metric. + // The test verifies that both default routes exist after the update. + WSL2_TEST_METHOD(UpdateDefaultRouteKeepsOtherInterfaceRoute) + { + constexpr auto c_dummyInterface = L"wsldummy0"; + constexpr auto c_sharedMetric = 100; + const std::wstring metricArg = L" metric " + std::to_wstring(c_sharedMetric); + + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::wstring(L"ip link add ") + c_dummyInterface + L" type dummy"), (DWORD)0); + + auto removeDummyInterface = wil::scope_exit([&] { LxsstuLaunchWsl(std::wstring(L"ip link del ") + c_dummyInterface); }); + + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::wstring(L"ip link set ") + c_dummyInterface + L" up"), (DWORD)0); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::wstring(L"ip route add default dev ") + c_dummyInterface + metricArg), (DWORD)0); + + const auto defaultRouteExistsOnDevice = [&](const std::wstring& device) { + return LxsstuLaunchWsl( + L"ip -4 route show default | grep -w \"dev " + device + L"\" | grep -qw \"metric " + + std::to_wstring(c_sharedMetric) + L"\"") == (DWORD)0; + }; + + VERIFY_IS_TRUE(defaultRouteExistsOnDevice(c_dummyInterface)); + + wsl::shared::hns::Route route; + route.NextHop = L"0.0.0.0"; + route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX; + route.Family = AF_INET; + route.Metric = c_sharedMetric; + SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Update, GuestEndpointResourceType::Route); + + VERIFY_IS_TRUE(defaultRouteExistsOnDevice(c_dummyInterface)); + VERIFY_IS_TRUE(defaultRouteExistsOnDevice(L"eth0")); + + SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Remove, GuestEndpointResourceType::Route); + VERIFY_IS_FALSE(defaultRouteExistsOnDevice(L"eth0")); + } + WSL2_TEST_METHOD(AddDefaultRouteWithOfflinkGateway) { TestCase({{L"eth0", {{L"100.96.5.160", 32}}, L"100.96.5.161"}}); From b5745d509156c3cdc045cb22de8a54fb0facb63f Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Wed, 19 Aug 2026 14:39:05 -0700 Subject: [PATCH 2/6] typo --- src/linux/netlinkutil/RoutingTable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linux/netlinkutil/RoutingTable.cpp b/src/linux/netlinkutil/RoutingTable.cpp index 43e52b8f2e..8970b090bb 100644 --- a/src/linux/netlinkutil/RoutingTable.cpp +++ b/src/linux/netlinkutil/RoutingTable.cpp @@ -96,7 +96,7 @@ void RoutingTable::ModifyRouteImpl(const Route& route, Operation action) // (destination prefix, tos, metric) alone - ignoring the next hop / output interface - // so replacing a route can silently overwrite a *different* interface's route that happens // to share the same key (e.g. two interfaces with a default route at the same metric), - // deleting it. An Update is send by the WSL service to mean "ensure this route exists", + // deleting it. An Update is sent by the WSL service to mean "ensure this route exists", // so a create is sufficient. // Note: The EEXIST error is already treated as non-fatal. // flags = NLM_F_CREATE; From ccf1a1049bbb5661deb81a1167aeb552a9e2ddbc Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Wed, 19 Aug 2026 15:49:45 -0700 Subject: [PATCH 3/6] remove dead code in service side --- .../service/exe/WslMirroredNetworking.cpp | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/windows/service/exe/WslMirroredNetworking.cpp b/src/windows/service/exe/WslMirroredNetworking.cpp index 28b8d3137e..78a6da9c1c 100644 --- a/src/windows/service/exe/WslMirroredNetworking.cpp +++ b/src/windows/service/exe/WslMirroredNetworking.cpp @@ -1396,24 +1396,18 @@ _Check_return_ bool wsl::core::networking::WslMirroredNetworkManager::SyncIpStat std::optional hr{}; switch (trackedRoute.SyncStatus) { + // For routes, an Add and an Update resolve to the same guest netlink operation + // (RTM_NEWROUTE | NLM_F_CREATE, with EEXIST ignored), so a single Add is sufficient to plumb + // a new route or re-assert an existing one. case PendingAdd: + case PendingUpdate: hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Add); - if (FAILED(hr.value())) - { - // try to update it instead if it already exists - hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Update); - } break; case Synced: if (refreshAllRoutes) { - hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Update); - if (FAILED(hr.value())) - { - // try to add it - hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Add); - } + hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Add); if (FAILED(hr.value())) { trackedRoute.SyncStatus = PendingUpdate; @@ -1422,15 +1416,6 @@ _Check_return_ bool wsl::core::networking::WslMirroredNetworkManager::SyncIpStat } break; - case PendingUpdate: - hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Update); - if (FAILED(hr.value())) - { - // try to add it - hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Add); - } - break; - case PendingRemoval: // This route is still slated for removal, which we'll try again later. continue; From 1ec920357edaf807bff889b8bee728f4e8ee36bc Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Wed, 19 Aug 2026 16:36:43 -0700 Subject: [PATCH 4/6] move fix to service --- src/linux/netlinkutil/RoutingTable.cpp | 9 ++--- .../service/exe/WslMirroredNetworking.cpp | 7 ++-- test/windows/NetworkTests.cpp | 37 ------------------- 3 files changed, 7 insertions(+), 46 deletions(-) diff --git a/src/linux/netlinkutil/RoutingTable.cpp b/src/linux/netlinkutil/RoutingTable.cpp index 8970b090bb..8b7efe1668 100644 --- a/src/linux/netlinkutil/RoutingTable.cpp +++ b/src/linux/netlinkutil/RoutingTable.cpp @@ -92,15 +92,12 @@ void RoutingTable::ModifyRouteImpl(const Route& route, Operation action) int operation = 0; if (action == Update) { - // Intentionally omit NLM_F_REPLACE. NLM_F_REPLACE matches an existing route by + // Note: NLM_F_REPLACE matches an existing route by // (destination prefix, tos, metric) alone - ignoring the next hop / output interface - // so replacing a route can silently overwrite a *different* interface's route that happens // to share the same key (e.g. two interfaces with a default route at the same metric), - // deleting it. An Update is sent by the WSL service to mean "ensure this route exists", - // so a create is sufficient. - // Note: The EEXIST error is already treated as non-fatal. - // flags = NLM_F_CREATE; - flags = NLM_F_CREATE; + // deleting the second route. + flags = NLM_F_CREATE | NLM_F_REPLACE; operation = RTM_NEWROUTE; } else if (action == Create) diff --git a/src/windows/service/exe/WslMirroredNetworking.cpp b/src/windows/service/exe/WslMirroredNetworking.cpp index 78a6da9c1c..474c45deea 100644 --- a/src/windows/service/exe/WslMirroredNetworking.cpp +++ b/src/windows/service/exe/WslMirroredNetworking.cpp @@ -1396,9 +1396,10 @@ _Check_return_ bool wsl::core::networking::WslMirroredNetworkManager::SyncIpStat std::optional hr{}; switch (trackedRoute.SyncStatus) { - // For routes, an Add and an Update resolve to the same guest netlink operation - // (RTM_NEWROUTE | NLM_F_CREATE, with EEXIST ignored), so a single Add is sufficient to plumb - // a new route or re-assert an existing one. + // Use Add rather than Update to (re-)assert routes. An Update replaces by + // (destination prefix, tos, metric) key alone and would silently overwrite a same-key route + // belonging to a *different* interface (e.g. another interface's default at the same metric). + // A create plumbs a new route or leaves an existing one in place (EEXIST is ignored). case PendingAdd: case PendingUpdate: hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Add); diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 7d7e4439fd..b06aae02b9 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -367,43 +367,6 @@ class NetworkTests VERIFY_ARE_EQUAL(v6State.DefaultRoute->Device, L"eth0"); } - // Adds a dummy interface with a default route over it, then updates the default route on eth0 to have the same metric. - // The test verifies that both default routes exist after the update. - WSL2_TEST_METHOD(UpdateDefaultRouteKeepsOtherInterfaceRoute) - { - constexpr auto c_dummyInterface = L"wsldummy0"; - constexpr auto c_sharedMetric = 100; - const std::wstring metricArg = L" metric " + std::to_wstring(c_sharedMetric); - - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::wstring(L"ip link add ") + c_dummyInterface + L" type dummy"), (DWORD)0); - - auto removeDummyInterface = wil::scope_exit([&] { LxsstuLaunchWsl(std::wstring(L"ip link del ") + c_dummyInterface); }); - - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::wstring(L"ip link set ") + c_dummyInterface + L" up"), (DWORD)0); - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::wstring(L"ip route add default dev ") + c_dummyInterface + metricArg), (DWORD)0); - - const auto defaultRouteExistsOnDevice = [&](const std::wstring& device) { - return LxsstuLaunchWsl( - L"ip -4 route show default | grep -w \"dev " + device + L"\" | grep -qw \"metric " + - std::to_wstring(c_sharedMetric) + L"\"") == (DWORD)0; - }; - - VERIFY_IS_TRUE(defaultRouteExistsOnDevice(c_dummyInterface)); - - wsl::shared::hns::Route route; - route.NextHop = L"0.0.0.0"; - route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX; - route.Family = AF_INET; - route.Metric = c_sharedMetric; - SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Update, GuestEndpointResourceType::Route); - - VERIFY_IS_TRUE(defaultRouteExistsOnDevice(c_dummyInterface)); - VERIFY_IS_TRUE(defaultRouteExistsOnDevice(L"eth0")); - - SendDeviceSettingsRequest(L"eth0", route, ModifyRequestType::Remove, GuestEndpointResourceType::Route); - VERIFY_IS_FALSE(defaultRouteExistsOnDevice(L"eth0")); - } - WSL2_TEST_METHOD(AddDefaultRouteWithOfflinkGateway) { TestCase({{L"eth0", {{L"100.96.5.160", 32}}, L"100.96.5.161"}}); From 52750b9d0d0dbd7b87e203d4d925adef4cb0a9c2 Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Thu, 20 Aug 2026 13:42:02 -0700 Subject: [PATCH 5/6] add more details in comment --- src/windows/service/exe/WslMirroredNetworking.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/windows/service/exe/WslMirroredNetworking.cpp b/src/windows/service/exe/WslMirroredNetworking.cpp index 474c45deea..6268f5d7d6 100644 --- a/src/windows/service/exe/WslMirroredNetworking.cpp +++ b/src/windows/service/exe/WslMirroredNetworking.cpp @@ -1396,10 +1396,17 @@ _Check_return_ bool wsl::core::networking::WslMirroredNetworkManager::SyncIpStat std::optional hr{}; switch (trackedRoute.SyncStatus) { - // Use Add rather than Update to (re-)assert routes. An Update replaces by + // Use ModifyRequestType::Add rather than ModifyRequestType::Update for PendingUpdate. + // An Update in GNS uses NLM_F_REPLACE and replaces by // (destination prefix, tos, metric) key alone and would silently overwrite a same-key route // belonging to a *different* interface (e.g. another interface's default at the same metric). - // A create plumbs a new route or leaves an existing one in place (EEXIST is ignored). + // An ModifyRequestType::Add plumbs a new route or leaves an existing one in place (EEXIST is ignored), + // which is the desited behavior for PendingUpdate. + // + // The route synchronization logic never attempts an in-place update of a route. Whenever + // a route change occurs on the host (ProcessRouteChange), we mark for removal the routes that are known + // to have been synced in Linux but are no longer part of the latest set of host routes, we do not + // do a diff of the route properties to determine if an in-place update is needed. case PendingAdd: case PendingUpdate: hr = SendRouteRequestToGns(endpoint, trackedRoute, hns::ModifyRequestType::Add); From e3083c42a4fa1d8e5075ad17d7b65261e9f59b52 Mon Sep 17 00:00:00 2001 From: Catalin-Emil Fetoiu Date: Thu, 20 Aug 2026 13:43:03 -0700 Subject: [PATCH 6/6] typo --- src/windows/service/exe/WslMirroredNetworking.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/service/exe/WslMirroredNetworking.cpp b/src/windows/service/exe/WslMirroredNetworking.cpp index 6268f5d7d6..158cf50571 100644 --- a/src/windows/service/exe/WslMirroredNetworking.cpp +++ b/src/windows/service/exe/WslMirroredNetworking.cpp @@ -1401,7 +1401,7 @@ _Check_return_ bool wsl::core::networking::WslMirroredNetworkManager::SyncIpStat // (destination prefix, tos, metric) key alone and would silently overwrite a same-key route // belonging to a *different* interface (e.g. another interface's default at the same metric). // An ModifyRequestType::Add plumbs a new route or leaves an existing one in place (EEXIST is ignored), - // which is the desited behavior for PendingUpdate. + // which is the desired behavior for PendingUpdate. // // The route synchronization logic never attempts an in-place update of a route. Whenever // a route change occurs on the host (ProcessRouteChange), we mark for removal the routes that are known