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