UNPKG

9.85 kBJavaScriptView Raw
1import {isIP} from "node:net";
2import {execa, execaSync} from "execa";
3import {platform, type, release, networkInterfaces} from "node:os";
4
5const plat = platform();
6const dests = new Set(["default", "0.0.0.0", "0.0.0.0/0", "::", "::/0"]);
7let promise, sync;
8
9if (plat === "linux") {
10 const parse = (stdout, family) => {
11 for (const line of (stdout || "").trim().split("\n")) {
12 const results = /default( via .+?)?( dev .+?)( |$)/.exec(line) || [];
13 const gateway = (results[1] || "").substring(5);
14 const iface = (results[2] || "").substring(5);
15 if (gateway && isIP(gateway)) { // default via 1.2.3.4 dev en0
16 return {gateway, version: family, int: (iface ?? null)};
17 } else if (iface && !gateway) { // default via dev en0
18 const interfaces = networkInterfaces();
19 const addresses = interfaces[iface];
20 for (const addr of addresses || []) {
21 if (Number(family.substring(3)) === family && isIP(addr.address)) {
22 return {gateway: addr.address, version: family, int: (iface ?? null)};
23 }
24 }
25 }
26 }
27 throw new Error("Unable to determine default gateway");
28 };
29
30 promise = async family => {
31 const {stdout} = await execa("ip", [`-${family}`, "r"]);
32 return parse(stdout, family);
33 };
34
35 sync = family => {
36 const {stdout} = execaSync("ip", [`-${family}`, "r"]);
37 return parse(stdout, family);
38 };
39} else if (plat === "darwin") {
40 // The IPv4 gateway is in column 3 in Darwin 19 (macOS 10.15 Catalina) and higher,
41 // previously it was in column 5
42 const v4IfaceColumn = parseInt(release()) >= 19 ? 3 : 5;
43
44 const parse = (stdout, family) => {
45 for (const line of (stdout || "").trim().split("\n")) {
46 const results = line.split(/ +/) || [];
47 const target = results[0];
48 const gateway = results[1];
49 const iface = results[family === 4 ? v4IfaceColumn : 3];
50 if (dests.has(target) && gateway && isIP(gateway)) {
51 return {gateway, version: family, int: (iface ?? null)};
52 }
53 }
54 throw new Error("Unable to determine default gateway");
55 };
56
57 promise = async family => {
58 const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
59 return parse(stdout, family);
60 };
61
62 sync = family => {
63 const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
64 return parse(stdout, family);
65 };
66} else if (plat === "win32") {
67 const gwArgs = "path Win32_NetworkAdapterConfiguration where IPEnabled=true get DefaultIPGateway,GatewayCostMetric,IPConnectionMetric,Index /format:table".split(" ");
68 const ifArgs = index => `path Win32_NetworkAdapter where Index=${index} get NetConnectionID,MACAddress /format:table`.split(" ");
69
70 const spawnOpts = {
71 windowsHide: true,
72 };
73
74 // Parsing tables like this. The final metric is GatewayCostMetric + IPConnectionMetric
75 //
76 // DefaultIPGateway GatewayCostMetric Index IPConnectionMetric
77 // {"1.2.3.4", "2001:db8::1"} {0, 256} 12 25
78 // {"2.3.4.5"} {25} 12 55
79 function parseGwTable(gwTable, family) { // eslint-disable-line no-inner-declarations
80 let [bestGw, bestMetric, bestId] = [null, null, null];
81
82 for (let line of (gwTable || "").trim().split(/\r?\n/).splice(1)) {
83 line = line.trim();
84 const [_, gwArr, gwCostsArr, id, ipMetric] = /({.+?}) +({.+?}) +([0-9]+) +([0-9]+)/.exec(line) || [];
85 if (!gwArr) continue;
86
87 const gateways = (gwArr.match(/"(.+?)"/g) || []).map(match => match.substring(1, match.length - 1));
88 const gatewayCosts = (gwCostsArr.match(/[0-9]+/g) || []);
89
90 for (const [index, gateway] of Object.entries(gateways)) {
91 if (!gateway || isIP(gateway) !== family) continue;
92
93 const metric = parseInt(gatewayCosts[index]) + parseInt(ipMetric);
94 if (!bestGw || metric < bestMetric) {
95 [bestGw, bestMetric, bestId] = [gateway, metric, id];
96 }
97 }
98 }
99
100 if (bestGw) return [bestGw, bestId];
101 }
102
103 function parseIfTable(ifTable) { // eslint-disable-line no-inner-declarations
104 const line = (ifTable || "").trim().split("\n")[1];
105
106 let [mac, name] = line.trim().split(/\s+/);
107 mac = mac.toLowerCase();
108
109 // try to get the interface name by matching the mac to os.networkInterfaces to avoid wmic's encoding issues
110 // https://github.com/silverwind/default-gateway/issues/14
111 for (const [osname, addrs] of Object.entries(networkInterfaces())) {
112 for (const addr of addrs) {
113 if (addr?.mac?.toLowerCase() === mac) {
114 return osname;
115 }
116 }
117 }
118 return name;
119 }
120
121 promise = async family => {
122 const {stdout} = await execa("wmic", gwArgs, spawnOpts);
123 const [gateway, id] = parseGwTable(stdout, family) || [];
124 if (!gateway) throw new Error("Unable to determine default gateway");
125
126 let name;
127 if (id) {
128 const {stdout} = await execa("wmic", ifArgs(id), spawnOpts);
129 name = parseIfTable(stdout);
130 }
131
132 return {gateway, version: family, int: name ?? null};
133 };
134
135 sync = family => {
136 const {stdout} = execaSync("wmic", gwArgs, spawnOpts);
137 const [gateway, id] = parseGwTable(stdout, family) || [];
138 if (!gateway) throw new Error("Unable to determine default gateway");
139
140 let name;
141 if (id) {
142 const {stdout} = execaSync("wmic", ifArgs(id), spawnOpts);
143 name = parseIfTable(stdout);
144 }
145
146 return {gateway, version: family, int: name ?? null};
147 };
148} else if (plat === "android") {
149 const parse = (stdout, family) => {
150 for (const line of (stdout || "").trim().split("\n")) {
151 const [_, gateway, iface] = /default via (.+?) dev (.+?)( |$)/.exec(line) || [];
152 if (gateway && isIP(gateway)) {
153 return {gateway, version: family, int: (iface ?? null)};
154 }
155 }
156 throw new Error("Unable to determine default gateway");
157 };
158
159 promise = async family => {
160 const {stdout} = await execa("ip", [`-${family}`, "r"]);
161 return parse(stdout, family);
162 };
163
164 sync = family => {
165 const {stdout} = execaSync("ip", [`-${family}`, "r"]);
166 return parse(stdout, family);
167 };
168} else if (plat === "freebsd") {
169 const parse = (stdout, family) => {
170 for (const line of (stdout || "").trim().split("\n")) {
171 const [target, gateway, _, iface] = line.split(/ +/) || [];
172 if (dests.has(target) && gateway && isIP(gateway)) {
173 return {gateway, version: family, int: (iface ?? null)};
174 }
175 }
176 throw new Error("Unable to determine default gateway");
177 };
178
179 promise = async family => {
180 const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
181 return parse(stdout, family);
182 };
183
184 sync = family => {
185 const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
186 return parse(stdout, family);
187 };
188} else if (plat === "aix" && type() === "OS400") {
189 const db2util = "/QOpenSys/pkgs/bin/db2util";
190 const sql = "select NEXT_HOP, LOCAL_BINDING_INTERFACE from QSYS2.NETSTAT_ROUTE_INFO where ROUTE_TYPE='DFTROUTE' and NEXT_HOP!='*DIRECT' and CONNECTION_TYPE=?";
191
192 const parse = (stdout, family) => {
193 try {
194 const resultObj = JSON.parse(stdout);
195 const gateway = resultObj.records[0].NEXT_HOP;
196 const iface = resultObj.records[0].LOCAL_BINDING_INTERFACE;
197 return {gateway, version: family, iface};
198 } catch {}
199 throw new Error("Unable to determine default gateway");
200 };
201
202 promise = async family => {
203 const {stdout} = await execa(db2util, [sql, "-p", `IPV${family}`, "-o", "json"]);
204 return parse(stdout, family);
205 };
206
207 sync = family => {
208 const {stdout} = execaSync(db2util, [sql, "-p", `IPV${family}`, "-o", "json"]);
209 return parse(stdout, family);
210 };
211} else if (plat === "openbsd") {
212 const parse = (stdout, family) => {
213 for (const line of (stdout || "").trim().split("\n")) {
214 const results = line.split(/ +/) || [];
215 const target = results[0];
216 const gateway = results[1];
217 const iface = results[7];
218 if (dests.has(target) && gateway && isIP(gateway)) {
219 return {gateway, version: family, int: (iface ?? null)};
220 }
221 }
222 throw new Error("Unable to determine default gateway");
223 };
224
225 promise = async family => {
226 const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
227 return parse(stdout, family);
228 };
229
230 sync = family => {
231 const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
232 return parse(stdout, family);
233 };
234} else if (plat === "sunos" || (plat === "aix" && type() !== "OS400")) { // AIX `netstat` output is compatible with Solaris
235 const parse = (stdout, family) => {
236 for (const line of (stdout || "").trim().split("\n")) {
237 const results = line.split(/ +/) || [];
238 const target = results[0];
239 const gateway = results[1];
240 const iface = results[5];
241 if (dests.has(target) && gateway && isIP(gateway)) {
242 return {gateway, version: family, int: (iface ?? null)};
243 }
244 }
245 throw new Error("Unable to determine default gateway");
246 };
247
248 promise = async family => {
249 const {stdout} = await execa("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
250 return parse(stdout, family);
251 };
252
253 sync = family => {
254 const {stdout} = execaSync("netstat", ["-rn", "-f", family === 4 ? "inet" : "inet6"]);
255 return parse(stdout, family);
256 };
257} else {
258 promise = (_) => { throw new Error("Unsupported Platform"); };
259 sync = (_) => { throw new Error("Unsupported Platform"); };
260}
261
262export const gateway4async = () => promise(4);
263export const gateway6async = () => promise(6);
264export const gateway4sync = () => sync(4);
265export const gateway6sync = () => sync(6);
266
267export default {
268 gateway4async,
269 gateway6async,
270 gateway4sync,
271 gateway6sync,
272};