Skip to content

Windows virtual adapters and macOS VPN tunnels are advertised as LAN routes #17246

Description

@astarktc

What happened

A remote environment's route list fills with "LAN · found automatically" addresses that only exist on the server host itself:

  • Windows host: http://192.168.56.1:3773/ (VirtualBox Host-Only adapter) and http://192.168.137.1:3773/ (the Wi-Fi Direct virtual adapter Windows uses for Mobile hotspot / Internet Connection Sharing), alongside the real Wi-Fi address.
  • macOS host: http://198.19.2.2:3773/, the address of a utun point-to-point interface created by a third-party VPN/security client (not Tailscale).

No client can reach these addresses, but they are saved as routes, and two of them rank above the route actually in use.

Diagnosis

resolveBoundEndpoints in apps/server/src/environment/DirectEndpoints.ts advertises every private IPv4 address on a wildcard-bound server. It skips container and VM networks only by interface-name prefix:

const VIRTUAL_INTERFACE =
  /^(docker|br-|veth|virbr|vmnet|vboxnet|vEthernet|podman|cni|flannel|cali|lxcbr|lxdbr|bridge1\d\d)/;

Two gaps:

  1. Windows interface names never match these prefixes. On Windows, the keys of os.networkInterfaces() are connection names, not driver names. VirtualBox's adapter is VirtualBox Host-Only Network (vboxnet is the Linux/macOS name), VMware's are VMware Network Adapter VMnet1/8, and the hotspot adapter is Local Area Connection* 2. Only Hyper-V/WSL (vEthernet) is caught.
  2. 198.18.0.0/15 counts as a LAN. isPrivateNetworkHost (packages/shared/src/hostClassification.ts) treats 198.18.0.0/15 as private, so isAdvertisableAddress accepts it. That RFC 2544 benchmarking range is not used for real LANs, but VPN, proxy and security clients use it to address their tunnel interfaces. On macOS those interfaces are named utunN, and a blanket utun filter would also drop Tailscale, so the address range is the better signal.

Impact is limited. Before connecting, ConnectionDriver.checkRoute fetches the unauthenticated descriptor and requires a matching environmentId, so a stray address on another network answers "silent" rather than receiving the credential. But each unreachable route still costs a probe on every connect attempt, and with a 60 s BETTER_ROUTE_CHECK_INTERVAL, every minute while a lower-ranked route is in use. The list also shows routes the user cannot act on.

Steps to reproduce

  1. On Windows, install VirtualBox (which creates the Host-Only adapter), or turn on Settings → Network & Internet → Mobile hotspot.
  2. Run T3 Code with Network access enabled, so the server binds a wildcard host.
  3. Pair it from another client, then open Settings → Connections on that client and expand the environment's routes.
  4. The routes include 192.168.56.1 and/or 192.168.137.1 marked "LAN · found automatically".

For the macOS case: run the server on a Mac with any client that creates a utun interface in 198.18.0.0/15 (ifconfig | grep -A1 utun).

Version

Main at 9a3070bcf0 (0.0.45). DirectEndpoints.ts and hostClassification.ts are identical to main.

Environment

  • Server A: Windows 10 Pro 22H2 (10.0.19045), VirtualBox 6.1.34, Mobile hotspot on.
  • Server B: macOS 26.7.1 (arm64), with a third-party network client's utun at 198.19.2.2.
  • Client: T3 Code desktop on macOS.

Evidence

Server A, Get-NetIPAddress joined with Get-NetAdapter (link-local and disconnected adapters omitted):

192.168.56.1     VirtualBox Host-Only Network   VirtualBox Host-Only Ethernet Adapter     Up
192.168.137.1    Local Area Connection* 2       Microsoft Wi-Fi Direct Virtual Adapter #2 Up
100.80.x.x       Tailscale                      Tailscale Tunnel                          Up
192.168.50.x     Wi-Fi                          Killer(R) Wi-Fi 6 AX1650w ...             Up

Route list on the client: 192.168.56.1, 192.168.50.x, 192.168.137.1 (all "found automatically"), then the user-added route "In use", then Tailscale.

Server B:

utun40: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 16384
	inet 198.19.2.2 --> 198.19.2.1 netmask 0xffffff00

The client lists http://198.19.2.2:3773/ as "LAN · found automatically" next to the host's two real LAN addresses.

Related issues

Fix applied or workaround

None applied. Possible fixes:

  • Extend VIRTUAL_INTERFACE with the Windows connection names: VirtualBox Host-Only Network, VMware Network Adapter, and Local Area Connection\* \d+. The asterisked form is the name Windows gives Wi-Fi Direct/hosted-network virtual adapters; real Ethernet connections are named Ethernet on Windows 10+.
  • Exclude 198.18.0.0/15 in isAdvertisableAddress, while keeping it private for isPrivateNetworkHost's other callers.

Workaround: disable the adapters on the server (turn off Mobile hotspot; disable the VirtualBox Host-Only adapter when no VM needs it).

Filed by

@astarktc, with an AI agent (Pi in T3 Code), after inspecting the adapters on both hosts and reading the route code on main.

Activity

  1. juliusmarminge commented on Oct 8, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the detailed report. I checked it against main at a6ec88f7a, and both gaps are real.

    1. Windows adapter names slip past the filter

    • VIRTUAL_INTERFACE (apps/server/src/environment/DirectEndpoints.ts:66-67) is a case-sensitive prefix regex. resolveBoundEndpoints drops a whole interface only when that regex matches its name (DirectEndpoints.ts:82).
    • I ran the regex on the names from your evidence. VirtualBox Host-Only Network, VMware Network Adapter VMnet1/VMnet8, Local Area Connection* 2 and Local Area Connection* 10 all pass through. Only vEthernet (WSL), vboxnet0 and vmnet8 get filtered. The tests only use Linux/macOS names plus vEthernet (WSL) (DirectEndpoints.test.ts:97-112).
    • One caution on the proposed fix: as far as I know, Windows connection names can be renamed by the user and are localized on non-English installs. If so, a list of English prefixes would only catch some cases. The adapter's MAC vendor prefix (from os.networkInterfaces()) might be a sturdier signal, but I haven't verified that.
    • 192.168.137.1 does work for devices joined to that PC's hotspot, and the host-only address works from VMs on that host. Filtering them gives up those (rare) clients.

    2. 198.18.0.0/15 counts as LAN

    • isPrivateIpv4Address includes 198.18/15 (packages/shared/src/hostClassification.ts:64). isPrivateNetworkHost uses it (:111-112), and isAdvertisableAddress accepts anything isPrivateNetworkHost accepts (DirectEndpoints.ts:54-58).
    • I don't see any route-specific reason for that range to be there. It came in with fix: stop favicon requests for private link hosts on web and mobile #5838 as part of the favicon privacy policy, where being too inclusive is the safe direction. feat(clients): learn an environment's LAN and tailnet addresses #15468 later reused isPrivateNetworkHost for advertising, and no route test or comment mentions 198.18.
    • isPrivateNetworkHost also feeds isPublicFaviconHost, the client's route-kind label (packages/client-runtime/src/connection/routes.ts:100) and the web browser target resolver. So excluding the range only in isAdvertisableAddress, as you suggest, seems like the right scope.

    What a stray route costs (packages/client-runtime/src/connection/)

    • On every connect: each saved route gets an unauthenticated descriptor check, all in parallel, with a 2.5 s timeout (driver.ts:42, :91-93, :144-154). A dead route ranked above the working one can delay connecting by up to about 2.5 s in total, not 2.5 s per route.
    • While connected on a lower-ranked route: every 60 s (supervisor.ts:46, :605), each route ranked above the one in use is preflighted (supervisor.ts:357-361).
    • The cooldown doesn't help here: the 5 min cooldown (supervisor.ts:47-49, :749) only applies to a route that answered and then failed to connect. A silent route gets checked again every tick.
    • Learned LAN routes are inserted ahead of tailnet and anything slower (routes.ts:105-125), which would explain why they ended up above your route in use.

    Workaround today: there's no way to hide or disable a learned route. The web and mobile route lists don't allow removing one, likely because it would just be learned again (apps/web/src/components/settings/EnvironmentRoutesList.tsx:124, apps/mobile/src/features/settings/EnvironmentRoutesSection.tsx:187). You can drag the stray routes below the route you use. That should stop the 60 s re-checks, but the per-connect check stays. Turning off the adapter on the server, as you mentioned, should remove them completely.

    Related

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 8, 2026
  3. diyaavirmani commented on Oct 8, 2026

    @diyaavirmani

    Hi @juliusmarminge , I’d like to work on this issue. Could you assign it to me if nobody else is already working on it?

    My proposed scope is to exclude 198.18.0.0/15 from direct-endpoint advertising without changing the shared isPrivateNetworkHost behavior, and extend the existing virtual-interface filter for the Windows adapter names reported here. I’ll add focused regression tests that also verify normal LAN and Tailscale routes remain available.

    Before implementing the Windows part, could you confirm whether hotspot and host-only interfaces should be excluded from automatic discovery, given that hotspot clients and local VMs can legitimately reach them? I understand that connection-name filtering also won’t cover every renamed or localized adapter.

    I’ll keep the patch focused on this discovery issue and account for the overlapping changes in #17158.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions