Skip to content

feat(vpn): prefer IPv4 in nodeAddrs selection for outbound VPN dial#93

Open
Sentinel-Bluebuilder wants to merge 2 commits into
sentinel-official:developmentfrom
Sentinel-Bluebuilder:feat/prefer-ipv4
Open

feat(vpn): prefer IPv4 in nodeAddrs selection for outbound VPN dial#93
Sentinel-Bluebuilder wants to merge 2 commits into
sentinel-official:developmentfrom
Sentinel-Bluebuilder:feat/prefer-ipv4

Conversation

@Sentinel-Bluebuilder

Copy link
Copy Markdown

Summary

Both `Wireguard.parseConfig` and `V2Ray.parseConfig` pick `nodeAddrs[0]` for the outbound endpoint:

  • `src/vpn/wireguard/index.ts:117` — `const host = nodeAddrs[0];`
  • `src/vpn/v2ray/index.ts:172` — `const address = nodeAddrs[0];`

Sentinel chain does not guarantee ordering of `result.addrs` returned by the node handshake. On dual-stack nodes that return IPv6 first, consumers on v4-only networks (typical home / mobile) silently fail to dial — `UDP send: network unreachable` for WireGuard, `connection refused` / no route for V2Ray.

Fix

Add a small `preferIPv4` helper in `src/utils.ts` and use it from both VPN classes. Falls back to `addrs[0]` when no IPv4 entry is present, so v6-only consumers see no behavior change.

Diff (helper)

```ts
export function preferIPv4(addrs: string[]): string {
return addrs.find(a => /^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$/.test(a)) ?? addrs[0];
}
```

Diff (call sites)

```diff
-const host = nodeAddrs[0];
+const host = preferIPv4(nodeAddrs);
```

```diff
-const address = nodeAddrs[0];
+const address = preferIPv4(nodeAddrs);
```

Note

If you'd rather inline the helper at the call sites instead of exporting it from utils, happy to refactor. Centralized helper keeps the rationale comment in one place.

Test plan

  • `npx tsc --noEmit` passes
  • v4-first arrays unchanged (helper returns `addrs[0]`)
  • v6-first arrays now return the v4 entry
  • v6-only arrays unchanged (helper returns `addrs[0]` via nullish fallback)

Sentinel-Bluebuilder and others added 2 commits April 26, 2026 15:59
Both Wireguard.parseConfig and V2Ray.parseConfig pick `nodeAddrs[0]` for
the outbound endpoint. Sentinel chain does not guarantee ordering of
`result.addrs`, so on dual-stack nodes that return IPv6 first, consumers
on v4-only networks (the common home / mobile environment) silently fail
to dial.

Add `preferIPv4` helper in utils and use it from both VPN classes. Falls
back to addrs[0] when no IPv4 is present, so v6-only consumers are
unaffected.
Per maintainer feedback: validate IPv4 with Node's net.isIP (returns 4 for
IPv4), matching how src/vpn/v2ray already detects address families, rather
than a hand-rolled /\d{1,3}.../ regex. isIP also rejects out-of-range octets
(e.g. 999.1.1.1) that the loose regex would accept.
@Sentinel-Bluebuilder

Copy link
Copy Markdown
Author

Switched to net.isIP per your feedback — preferIPv4 now uses addrs.find(a => isIP(a) === 4) ?? addrs[0], matching how src/vpn/v2ray already classifies addresses. Bonus over the old regex: isIP rejects out-of-range octets like 999.1.1.1 that /^\d{1,3}(\.\d{1,3}){3}$/ would wrongly accept.

One thing I want to confirm before considering this fully correct — a question about the input shape:

net.isIP("1.2.3.4:51820") returns 0, not 4 — it only accepts a bare IP, no port. The WireGuard.parseConfig JSDoc example shows result.addrs as bare IPs (handshakeData.addrs → interface addresses), but the handshake metadata carries the port separately (meta.port). So as far as I can tell result.addrs entries are always bare hosts and isIP === 4 is exactly right.

If there's any path where an entry in result.addrs can arrive as host:port, we'd need to split the port off before the isIP check (and the old regex would have silently failed there too). Can you confirm result.addrs is always bare IPs/hostnames with no port suffix? If so this is complete as written; if not I'll add a port-strip.

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.

1 participant