[GH-ISSUE #3134] Windows Xray can bind to a disconnected uplink adapter and fail with WSAEHOSTUNREACH #1455

Open
opened 2026-10-13 20:42:46 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @alfateam22 on GitHub (Sep 7, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3134

Describe the bug

On Windows 10, AmneziaVPN 5.0.1.5 can select a disconnected network adapter as the uplink for the embedded Xray core when multiple adapters/gateways are present.

In the reproduced case, the workstation had working Internet over Ethernet (ifIndex 11), while Wi-Fi (ifIndex 8) was disconnected but still present in the adapter/gateway enumeration. Windows' best route to the remote Xray server used Ethernet, but Amnezia's Xray service selected Wi-Fi instead:

[xray] using uplink interface: "wireless_32768" ( 8 )

The embedded Xray then repeatedly failed before any TCP SYN left the workstation:

transport/internet/tcp: dialing TCP to tcp::8443
...
connectex: A socket operation was attempted to an unreachable host.

The Amnezia UI still reached Connected, because the local TUN/tun2socks/Xray stack started successfully, but browser traffic did not pass.

A direct Test-NetConnection to the same remote endpoint/port succeeded outside the tunnel, and a server-side packet capture during the failed Amnezia attempt saw 0 packets on TCP 8443.

To Reproduce

Windows 10 machine with Internet over Ethernet.
Keep a Wi-Fi adapter present but disconnected (not administratively disabled). Multiple gateway-bearing/virtual adapters may also be present.
Import/connect an Xray/VLESS/REALITY profile in AmneziaVPN 5.0.1.5.
Enable debug logging and inspect the service log.
Observe that Amnezia Xray may select the disconnected Wi-Fi interface instead of the actual route to the remote Xray server:
[xray] using uplink interface: "wireless_32768" ( 8 )
Internet access through the Xray profile fails with local Windows connectex ... unreachable host errors.
Administratively disable only the disconnected Wi-Fi adapter.
Reconnect the same Xray profile.
Observe that Amnezia now selects the working Ethernet interface:
[xray] using uplink interface: "ethernet_32772" ( 11 )
The same Xray profile then works end-to-end; normal web browsing succeeds and public egress is confirmed through the remote Xray server.

Expected behavior

For Xray outbound sockets, AmneziaVPN should select the interface corresponding to the Windows best route to the actual remote Xray server endpoint, rather than a generic/last gateway-bearing adapter.

A disconnected adapter should not be used as the forced IP_UNICAST_IF for the Xray outbound socket when Windows would route the destination through a different active interface.

Log files

Sanitized relevant excerpts:

Failed state:

[xray] using uplink interface: "wireless_32768" ( 8 )
transport/internet/tcp: dialing TCP to tcp::8443
connectex: A socket operation was attempted to an unreachable host.

Working state after disabling only Wi-Fi:

[xray] using uplink interface: "ethernet_32772" ( 11 )

No credentials, UUIDs, REALITY keys, short IDs or full live profiles are included here. Additional sanitized diagnostics can be provided if useful.

Screenshots

Not required for reproduction. Adapter/log output is more diagnostic than the UI state in this case.

Desktop (please complete the following information):

OS: Windows 10
AmneziaVPN: 5.0.1.5

Smartphone (please complete the following information):

Not applicable

Server (please complete the following information):

OS: Ubuntu 24.04.4 LTS
Native Xray server, VLESS + REALITY, TCP 8443

Additional context

The behavior appears consistent with the Windows Xray uplink-selection path in:

service/server/xray.cpp
client/core/utils/networkUtilities.cpp

Xray::startXray() obtains an interface through NetworkUtilities::getGatewayAndIface(), and the Windows socket callback later forces outbound sockets to that interface via IP_UNICAST_IF / IPV6_UNICAST_IF.

The Windows getGatewayAndIface() implementation appears to enumerate adapters with gateways and overwrite the selected result as it iterates, rather than selecting by the route to the actual Xray endpoint.

There is already an endpoint-specific route helper (AdapterIndexTo() using Windows GetBestRoute()), but it does not appear to be what determines the Xray socket-binding interface in this path.

A possible fix direction would be to resolve the uplink interface from the best route to the actual Xray server address before applying IP_UNICAST_IF.

A comparison of the directly relevant source files between 5.0.1.5 and 5.0.2.1 did not show a change to this particular adapter-selection/socket-binding path, so upgrading alone does not appear to address this specific case._______

Originally created by @alfateam22 on GitHub (Sep 7, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3134 **Describe the bug** On Windows 10, AmneziaVPN 5.0.1.5 can select a disconnected network adapter as the uplink for the embedded Xray core when multiple adapters/gateways are present. In the reproduced case, the workstation had working Internet over Ethernet (`ifIndex 11`), while Wi-Fi (`ifIndex 8`) was disconnected but still present in the adapter/gateway enumeration. Windows' best route to the remote Xray server used Ethernet, but Amnezia's Xray service selected Wi-Fi instead: ```text [xray] using uplink interface: "wireless_32768" ( 8 ) ``` The embedded Xray then repeatedly failed before any TCP SYN left the workstation: transport/internet/tcp: dialing TCP to tcp:<remote-xray-endpoint>:8443 ... connectex: A socket operation was attempted to an unreachable host. The Amnezia UI still reached Connected, because the local TUN/tun2socks/Xray stack started successfully, but browser traffic did not pass. A direct Test-NetConnection to the same remote endpoint/port succeeded outside the tunnel, and a server-side packet capture during the failed Amnezia attempt saw 0 packets on TCP 8443. To Reproduce Windows 10 machine with Internet over Ethernet. Keep a Wi-Fi adapter present but disconnected (not administratively disabled). Multiple gateway-bearing/virtual adapters may also be present. Import/connect an Xray/VLESS/REALITY profile in AmneziaVPN 5.0.1.5. Enable debug logging and inspect the service log. Observe that Amnezia Xray may select the disconnected Wi-Fi interface instead of the actual route to the remote Xray server: [xray] using uplink interface: "wireless_32768" ( 8 ) Internet access through the Xray profile fails with local Windows connectex ... unreachable host errors. Administratively disable only the disconnected Wi-Fi adapter. Reconnect the same Xray profile. Observe that Amnezia now selects the working Ethernet interface: [xray] using uplink interface: "ethernet_32772" ( 11 ) The same Xray profile then works end-to-end; normal web browsing succeeds and public egress is confirmed through the remote Xray server. Expected behavior For Xray outbound sockets, AmneziaVPN should select the interface corresponding to the Windows best route to the actual remote Xray server endpoint, rather than a generic/last gateway-bearing adapter. A disconnected adapter should not be used as the forced IP_UNICAST_IF for the Xray outbound socket when Windows would route the destination through a different active interface. Log files Sanitized relevant excerpts: Failed state: [xray] using uplink interface: "wireless_32768" ( 8 ) transport/internet/tcp: dialing TCP to tcp:<remote-xray-endpoint>:8443 connectex: A socket operation was attempted to an unreachable host. Working state after disabling only Wi-Fi: [xray] using uplink interface: "ethernet_32772" ( 11 ) No credentials, UUIDs, REALITY keys, short IDs or full live profiles are included here. Additional sanitized diagnostics can be provided if useful. Screenshots Not required for reproduction. Adapter/log output is more diagnostic than the UI state in this case. Desktop (please complete the following information): OS: Windows 10 AmneziaVPN: 5.0.1.5 Smartphone (please complete the following information): Not applicable Server (please complete the following information): OS: Ubuntu 24.04.4 LTS Native Xray server, VLESS + REALITY, TCP 8443 Additional context The behavior appears consistent with the Windows Xray uplink-selection path in: service/server/xray.cpp client/core/utils/networkUtilities.cpp Xray::startXray() obtains an interface through NetworkUtilities::getGatewayAndIface(), and the Windows socket callback later forces outbound sockets to that interface via IP_UNICAST_IF / IPV6_UNICAST_IF. The Windows getGatewayAndIface() implementation appears to enumerate adapters with gateways and overwrite the selected result as it iterates, rather than selecting by the route to the actual Xray endpoint. There is already an endpoint-specific route helper (AdapterIndexTo() using Windows GetBestRoute()), but it does not appear to be what determines the Xray socket-binding interface in this path. A possible fix direction would be to resolve the uplink interface from the best route to the actual Xray server address before applying IP_UNICAST_IF. A comparison of the directly relevant source files between 5.0.1.5 and 5.0.2.1 did not show a change to this particular adapter-selection/socket-binding path, so upgrading alone does not appear to address this specific case._______
Author
Owner

@HeisLuka commented on GitHub (Sep 8, 2026):

I think there is a stronger invariant here than only filtering disconnected adapters.

Current dev appears to have two independent sources of truth for the XRay underlay interface.

The client routing path already does endpoint-specific selection:

const int inetAdapterIndex =
NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress));

AdapterIndexTo() uses Windows GetBestRoute() and returns the interface that Windows would actually use for that destination.

That index is subsequently used by the Windows routing/WFP path through enablePeerTraffic().

But the embedded XRay service independently calls:

NetworkUtilities::getGatewayAndIface()

and stores that result as m_defaultIfaceIdx. sockCallback() then forces XRay sockets onto it using:

IP_UNICAST_IF
IPV6_UNICAST_IF

On Windows, getGatewayAndIface() does not select a route for the XRay endpoint. It enumerates adapters and overwrites resGateway/resIndex whenever it sees another IPv4 gateway.

This creates a possible split-brain:

Windows best route to XRay endpoint = ifIndex 11
enablePeerTraffic path = ifIndex 11

XRay socket IP_UNICAST_IF = ifIndex 8

which matches the reported failure very closely.

It is also enumeration-order dependent. For the same adapter state:

Ethernet ifIndex 11, active, gateway
Wi-Fi ifIndex 8, disconnected, stale gateway

current selection behaves approximately as:

enumeration [Ethernet, Wi-Fi]
-> selected XRay interface = 8

enumeration [Wi-Fi, Ethernet]
-> selected XRay interface = 11

No routing fact changed between those two cases.

I would therefore avoid fixing this only with:

if OperStatus != Up -> skip

That would remove this particular disconnected-adapter case, but the selector would still not express the actual invariant when two valid uplinks exist.

The stronger rule should be:

XRay socket underlay interface

Windows best-route interface to the actual remote XRay endpoint

interface used by routing/WFP setup

There is already code for the destination-specific part, so one possible direction is to make the decision once and reuse it.

Conceptually on Windows:

resolve remote endpoint
↓
AdapterIndexTo(remote)
↓
one immutable/session uplink index
├─> pass to xrayStart(...)
│ └─> IP_UNICAST_IF
│
└─> setupRouting()/enablePeerTraffic(...)

rather than:

setupRouting -> GetBestRoute(remote) -> interface A

xrayStart -> enumerate gateways -> interface B

xrayStart() IPC currently accepts only the config string, so passing the already selected uplink index through that boundary would also make the path identity explicit rather than recomputing it in the service.

Regression cases I would add:

  1. Ethernet active, Wi-Fi disconnected but still has gateway metadata
    best route(remote) = Ethernet
    expected XRay bind = Ethernet

  2. Both Ethernet and Wi-Fi active
    best route(remote) = Wi-Fi
    expected XRay bind = Wi-Fi

  3. Same adapter facts, enumeration order reversed
    expected selected uplink unchanged

  4. Generic default gateway differs from best route to remote
    expected endpoint-specific route wins

And a useful runtime assertion/log would be:

remote=
bestRouteIf=
xrayBoundIf=
routingIf=

with a warning/error if those identities diverge.

This would turn the current symptom from “XRay randomly picked Wi-Fi” into a testable path-binding invariant and should cover multi-homed systems as well as the disconnected-adapter reproduction.

<!-- gh-comment-id:5586238977 --> @HeisLuka commented on GitHub (Sep 8, 2026): I think there is a stronger invariant here than only filtering disconnected adapters. Current dev appears to have two independent sources of truth for the XRay underlay interface. The client routing path already does endpoint-specific selection: const int inetAdapterIndex = NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress)); AdapterIndexTo() uses Windows GetBestRoute() and returns the interface that Windows would actually use for that destination. That index is subsequently used by the Windows routing/WFP path through enablePeerTraffic(). But the embedded XRay service independently calls: NetworkUtilities::getGatewayAndIface() and stores that result as m_defaultIfaceIdx. sockCallback() then forces XRay sockets onto it using: IP_UNICAST_IF IPV6_UNICAST_IF On Windows, getGatewayAndIface() does not select a route for the XRay endpoint. It enumerates adapters and overwrites resGateway/resIndex whenever it sees another IPv4 gateway. This creates a possible split-brain: Windows best route to XRay endpoint = ifIndex 11 enablePeerTraffic path = ifIndex 11 XRay socket IP_UNICAST_IF = ifIndex 8 which matches the reported failure very closely. It is also enumeration-order dependent. For the same adapter state: Ethernet ifIndex 11, active, gateway Wi-Fi ifIndex 8, disconnected, stale gateway current selection behaves approximately as: enumeration [Ethernet, Wi-Fi] -> selected XRay interface = 8 enumeration [Wi-Fi, Ethernet] -> selected XRay interface = 11 No routing fact changed between those two cases. I would therefore avoid fixing this only with: if OperStatus != Up -> skip That would remove this particular disconnected-adapter case, but the selector would still not express the actual invariant when two valid uplinks exist. The stronger rule should be: XRay socket underlay interface == Windows best-route interface to the actual remote XRay endpoint == interface used by routing/WFP setup There is already code for the destination-specific part, so one possible direction is to make the decision once and reuse it. Conceptually on Windows: resolve remote endpoint ↓ AdapterIndexTo(remote) ↓ one immutable/session uplink index ├─> pass to xrayStart(...) │ └─> IP_UNICAST_IF │ └─> setupRouting()/enablePeerTraffic(...) rather than: setupRouting -> GetBestRoute(remote) -> interface A xrayStart -> enumerate gateways -> interface B xrayStart() IPC currently accepts only the config string, so passing the already selected uplink index through that boundary would also make the path identity explicit rather than recomputing it in the service. Regression cases I would add: 1. Ethernet active, Wi-Fi disconnected but still has gateway metadata best route(remote) = Ethernet expected XRay bind = Ethernet 2. Both Ethernet and Wi-Fi active best route(remote) = Wi-Fi expected XRay bind = Wi-Fi 3. Same adapter facts, enumeration order reversed expected selected uplink unchanged 4. Generic default gateway differs from best route to remote expected endpoint-specific route wins And a useful runtime assertion/log would be: remote=<resolved endpoint> bestRouteIf=<N> xrayBoundIf=<N> routingIf=<N> with a warning/error if those identities diverge. This would turn the current symptom from “XRay randomly picked Wi-Fi” into a testable path-binding invariant and should cover multi-homed systems as well as the disconnected-adapter reproduction.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amnezia-client#1455
No description provided.