@samitouri / QOSAMI-WSL / commits / 722fb02e

Fixes to route mirroring (#40099)

* route fixes from protonvpn testing * add unit test, fix log * ai code review * format with vs 2022 * review * remove optional has_value checks * harden contracts for to, via having or not having values * update IsOnlink check * fix constructor parameters * fix build --------- Co-authored-by: Catalin-Emil Fetoiu <cfetoiu@microsoft.com>

FetoiuCatalin committed Apr 13, 2026 at 11:31 UTC 722fb02e330f2e9cb5c99213540e0b19425c3ddc
6 files changed +123 -22
src/linux/netlinkutil/Route.cpp
+8 -3
@@ -3,9 +3,14 @@
3 #include "Route.h"
4 #include "Utils.h"
5
6 -Route::Route(int family, const std::optional<Address>& via, int dev, bool defaultRoute, const std::optional<Address>& to, int metric) :
7 - family(family), via(via), dev(dev), defaultRoute(defaultRoute), to(to), metric(metric)
6 +Route::Route(int routeFamily, const std::optional<Address>& routeNextHop, int routeInterface, bool isRouteDefault, const std::optional<Address>& routeDestination, int routeMetric) :
7 + family(routeFamily), via(routeNextHop), dev(routeInterface), defaultRoute(isRouteDefault), to(routeDestination), metric(routeMetric)
8 {
9 + // For onlink routes, ensure the via field is empty
10 + if (via.has_value() && ((family == AF_INET && via->Addr() == "0.0.0.0") || (family == AF_INET6 && via->Addr() == "::")))
11 + {
12 + via.reset();
13 + }
14 }
15
16 std::ostream& operator<<(std::ostream& out, const Route& route)
@@ -30,7 +35,7 @@ std::ostream& operator<<(std::ostream& out, const Route& route)
35
36 bool Route::IsOnlink() const
37 {
33 - return !via.has_value() || (family == AF_INET && via->Addr() == "0.0.0.0") || (family == AF_INET6 && via->Addr() == "::");
38 + return !via.has_value();
39 }
40
41 bool Route::IsMulticast() const
src/linux/netlinkutil/Route.h
+1 -1
@@ -14,7 +14,7 @@ struct Route
14 int metric = 0;
15 bool isLoopbackRoute = false;
16
17 - Route(int family, const std::optional<Address>& via, int dev, bool defaultRoute, const std::optional<Address>& to, int metric);
17 + Route(int routeFamily, const std::optional<Address>& routeNextHop, int routeInterface, bool isRouteDefault, const std::optional<Address>& routeDestination, int routeMetric);
18
19 bool IsOnlink() const;
20 bool IsMulticast() const;
src/linux/netlinkutil/RoutingTable.cpp
+67 -18
@@ -111,6 +111,10 @@ void RoutingTable::ModifyRouteImpl(const Route& route, Operation action)
111 {
112 ModifyLoopbackRouteImpl<TAddr>(route, operation, flags);
113 }
114 + else if (route.defaultRoute && route.IsOnlink())
115 + {
116 + ModifyDefaultLinkLocalRouteImpl<TAddr>(route, operation, flags);
117 + }
118 else if (route.defaultRoute)
119 {
120 ModifyDefaultRouteImpl<TAddr>(route, operation, flags);
@@ -181,7 +185,7 @@ void RoutingTable::ModifyLoopbackRouteImpl(const Route& route, int operation, in
185 {
186 if (!route.to.has_value() || !route.via.has_value())
187 {
184 - throw RuntimeErrorWithSourceLocation(std::format("Loopback route {} missing destination or gateway address", utils::Stringify(route)));
188 + throw RuntimeErrorWithSourceLocation(std::format("Loopback route {} missing destination or next hop", utils::Stringify(route)));
189 }
190
191 struct Message : RouteMessage
@@ -193,8 +197,8 @@ void RoutingTable::ModifyLoopbackRouteImpl(const Route& route, int operation, in
197
198 GNS_LOG_INFO(
199 "SendMessage Route (to {}, via {}), operation ({}), netLinkflags ({})",
196 - route.to.has_value() ? route.to.value().Addr().c_str() : "[empty]",
197 - route.via.has_value() ? route.via.value().Addr().c_str() : "[empty]",
200 + route.to.value().Addr().c_str(),
201 + route.via.value().Addr().c_str(),
202 RouteOperationToString(operation),
203 NetLinkFormatFlagsToString(flags).c_str());
204
@@ -222,7 +226,7 @@ void RoutingTable::ModifyLoopbackRouteImpl(const Route& route, int operation, in
226
227 message.route.rtm_flags |= RTNH_F_ONLINK;
228 GNS_LOG_INFO(
225 - "InitializeAddressAttribute RTA_DST ({}) RTA_GATEWAY ({}) RTA_PRIORITY ([not set])",
229 + "Netlink message configuration: RTA_DST ({}) RTA_GATEWAY ({}) RTA_PRIORITY ([not set])",
230 route.to.value().Addr().c_str(),
231 route.via.value().Addr().c_str());
232 utils::InitializeAddressAttribute<TAddr>(message.to, route.to.value(), RTA_DST);
@@ -230,12 +234,44 @@ void RoutingTable::ModifyLoopbackRouteImpl(const Route& route, int operation, in
234 });
235 }
236
237 +template <typename TAddr>
238 +void RoutingTable::ModifyDefaultLinkLocalRouteImpl(const Route& route, int operation, int flags)
239 +{
240 + if (route.via.has_value())
241 + {
242 + throw RuntimeErrorWithSourceLocation("Default route has unexpected next hop");
243 + }
244 + if (route.to.has_value())
245 + {
246 + throw RuntimeErrorWithSourceLocation("Default route has unexpected destination address");
247 + }
248 +
249 + struct Message : RouteMessage
250 + {
251 + utils::IntegerAttribute metric;
252 + } __attribute__((packed));
253 +
254 + GNS_LOG_INFO(
255 + "SendMessage Route (default onlink), operation ({}), netLinkflags ({})",
256 + RouteOperationToString(operation),
257 + NetLinkFormatFlagsToString(flags).c_str());
258 +
259 + SendMessage<Message>(route, operation, flags, [&](Message& message) {
260 + GNS_LOG_INFO("Netlink message configuration: RTA_DST ([not set]) RTA_GATEWAY ([not set]), RTA_PRIORITY ({})", route.metric);
261 + utils::InitializeIntegerAttribute(message.metric, route.metric, RTA_PRIORITY);
262 + });
263 +}
264 +
265 template <typename TAddr>
266 void RoutingTable::ModifyDefaultRouteImpl(const Route& route, int operation, int flags)
267 {
268 if (!route.via.has_value())
269 {
238 - throw RuntimeErrorWithSourceLocation("Default route is missing its gateway address");
270 + throw RuntimeErrorWithSourceLocation("Default route is missing its next hop");
271 + }
272 + if (route.to.has_value())
273 + {
274 + throw RuntimeErrorWithSourceLocation("Default route has unexpected destination address");
275 }
276
277 struct Message : RouteMessage
@@ -246,15 +282,15 @@ void RoutingTable::ModifyDefaultRouteImpl(const Route& route, int operation, int
282
283 GNS_LOG_INFO(
284 "SendMessage Route (to {}, via {}), operation ({}), netLinkflags ({})",
249 - route.to.has_value() ? route.to.value().Addr().c_str() : "[empty]",
250 - route.via.has_value() ? route.via.value().Addr().c_str() : "[empty]",
285 + "[empty]",
286 + route.via.value().Addr().c_str(),
287 RouteOperationToString(operation),
288 NetLinkFormatFlagsToString(flags).c_str());
289
290 SendMessage<Message>(route, operation, flags, [&](Message& message) {
291 GNS_LOG_INFO(
256 - "InitializeAddressAttribute RTA_DST ([not set]) RTA_GATEWAY ({}), RTA_PRIORITY ({})",
257 - route.to.has_value() ? route.to.value().Addr().c_str() : "[empty]",
292 + "Netlink message configuration: RTA_DST ([not set]) RTA_GATEWAY ({}), RTA_PRIORITY ({})",
293 + route.via.value().Addr().c_str(),
294 route.metric);
295 utils::InitializeAddressAttribute<TAddr>(message.via, route.via.value(), RTA_GATEWAY);
296 utils::InitializeIntegerAttribute(message.metric, route.metric, RTA_PRIORITY);
@@ -264,6 +300,15 @@ void RoutingTable::ModifyDefaultRouteImpl(const Route& route, int operation, int
300 template <typename TAddr>
301 void RoutingTable::ModifyLinkLocalRouteImpl(const Route& route, int operation, int flags)
302 {
303 + if (!route.to.has_value())
304 + {
305 + throw RuntimeErrorWithSourceLocation("Link-local route is missing its destination address");
306 + }
307 + if (route.via.has_value())
308 + {
309 + throw RuntimeErrorWithSourceLocation("Link-local route has unexpected next hop");
310 + }
311 +
312 struct Message : RouteMessage
313 {
314 utils::AddressAttribute<TAddr> to;
@@ -272,15 +317,15 @@ void RoutingTable::ModifyLinkLocalRouteImpl(const Route& route, int operation, i
317
318 GNS_LOG_INFO(
319 "SendMessage Route (to {}, via {}), operation ({}), netLinkflags ({})",
275 - route.to.has_value() ? route.to.value().Addr().c_str() : "[empty]",
276 - route.via.has_value() ? route.via.value().Addr().c_str() : "[empty]",
320 + route.to.value().Addr().c_str(),
321 + "[empty]",
322 RouteOperationToString(operation),
323 NetLinkFormatFlagsToString(flags).c_str());
324
325 SendMessage<Message>(route, operation, flags, [&](Message& message) {
326 GNS_LOG_INFO(
282 - "InitializeAddressAttribute RTA_DST ({}) RTA_GATEWAY ([not set]), RTA_PRIORITY ({})",
283 - route.to.has_value() ? route.to.value().Addr().c_str() : "[empty]",
327 + "Netlink message configuration: RTA_DST ({}) RTA_GATEWAY ([not set]), RTA_PRIORITY ({})",
328 + route.to.value().Addr().c_str(),
329 route.metric);
330 utils::InitializeAddressAttribute<TAddr>(message.to, route.to.value(), RTA_DST);
331 utils::InitializeIntegerAttribute(message.metric, route.metric, RTA_PRIORITY);
@@ -294,6 +339,10 @@ void RoutingTable::ModifyOfflinkRouteImpl(const Route& route, int operation, int
339 {
340 throw RuntimeErrorWithSourceLocation("Offlink route is missing its next hop");
341 }
342 + if (!route.to.has_value())
343 + {
344 + throw RuntimeErrorWithSourceLocation("Offlink route is missing its destination address");
345 + }
346
347 struct Message : RouteMessage
348 {
@@ -304,16 +353,16 @@ void RoutingTable::ModifyOfflinkRouteImpl(const Route& route, int operation, int
353
354 GNS_LOG_INFO(
355 "SendMessage Route (to {}, via {}), operation ({}), netLinkflags ({})",
307 - route.to.has_value() ? route.to.value().Addr().c_str() : "[empty]",
308 - route.via.has_value() ? route.via.value().Addr().c_str() : "[empty]",
356 + route.to.value().Addr().c_str(),
357 + route.via.value().Addr().c_str(),
358 RouteOperationToString(operation),
359 NetLinkFormatFlagsToString(flags).c_str());
360
361 SendMessage<Message>(route, operation, flags, [&](Message& message) {
362 GNS_LOG_INFO(
314 - "InitializeAddressAttribute RTA_DST ({}) RTA_GATEWAY ({}), RTA_PRIORITY ({})",
315 - route.to.has_value() ? route.to.value().Addr().c_str() : "[empty]",
316 - route.via.has_value() ? route.via.value().Addr().c_str() : "[empty]",
363 + "Netlink message configuration: RTA_DST ({}) RTA_GATEWAY ({}), RTA_PRIORITY ({})",
364 + route.to.value().Addr().c_str(),
365 + route.via.value().Addr().c_str(),
366 route.metric);
367 utils::InitializeAddressAttribute<TAddr>(message.to, route.to.value(), RTA_DST);
368 utils::InitializeAddressAttribute<TAddr>(message.via, route.via.value(), RTA_GATEWAY);
src/linux/netlinkutil/RoutingTable.h
+3
@@ -51,6 +51,9 @@ private:
51 template <typename TAddr>
52 void ModifyDefaultRouteImpl(const Route& route, int operation, int flags);
53
54 + template <typename TAddr>
55 + void ModifyDefaultLinkLocalRouteImpl(const Route& route, int operation, int flags);
56 +
57 template <typename TAddr>
58 void ModifyLinkLocalRouteImpl(const Route& route, int operation, int flags);
59
src/windows/service/exe/WslMirroredNetworking.cpp
+9
@@ -347,6 +347,15 @@ void wsl::core::networking::WslMirroredNetworkManager::ProcessRouteChange()
347 {
348 endpointRoute.Metric = UINT16_MAX;
349 }
350 +
351 + // Some Windows interfaces (like VPNs) can have metric 0 and routes over that interface with metric also 0, adding
352 + // up to 0. Linux treats metric 0 as unspecified and will default to a 1024 metric. The highest priority metric in
353 + // Linux is 1 instead so we need to switch the metric from 0 to 1.
354 + if (endpointRoute.Metric == 0)
355 + {
356 + endpointRoute.Metric = 1;
357 + }
358 +
359 endpoint.Network->Routes.insert(endpointRoute);
360 }
361 }
test/windows/NetworkTests.cpp
+35
@@ -345,6 +345,41 @@ class NetworkTests
345 VERIFY_ARE_EQUAL(v6State.DefaultRoute->Device, L"eth0");
346 }
347
348 + WSL2_TEST_METHOD(AddRemoveDefaultOnlinkRoutes)
349 + {
350 + wsl::shared::hns::Route defaultRouteV4;
351 + defaultRouteV4.NextHop = L"0.0.0.0";
352 + defaultRouteV4.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX;
353 + defaultRouteV4.Family = AF_INET;
354 + defaultRouteV4.Metric = 1;
355 + SendDeviceSettingsRequest(L"eth0", defaultRouteV4, ModifyRequestType::Add, GuestEndpointResourceType::Route);
356 +
357 + wsl::shared::hns::Route defaultRouteV6;
358 + defaultRouteV6.NextHop = L"::";
359 + defaultRouteV6.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_V6_PREFIX;
360 + defaultRouteV6.Family = AF_INET6;
361 + defaultRouteV6.Metric = 1;
362 + SendDeviceSettingsRequest(L"eth0", defaultRouteV6, ModifyRequestType::Add, GuestEndpointResourceType::Route);
363 +
364 + const bool defaultV4RouteExists =
365 + LxsstuLaunchWsl(L"ip -4 route show | grep \"default dev eth0\" | grep -w \"metric 1\"") == (DWORD)0;
366 + const bool defaultV6RouteExists =
367 + LxsstuLaunchWsl(L"ip -6 route show | grep \"default dev eth0\" | grep -w \"metric 1\"") == (DWORD)0;
368 +
369 + SendDeviceSettingsRequest(L"eth0", defaultRouteV4, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
370 + SendDeviceSettingsRequest(L"eth0", defaultRouteV6, ModifyRequestType::Remove, GuestEndpointResourceType::Route);
371 +
372 + const bool defaultV4RouteRemoved =
373 + LxsstuLaunchWsl(L"ip -4 route show | grep \"default dev eth0\" | grep -w \"metric 1\"") != (DWORD)0;
374 + const bool defaultV6RouteRemoved =
375 + LxsstuLaunchWsl(L"ip -6 route show | grep \"default dev eth0\" | grep -w \"metric 1\"") != (DWORD)0;
376 +
377 + VERIFY_IS_TRUE(defaultV4RouteExists);
378 + VERIFY_IS_TRUE(defaultV6RouteExists);
379 + VERIFY_IS_TRUE(defaultV4RouteRemoved);
380 + VERIFY_IS_TRUE(defaultV6RouteRemoved);
381 + }
382 +
383 WSL2_TEST_METHOD(SetInterfaceDownAndUp)
384 {
385 // Disconnect interface