1 | import {isIP} from "node:net";
|
2 | import {execa, execaSync} from "execa";
|
3 | import {platform, type, release, networkInterfaces} from "node:os";
|
4 |
|
5 | const plat = platform();
|
6 | const dests = new Set(["default", "0.0.0.0", "0.0.0.0/0", "::", "::/0"]);
|
7 | let promise, sync;
|
8 |
|
9 | if (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)) {
|
16 | return {gateway, version: family, int: (iface ?? null)};
|
17 | } else if (iface && !gateway) {
|
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 |
|
41 |
|
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 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | function parseGwTable(gwTable, family) {
|
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) {
|
104 | const line = (ifTable || "").trim().split("\n")[1];
|
105 |
|
106 | let [mac, name] = line.trim().split(/\s+/);
|
107 | mac = mac.toLowerCase();
|
108 |
|
109 |
|
110 |
|
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")) {
|
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 |
|
262 | export const gateway4async = () => promise(4);
|
263 | export const gateway6async = () => promise(6);
|
264 | export const gateway4sync = () => sync(4);
|
265 | export const gateway6sync = () => sync(6);
|
266 |
|
267 | export default {
|
268 | gateway4async,
|
269 | gateway6async,
|
270 | gateway4sync,
|
271 | gateway6sync,
|
272 | };
|