default-gateway
Version:
Get the default network gateway, cross-platform.
273 lines (236 loc) • 9.85 kB
JavaScript
import {isIP} from "node:net";
import {execa, execaSync} from "execa";
import {platform, type, release, networkInterfaces} from "node:os";
const plat = platform();
const dests = new Set(["default", "0.0.0.0", "0.0.0.0/0", "::", "::/0"]);
let promise, sync;
if (plat === "linux") {
const parse = (stdout, family) => {
for (const line of (stdout || "").trim().split("\n")) {
const results = /default( via .+?)?( dev .+?)( |$)/.exec(line) || [];
const gateway = (results[1] || "").substring(5);
const iface = (results[2] || "").substring(5);
if (gateway && isIP(gateway)) { // default via 1.2.3.4 dev en0
return {gateway, version: family, int: (iface ?? null)};
} else if (iface && !gateway) { // default via dev en0
const interfaces = networkInterfaces();
const addresses = interfaces[iface];
for (const addr of addresses || []) {
if (Number(family.substring(3)) === family && isIP(addr.address)) {
return {gateway: addr.address, version: family, int: (iface ?? null)};
}
}
}
}
throw new Error("Unable to determine default gateway");
};
promise = async family => {
const {stdout} = await execa("ip", [`-${family}`, "r"]);
return parse(stdout, family);
};
sync = family => {
const {stdout} = execaSync("ip", [`-${family}`, "r"]);
return parse(stdout, family);
};
} else if (plat === "darwin") {
// The IPv4 gateway is in column 3 in Darwin 19 (macOS 10.15 Catalina) and higher,
// previously it was in column 5
const v4IfaceColumn = parseInt(release()) >= 19 ? 3 : 5;
const parse = (stdout, family) => {
for (const line of (stdout || "").trim().split("\n")) {
const results = line.split(/ +/) || [];
const target = results[0];
const gateway = results[1];
const iface = results[family === 4 ? v4IfaceColumn : 3];
if (dests.has(target) && gateway && isIP(gateway)) {
return {gateway, version: family, int: (iface ?? null)};
}
}
throw new Error("Unable to determine default gateway");
};
promise = async family => {
const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
sync = family => {
const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
} else if (plat === "win32") {
const gwArgs = "path Win32_NetworkAdapterConfiguration where IPEnabled=true get DefaultIPGateway,GatewayCostMetric,IPConnectionMetric,Index /format:table".split(" ");
const ifArgs = index => `path Win32_NetworkAdapter where Index=${index} get NetConnectionID,MACAddress /format:table`.split(" ");
const spawnOpts = {
windowsHide: true,
};
// Parsing tables like this. The final metric is GatewayCostMetric + IPConnectionMetric
//
// DefaultIPGateway GatewayCostMetric Index IPConnectionMetric
// {"1.2.3.4", "2001:db8::1"} {0, 256} 12 25
// {"2.3.4.5"} {25} 12 55
function parseGwTable(gwTable, family) { // eslint-disable-line no-inner-declarations
let [bestGw, bestMetric, bestId] = [null, null, null];
for (let line of (gwTable || "").trim().split(/\r?\n/).splice(1)) {
line = line.trim();
const [_, gwArr, gwCostsArr, id, ipMetric] = /({.+?}) +({.+?}) +([0-9]+) +([0-9]+)/.exec(line) || [];
if (!gwArr) continue;
const gateways = (gwArr.match(/"(.+?)"/g) || []).map(match => match.substring(1, match.length - 1));
const gatewayCosts = (gwCostsArr.match(/[0-9]+/g) || []);
for (const [index, gateway] of Object.entries(gateways)) {
if (!gateway || isIP(gateway) !== family) continue;
const metric = parseInt(gatewayCosts[index]) + parseInt(ipMetric);
if (!bestGw || metric < bestMetric) {
[bestGw, bestMetric, bestId] = [gateway, metric, id];
}
}
}
if (bestGw) return [bestGw, bestId];
}
function parseIfTable(ifTable) { // eslint-disable-line no-inner-declarations
const line = (ifTable || "").trim().split("\n")[1];
let [mac, name] = line.trim().split(/\s+/);
mac = mac.toLowerCase();
// try to get the interface name by matching the mac to os.networkInterfaces to avoid wmic's encoding issues
// https://github.com/silverwind/default-gateway/issues/14
for (const [osname, addrs] of Object.entries(networkInterfaces())) {
for (const addr of addrs) {
if (addr?.mac?.toLowerCase() === mac) {
return osname;
}
}
}
return name;
}
promise = async family => {
const {stdout} = await execa("wmic", gwArgs, spawnOpts);
const [gateway, id] = parseGwTable(stdout, family) || [];
if (!gateway) throw new Error("Unable to determine default gateway");
let name;
if (id) {
const {stdout} = await execa("wmic", ifArgs(id), spawnOpts);
name = parseIfTable(stdout);
}
return {gateway, version: family, int: name ?? null};
};
sync = family => {
const {stdout} = execaSync("wmic", gwArgs, spawnOpts);
const [gateway, id] = parseGwTable(stdout, family) || [];
if (!gateway) throw new Error("Unable to determine default gateway");
let name;
if (id) {
const {stdout} = execaSync("wmic", ifArgs(id), spawnOpts);
name = parseIfTable(stdout);
}
return {gateway, version: family, int: name ?? null};
};
} else if (plat === "android") {
const parse = (stdout, family) => {
for (const line of (stdout || "").trim().split("\n")) {
const [_, gateway, iface] = /default via (.+?) dev (.+?)( |$)/.exec(line) || [];
if (gateway && isIP(gateway)) {
return {gateway, version: family, int: (iface ?? null)};
}
}
throw new Error("Unable to determine default gateway");
};
promise = async family => {
const {stdout} = await execa("ip", [`-${family}`, "r"]);
return parse(stdout, family);
};
sync = family => {
const {stdout} = execaSync("ip", [`-${family}`, "r"]);
return parse(stdout, family);
};
} else if (plat === "freebsd") {
const parse = (stdout, family) => {
for (const line of (stdout || "").trim().split("\n")) {
const [target, gateway, _, iface] = line.split(/ +/) || [];
if (dests.has(target) && gateway && isIP(gateway)) {
return {gateway, version: family, int: (iface ?? null)};
}
}
throw new Error("Unable to determine default gateway");
};
promise = async family => {
const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
sync = family => {
const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
} else if (plat === "aix" && type() === "OS400") {
const db2util = "/QOpenSys/pkgs/bin/db2util";
const sql = "select NEXT_HOP, LOCAL_BINDING_INTERFACE from QSYS2.NETSTAT_ROUTE_INFO where ROUTE_TYPE='DFTROUTE' and NEXT_HOP!='*DIRECT' and CONNECTION_TYPE=?";
const parse = (stdout, family) => {
try {
const resultObj = JSON.parse(stdout);
const gateway = resultObj.records[0].NEXT_HOP;
const iface = resultObj.records[0].LOCAL_BINDING_INTERFACE;
return {gateway, version: family, iface};
} catch {}
throw new Error("Unable to determine default gateway");
};
promise = async family => {
const {stdout} = await execa(db2util, [sql, "-p", `IPV${family}`, "-o", "json"]);
return parse(stdout, family);
};
sync = family => {
const {stdout} = execaSync(db2util, [sql, "-p", `IPV${family}`, "-o", "json"]);
return parse(stdout, family);
};
} else if (plat === "openbsd") {
const parse = (stdout, family) => {
for (const line of (stdout || "").trim().split("\n")) {
const results = line.split(/ +/) || [];
const target = results[0];
const gateway = results[1];
const iface = results[7];
if (dests.has(target) && gateway && isIP(gateway)) {
return {gateway, version: family, int: (iface ?? null)};
}
}
throw new Error("Unable to determine default gateway");
};
promise = async family => {
const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
sync = family => {
const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
} else if (plat === "sunos" || (plat === "aix" && type() !== "OS400")) { // AIX `netstat` output is compatible with Solaris
const parse = (stdout, family) => {
for (const line of (stdout || "").trim().split("\n")) {
const results = line.split(/ +/) || [];
const target = results[0];
const gateway = results[1];
const iface = results[5];
if (dests.has(target) && gateway && isIP(gateway)) {
return {gateway, version: family, int: (iface ?? null)};
}
}
throw new Error("Unable to determine default gateway");
};
promise = async family => {
const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
sync = family => {
const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
return parse(stdout, family);
};
} else {
promise = (_) => { throw new Error("Unsupported Platform"); };
sync = (_) => { throw new Error("Unsupported Platform"); };
}
export const gateway4async = () => promise(4);
export const gateway6async = () => promise(6);
export const gateway4sync = () => sync(4);
export const gateway6sync = () => sync(6);
export default {
gateway4async,
gateway6async,
gateway4sync,
gateway6sync,
};