Skip to content

client: pin outside socket egress on Windows/Linux via IP_UNICAST_IF - #527

Draft
kp-antonio-yang wants to merge 3 commits into
mainfrom
windows-wifi-roam
Draft

client: pin outside socket egress on Windows/Linux via IP_UNICAST_IF#527
kp-antonio-yang wants to merge 3 commits into
mainfrom
windows-wifi-roam

Conversation

@kp-antonio-yang

@kp-antonio-yang kp-antonio-yang commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Description

Pin the Windows outside socket to the physical egress interface using IP_UNICAST_IF/IPV6_UNICAST_IF so that routing-table changes (including the tunnel's own default route) cannot redirect outside traffic back into the tunnel. Re-pin on every network-change event and swallow transient send errors that occur mid-roam. The encapsulation loop from outside packet back to tunnel cause issues in CVPN-2746, CVPN-2747 and CVPN-2748.

This also apply on Linux, such that the roaming in NoExec mode from wifi in different subnet works. But this is not perfect for Linux. Because IP_UNICAST_IF is evaluated on every send call on Windows (Winsock), and it overrides routing table decisions per-packet, regardless of whether the socket has previously called connect(). However, IP_UNICAST_IF sets sk->sk_bound_dev_if, only consulted during a route lookup on Linux. For a connected UDP socket, the route is cached in sk_dst_cache at connect() time, re-calling setsockopt(IP_UNICAST_IF, same_index) updates the flag but does not flush the route cache. #492 still needed.

Change details

  • platform/windows/egress.rs (new): wraps GetBestInterfaceEx + setsockopt(IP_UNICAST_IF/IPV6_UNICAST_IF) into three helpers — best_interface_index, set_unicast_if, and pin_to_peer_interface.
  • io/outside/udp.rs: pins the UDP socket at creation; stores the current pinned_if_index (AtomicU32) and implements OutsideIO::pin_egress_interface to re-pin on network change; swallows transient WSAEADDRNOTAVAIL/WSAEHOSTUNREACH send errors that arise mid-roam before the next send re-selects a source address.
  • io/outside/tcp.rs: pins the TCP socket before connect() (the option has no effect afterwards, so this is the only opportunity).
  • io/outside.rs: adds OutsideIO::pin_egress_interface (Windows only, default no-op).
  • lib.rs: calls pin_egress_interface from network_event_coordinator after each successful route refresh; extends the outside_io weak reference from Apple-only to any(apple, windows).
  • route_manager.rs: adds RouteUpdater::server_route_if_index() to expose the interface index of the currently installed server route.

Motivation and Context

On Windows without pinning, once the tunnel's default route is installed the OS may route outside packets back into the tunnel, causing a routing loop. Pinning the socket to the physical interface that currently reaches the server makes egress selection independent of the routing table. The pin is refreshed after each network-change event so that switching adapters (Wi-Fi → Ethernet, docking) is handled correctly.

How Has This Been Tested?

  • Follow current CI testcases
  • Manual Windows Wi-Fi roam test

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactor (internal restructuring, no behaviour change for end users)
  • CI / infrastructure change (no production code affected)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • The correct base branch is being used, if not main
  • Any update to xenon/libxenon-src is accompanied by a reference to the specific commit in the git changelog

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Code coverage summary for 0937705:

Filename                                                          Regions    Missed Regions     Cover   Functions  Missed Functions  Executed       Lines      Missed Lines     Cover    Branches   Missed Branches     Cover
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
lightway-app-utils/src/args/cipher.rs                                   5                 5     0.00%           1                 1     0.00%           5                 5     0.00%           0                 0         -
lightway-app-utils/src/args/connection_type.rs                         11                 5    54.55%           3                 1    66.67%          11                 5    54.55%           0                 0         -
lightway-app-utils/src/args/duration.rs                               121                22    81.82%          11                 5    54.55%          64                15    76.56%           0                 0         -
lightway-app-utils/src/args/ip_map.rs                                  27                27     0.00%           5                 5     0.00%          19                19     0.00%           0                 0         -
lightway-app-utils/src/args/keyshare.rs                                 5                 5     0.00%           1                 1     0.00%           5                 5     0.00%           0                 0         -
lightway-app-utils/src/args/logging.rs                                 37                37     0.00%           3                 3     0.00%          31                31     0.00%           0                 0         -
lightway-app-utils/src/args/nonzero_duration.rs                       128                24    81.25%          10                 4    60.00%          68                16    76.47%           0                 0         -
lightway-app-utils/src/cmsg.rs                                        238                63    73.53%          20                 5    75.00%         188                49    73.94%           0                 0         -
lightway-app-utils/src/connection_ticker.rs                           229                17    92.58%          28                 4    85.71%         126                15    88.10%           0                 0         -
lightway-app-utils/src/dplpmtud_timer.rs                              209                13    93.78%          22                 4    81.82%         117                11    90.60%           0                 0         -
lightway-app-utils/src/event_stream.rs                                 19                 0   100.00%           3                 0   100.00%          11                 0   100.00%           0                 0         -
lightway-app-utils/src/metrics.rs                                       4                 4     0.00%           2                 2     0.00%           4                 4     0.00%           0                 0         -
lightway-app-utils/src/network_change_monitor.rs                      348                87    75.00%          29                 8    72.41%         200                75    62.50%           0                 0         -
lightway-app-utils/src/packet_codec.rs                                  2                 2     0.00%           1                 1     0.00%           1                 1     0.00%           0                 0         -
lightway-app-utils/src/sockopt/ip_mtu_discover.rs                     109                17    84.40%           8                 0   100.00%          80                17    78.75%           0                 0         -
lightway-app-utils/src/sockopt/ip_pktinfo.rs                           14                 1    92.86%           1                 0   100.00%          16                 1    93.75%           0                 0         -
lightway-app-utils/src/tun.rs                                         478               478     0.00%          48                48     0.00%         306               306     0.00%           0                 0         -
lightway-app-utils/src/utils.rs                                        21                21     0.00%           1                 1     0.00%          11                11     0.00%           0                 0         -
lightway-boring/src/aes256.rs                                         224                 5    97.77%          13                 0   100.00%          97                 0   100.00%           0                 0         -
lightway-boring/src/cert.rs                                            15                 1    93.33%           1                 0   100.00%           7                 0   100.00%           0                 0         -
lightway-boring/src/chacha20_poly1305.rs                              232                 6    97.41%          16                 3    81.25%         107                 3    97.20%           0                 0         -
lightway-boring/src/config.rs                                         111                17    84.68%          16                 4    75.00%          82                14    82.93%           0                 0         -
lightway-boring/src/context.rs                                        391                89    77.24%          31                 6    80.65%         247                35    85.83%           0                 0         -
lightway-boring/src/error.rs                                           37                 8    78.38%           4                 2    50.00%          28                 6    78.57%           0                 0         -
lightway-boring/src/session.rs                                        821               167    79.66%          53                 8    84.91%         448               101    77.46%           0                 0         -
lightway-boring/src/test_utils.rs                                     329                35    89.36%          25                 5    80.00%         210                24    88.57%           0                 0         -
lightway-boring/src/types.rs                                          137                10    92.70%          16                 1    93.75%          92                 9    90.22%           0                 0         -
lightway-client/src/config.rs                                         471               105    77.71%          24                 8    66.67%         313                67    78.59%           0                 0         -
lightway-client/src/dns_manager.rs                                     18                18     0.00%           4                 4     0.00%          16                16     0.00%           0                 0         -
lightway-client/src/io/inside.rs                                        3                 3     0.00%           1                 1     0.00%           3                 3     0.00%           0                 0         -
lightway-client/src/io/inside/tun.rs                                   86                86     0.00%          13                13     0.00%          63                63     0.00%           0                 0         -
lightway-client/src/io/outside.rs                                      16                16     0.00%           3                 3     0.00%          13                13     0.00%           0                 0         -
lightway-client/src/io/outside/tcp.rs                                 143               143     0.00%          15                15     0.00%          97                97     0.00%           0                 0         -
lightway-client/src/io/outside/udp.rs                                 267               267     0.00%          23                23     0.00%         188               188     0.00%           0                 0         -
lightway-client/src/io/outside/udp/batch_receive.rs                    99                13    86.87%          11                 1    90.91%          84                13    84.52%           0                 0         -
lightway-client/src/keepalive.rs                                      687                45    93.45%          59                 6    89.83%         357                24    93.28%           0                 0         -
lightway-client/src/lib.rs                                           1475               899    39.05%         114                65    42.98%        1060               714    32.64%           0                 0         -
lightway-client/src/main.rs                                           277               277     0.00%          17                17     0.00%         157               157     0.00%           0                 0         -
lightway-client/src/platform/linux/dns_manager.rs                     118                91    22.88%          14                 9    35.71%          87                62    28.74%           0                 0         -
lightway-client/src/platform/linux/dns_manager/direct_file.rs         106                42    60.38%          12                 6    50.00%          65                29    55.38%           0                 0         -
lightway-client/src/platform/linux/dns_manager/resolvconf.rs           61                61     0.00%           8                 8     0.00%          52                52     0.00%           0                 0         -
lightway-client/src/platform/linux/dns_manager/resolvectl.rs           41                41     0.00%           5                 5     0.00%          34                34     0.00%           0                 0         -
lightway-client/src/platform/linux/egress.rs                          179               179     0.00%           7                 7     0.00%         132               132     0.00%           0                 0         -
lightway-client/src/route_manager.rs                                 1169               150    87.17%          81                 6    92.59%         644                73    88.66%           0                 0         -
lightway-core/src/borrowed_bytesmut.rs                                373                 0   100.00%          24                 0   100.00%         185                 0   100.00%           0                 0         -
lightway-core/src/builder_predicates.rs                                24                12    50.00%           4                 2    50.00%          24                12    50.00%           0                 0         -
lightway-core/src/cipher.rs                                           203                21    89.66%          10                 0   100.00%         101                 4    96.04%           0                 0         -
lightway-core/src/connection.rs                                      2120               829    60.90%          95                22    76.84%        1519               566    62.74%           0                 0         -
lightway-core/src/connection/builders.rs                              252                46    81.75%          22                 8    63.64%         260                53    79.62%           0                 0         -
lightway-core/src/connection/dplpmtud.rs                             1741                81    95.35%          63                 0   100.00%         830                 7    99.16%           0                 0         -
lightway-core/src/connection/expresslane.rs                            88                 0   100.00%          10                 0   100.00%          74                 0   100.00%           0                 0         -
lightway-core/src/connection/fragment_map.rs                          366                 6    98.36%          25                 0   100.00%         254                 3    98.82%           0                 0         -
lightway-core/src/connection/io_adapter.rs                            882               154    82.54%          56                 9    83.93%         470                98    79.15%           0                 0         -
lightway-core/src/connection/key_update.rs                             34                13    61.76%           5                 0   100.00%          38                19    50.00%           0                 0         -
lightway-core/src/context.rs                                          219                42    80.82%          29                 8    72.41%         243                46    81.07%           0                 0         -
lightway-core/src/context/ip_pool.rs                                    8                 3    62.50%           1                 0   100.00%           5                 0   100.00%           0                 0         -
lightway-core/src/context/server_auth.rs                               37                37     0.00%           4                 4     0.00%          26                26     0.00%           0                 0         -
lightway-core/src/encoding_request_states.rs                            3                 0   100.00%           1                 0   100.00%           3                 0   100.00%           0                 0         -
lightway-core/src/gso.rs                                             1104                88    92.03%          41                 3    92.68%         519                55    89.40%           0                 0         -
lightway-core/src/io.rs                                                10                10     0.00%           3                 3     0.00%           9                 9     0.00%           0                 0         -
lightway-core/src/keyshare.rs                                           5                 0   100.00%           1                 0   100.00%           5                 0   100.00%           0                 0         -
lightway-core/src/lib.rs                                                9                 0   100.00%           3                 0   100.00%           9                 0   100.00%           0                 0         -
lightway-core/src/metrics.rs                                           81                66    18.52%          33                27    18.18%          73                58    20.55%           0                 0         -
lightway-core/src/packet.rs                                            38                10    73.68%           4                 1    75.00%          30                 6    80.00%           0                 0         -
lightway-core/src/plugin.rs                                           307                17    94.46%          22                 4    81.82%         148                10    93.24%           0                 0         -
lightway-core/src/tls/mod.rs                                            4                 4     0.00%           1                 1     0.00%           3                 3     0.00%           0                 0         -
lightway-core/src/utils.rs                                            341                28    91.79%          24                 3    87.50%         170                18    89.41%           0                 0         -
lightway-core/src/version.rs                                           93                 0   100.00%          17                 0   100.00%          82                 0   100.00%           0                 0         -
lightway-core/src/wire.rs                                             438                39    91.10%          29                 0   100.00%         235                 9    96.17%           0                 0         -
lightway-core/src/wire/auth_failure.rs                                 27                 1    96.30%           3                 0   100.00%          17                 0   100.00%           0                 0         -
lightway-core/src/wire/auth_request.rs                                684                57    91.67%          34                 1    97.06%         351                24    93.16%           0                 0         -
lightway-core/src/wire/auth_success_with_config_ipv4.rs               222                 3    98.65%          11                 0   100.00%         124                 0   100.00%           0                 0         -
lightway-core/src/wire/data.rs                                         51                 1    98.04%           5                 0   100.00%          33                 0   100.00%           0                 0         -
lightway-core/src/wire/data_frag.rs                                   127                 1    99.21%          14                 0   100.00%          80                 0   100.00%           0                 0         -
lightway-core/src/wire/encoding_request.rs                             82                 2    97.56%           6                 0   100.00%          42                 1    97.62%           0                 0         -
lightway-core/src/wire/encoding_response.rs                            82                 2    97.56%           6                 0   100.00%          42                 1    97.62%           0                 0         -
lightway-core/src/wire/expresslane_config.rs                          188                 1    99.47%          10                 0   100.00%         113                 0   100.00%           0                 0         -
lightway-core/src/wire/expresslane_data.rs                             48                21    56.25%           5                 2    60.00%          52                23    55.77%           0                 0         -
lightway-core/src/wire/ping.rs                                         95                 2    97.89%           7                 0   100.00%          59                 0   100.00%           0                 0         -
lightway-core/src/wire/pong.rs                                        109                 2    98.17%           8                 0   100.00%          72                 0   100.00%           0                 0         -
lightway-core/src/wire/server_config.rs                                69                 2    97.10%           4                 0   100.00%          37                 0   100.00%           0                 0         -
lightway-expresslane/src/aead.rs                                       29                29     0.00%           2                 2     0.00%          23                23     0.00%           0                 0         -
lightway-expresslane/src/aead/pool.rs                                 127                71    44.09%           9                 4    55.56%          90                50    44.44%           0                 0         -
lightway-expresslane/src/key.rs                                        24                 0   100.00%           5                 0   100.00%          17                 0   100.00%           0                 0         -
lightway-expresslane/src/replay_window.rs                             269                 4    98.51%          20                 0   100.00%         174                 4    97.70%           0                 0         -
lightway-expresslane/src/session.rs                                   469               195    58.42%          19                 2    89.47%         283               114    59.72%           0                 0         -
lightway-expresslane/src/version.rs                                    32                 0   100.00%           4                 0   100.00%          25                 0   100.00%           0                 0         -
lightway-server/src/auth.rs                                           273                49    82.05%          22                 6    72.73%         171                28    83.63%           0                 0         -
lightway-server/src/config.rs                                         159                 0   100.00%          11                 0   100.00%         130                 0   100.00%           0                 0         -
lightway-server/src/connection.rs                                     137               137     0.00%          11                11     0.00%         110               110     0.00%           0                 0         -
lightway-server/src/connection_manager.rs                             346               346     0.00%          42                42     0.00%         303               303     0.00%           0                 0         -
lightway-server/src/connection_manager/connection_map.rs              407                16    96.07%          21                 1    95.24%         228                 7    96.93%           0                 0         -
lightway-server/src/io/inside.rs                                      100                21    79.00%          23                 9    60.87%          69                19    72.46%           0                 0         -
lightway-server/src/io/inside/tun.rs                                   61                61     0.00%          14                14     0.00%          45                45     0.00%           0                 0         -
lightway-server/src/io/outside/tcp.rs                                  93                93     0.00%          10                10     0.00%          68                68     0.00%           0                 0         -
lightway-server/src/io/outside/udp.rs                                 483               428    11.39%          27                24    11.11%         331               297    10.27%           0                 0         -
lightway-server/src/io/outside/udp/batch_receive.rs                   467                19    95.93%          20                 1    95.00%         266                15    94.36%           0                 0         -
lightway-server/src/io/outside/udp/send_queue.rs                      381                26    93.18%          24                 1    95.83%         209                16    92.34%           0                 0         -
lightway-server/src/ip_manager.rs                                     542                50    90.77%          22                 4    81.82%         242                23    90.50%           0                 0         -
lightway-server/src/ip_manager/ip_pool.rs                             545                 0   100.00%          28                 0   100.00%         257                 0   100.00%           0                 0         -
lightway-server/src/lib.rs                                            606               502    17.16%          34                26    23.53%         414               341    17.63%           0                 0         -
lightway-server/src/main.rs                                           201               201     0.00%          10                10     0.00%          96                96     0.00%           0                 0         -
lightway-server/src/metrics.rs                                        287               265     7.67%          99                89    10.10%         243               223     8.23%           0                 0         -
lightway-server/src/offload_stats.rs                                  125                74    40.80%           8                 3    62.50%          82                42    48.78%           0                 0         -
lightway-server/src/statistics.rs                                     133                59    55.64%           8                 4    50.00%          91                39    57.14%           0                 0         -
uniffi-bindgen/src/uniffi-bindgen.rs                                    3                 3     0.00%           1                 1     0.00%           3                 3     0.00%           0                 0         -
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL                                                               26381              7852    70.24%        1917               681    64.48%       16253              5432    66.58%           0                 0         -

✅ Region coverage 70% passes
✅ Line coverage 66% passes

@kp-antonio-yang
kp-antonio-yang force-pushed the windows-wifi-roam branch 3 times, most recently from 0156339 to c87e096 Compare August 25, 2026 07:34
@kp-antonio-yang
kp-antonio-yang marked this pull request as ready for review August 25, 2026 10:01
@kp-antonio-yang
kp-antonio-yang requested a review from a team as a code owner August 25, 2026 10:01
@kp-antonio-yang kp-antonio-yang changed the title client: pin outside socket egress on Windows via IP_UNICAST_IF client: pin outside socket egress on Windows/Linux via IP_UNICAST_IF Aug 26, 2026
Add platform/windows/egress.rs with GetBestInterfaceEx +
IP_UNICAST_IF/IPV6_UNICAST_IF helpers. Pin the UDP and TCP outside
sockets to the physical interface at creation time so routing-table
changes (including the tunnel's own default route) cannot redirect
outside traffic into the tunnel.

Re-pin the UDP socket after each network-change event via a new
OutsideIO::pin_egress_interface hook called from
network_event_coordinator, using the interface index of the freshly
validated server route.

Swallow transient WSAEADDRNOTAVAIL/WSAEHOSTUNREACH send errors on
UDP after a roam; the socket is unconnected so the next send
re-selects a source address on the pinned interface.
Add disable_pin_egress_interface: bool (default false, i.e. pinning on)
to Config and ClientConfig, gated on Windows desktop builds.

Thread a pin_egress_interface: bool parameter through Udp::new, Tcp::new,
initialize_routes, and network_event_coordinator so the IP_UNICAST_IF /
IPV6_UNICAST_IF pin — both at socket creation and on network-change
re-pins — is skipped when the flag is set.
Add platform/linux/egress.rs mirroring the Windows egress module.
Uses a throw-away SOCK_DGRAM connect + getsockname + getifaddrs to
find the best interface index, then applies IP_UNICAST_IF /
IPV6_UNICAST_IF via setsockopt.

Extend the UDP/TCP socket constructors and OutsideIO::pin_egress_interface
trait method to cover both Linux and Windows (not(feature = "mobile")).
Expand server_route_if_index and the network_event_coordinator re-pin
logic to any(linux, windows).
Comment thread lightway-client/src/io/outside/tcp.rs Outdated

// the route lookup that binds the local address happens there,
// and IP_UNICAST_IF has no effect on an already-connected socket.
#[cfg(all(windows, not(feature = "mobile")))]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to have !mobile ?
Is not windows only cover desktop clients ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If windows developer cross compiling the android, it might be a issue. The mobile is not a platform gate, we need use !mobile to ensure this will not go into mobile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per rust documentation, https://doc.rust-lang.org/reference/conditional-compilation.html#r-cfg.target_os
target_os should be set to Android, if some dev compiles Android from Windows host os.

Are you seeing different behavior ?

use std::os::fd::RawFd;

/// Constrain `socket`'s egress to interface `if_index`.
pub fn set_unicast_if(fd: RawFd, if_index: u32, ipv6: bool) -> io::Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sockopt, follow the existing apputils module, which has absrtactions for different os'es
https://github.com/expressvpn/lightway/blob/main/lightway-app-utils/src/sockopt/ip_mtu_discover.rs

maybe_sock: Option<TcpStream>,
#[cfg(all(linux, not(feature = "mobile")))] fwmark: u32,
#[cfg(all(windows, not(feature = "mobile")))] pin_egress_interface: bool,
#[cfg(all(any(linux, windows), not(feature = "mobile")))] pin_egress_interface: bool,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i understood why you added !mobile with windows, to avoid android ??

I suggest to create more cfg in

linux: { target_os = "linux" },
like linux_desktop here and use it.

It will be clear and avoid mistakes.


// The route lookup that binds the local address happens at
// connect() and IP_UNICAST_IF has no effect on an already-connected
// socket, so pin must be applied before connect.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From windows documentation, i do not see the option should be set before connect
https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options

Can you add the reference to this claim ?
If this is not true, we can ignore setting this now, our route manager will set this when first default route is added.

};

/// The interface index Windows would currently use to reach `dst`.
pub fn best_interface_index(dst: IpAddr) -> io::Result<u32> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are trying to replicate the code from routw manager here
https://github.com/expressvpn/lightway/blob/main/lightway-client/src/route_manager.rs#L248

In worst case, both implementation deviates and server route is set with 1 interface and the socket is pinned to different interface and it might break the connection.

Better to use same implementation.
Also if we confirm we can set UNICAST_IP after connection, we can get rid of this flow and pin egress only from route_manager.

@kp-antonio-yang
kp-antonio-yang marked this pull request as draft September 1, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants