fix: harden nordvpn wireguard verification

This commit is contained in:
Stefano Fiorini
2026-03-12 00:50:05 -05:00
parent a8a285b356
commit d0c50f5d8a
2 changed files with 217 additions and 4 deletions

View File

@@ -1,7 +1,9 @@
#!/usr/bin/env node
const { execFile, spawn } = require("node:child_process");
const dns = require("node:dns");
const fs = require("node:fs");
const net = require("node:net");
const os = require("node:os");
const path = require("node:path");
const https = require("node:https");
@@ -34,6 +36,7 @@ const MAC_WG_HELPER_PATH = path.join(
const DEFAULT_DNS_IPV4 = "103.86.96.100";
const DEFAULT_DNS_IPV6 = "2400:bb40:4444::100";
const CLIENT_IPV4 = "10.5.0.2";
const DNS_FALLBACK_RESOLVERS = ["1.1.1.1", "8.8.8.8"];
function printJson(payload, exitCode = 0, errorStream = false) {
const body = `${JSON.stringify(payload, null, 2)}\n`;
@@ -217,16 +220,89 @@ function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function detectMacWireguardActiveFromIfconfig(ifconfigOutput) {
let inUtunBlock = false;
const lines = `${ifconfigOutput || ""}`.split("\n");
for (const line of lines) {
if (/^utun\d+:/.test(line)) {
inUtunBlock = true;
continue;
}
if (/^\S/.test(line)) {
inUtunBlock = false;
}
if (inUtunBlock && line.includes(`inet ${CLIENT_IPV4}`)) {
return true;
}
}
return false;
}
async function resolveHostnameWithFallback(hostname, options = {}) {
const resolvers = options.resolvers || DNS_FALLBACK_RESOLVERS;
const resolveWithResolver =
options.resolveWithResolver ||
(async (targetHostname, resolverIp) => {
const resolver = new dns.promises.Resolver();
resolver.setServers([resolverIp]);
return resolver.resolve4(targetHostname);
});
for (const resolverIp of resolvers) {
try {
const addresses = await resolveWithResolver(hostname, resolverIp);
if (Array.isArray(addresses) && addresses.length) {
return addresses[0];
}
} catch {
// Try the next resolver.
}
}
return "";
}
function buildLookupResult(address, options = {}) {
if (options && options.all) {
return [{ address, family: 4 }];
}
return [address, 4];
}
function fetchJson(url, headers = {}) {
return new Promise((resolve) => {
const targetUrl = new URL(url);
const req = https.get(
url,
targetUrl,
{
headers: {
"User-Agent": "nordvpn-client-skill/1.0",
Accept: "application/json",
...headers,
},
lookup: (hostname, options, callback) => {
const ipVersion = net.isIP(hostname);
if (ipVersion) {
callback(null, hostname, ipVersion);
return;
}
resolveHostnameWithFallback(hostname)
.then((address) => {
if (address) {
const result = buildLookupResult(address, options);
if (options && options.all) {
callback(null, result);
} else {
callback(null, result[0], result[1]);
}
return;
}
dns.lookup(hostname, options, callback);
})
.catch(() => dns.lookup(hostname, options, callback));
},
},
(res) => {
let data = "";
@@ -288,6 +364,7 @@ async function probeMacWireguard() {
let active = false;
let showRaw = "";
let endpoint = "";
let ifconfigRaw = "";
if (wgPath) {
const show = await runExec(wgPath, ["show", MAC_WG_INTERFACE]);
@@ -299,6 +376,17 @@ async function probeMacWireguard() {
}
}
const ifconfig = await runExec("ifconfig");
ifconfigRaw = (ifconfig.stdout || ifconfig.stderr).trim();
if (!active) {
active = detectMacWireguardActiveFromIfconfig(ifconfigRaw);
}
if (!endpoint && fileExists(WG_CONFIG_PATH)) {
const configText = fs.readFileSync(WG_CONFIG_PATH, "utf8");
const match = configText.match(/^Endpoint\s*=\s*(.+)$/m);
if (match) endpoint = match[1].trim();
}
return {
wgPath: wgPath || null,
wgQuickPath: wgQuickPath || null,
@@ -311,6 +399,7 @@ async function probeMacWireguard() {
active,
endpoint: endpoint || null,
showRaw,
ifconfigRaw,
authCache: readJsonFile(AUTH_CACHE_PATH),
lastConnection: readJsonFile(LAST_CONNECTION_PATH),
};
@@ -762,6 +851,27 @@ async function verifyConnection(target) {
};
}
async function verifyConnectionWithRetry(target, options = {}) {
const attempts = Math.max(1, Number(options.attempts || 4));
const delayMs = Math.max(0, Number(options.delayMs || 1500));
const getIpInfo = options.getPublicIpInfo || getPublicIpInfo;
let lastIpInfo = null;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
const ipInfo = await getIpInfo();
lastIpInfo = ipInfo;
const ok = target ? locationMatches(ipInfo, target) : Boolean(ipInfo && ipInfo.ok);
if (ok) {
return { ok: true, ipInfo };
}
if (attempt < attempts) {
await sleep(delayMs);
}
}
return { ok: false, ipInfo: lastIpInfo };
}
async function connectViaCli(cliPath, target) {
const attempts = [];
if (target.city) attempts.push([target.city]);
@@ -1020,7 +1130,7 @@ async function main() {
if (action === "verify") {
const target = args.country || args.city ? buildConnectTarget(args) : null;
const verified = await verifyConnection(target);
const verified = await verifyConnectionWithRetry(target);
const refreshed = await probeInstallation(platform);
printJson(
{
@@ -1059,8 +1169,7 @@ async function main() {
printJson({ action, requestedTarget: target, ...connectResult });
}
await sleep(3000);
const verified = await verifyConnection(target);
const verified = await verifyConnectionWithRetry(target, { attempts: 6, delayMs: 2000 });
const refreshed = await probeInstallation(platform);
printJson(
{