const net = require('node:net');
const { toLong, cidrContains } = require('./ip');
const { getRoutingTable } = require('./routing');
async function check() {
await gatewayFor('216.58.209.78').then(console.log).catch(console.error);
await gatewayFor('127.0.0.1').then(console.log).catch(console.error);
await gatewayFor('2a00:1450:4001:810::200e').then(console.log).catch(console.error);
await gatewayFor('::1').then(console.log).catch(console.error);
}
//check();
/**
* Result object containing gateway and interface information.
* @typedef {Object} GatewayInfo
* @property {string} iface - The name of the network interface (e.g., 'eth0', 'wlan0').
* @property {string} ip - The next-hop gateway IP address, or the destination IP itself if on-link.
* @property {string} family - The address family ('AF_INET' for IPv4 or 'AF_INET6' for IPv6).
*/
/**
* Determines the appropriate gateway IP address and network interface required to reach a specific destination IP.
* If no destination IP is provided, it returns the default gateway.
*
* @async
* @function gatewayFor
* @memberof module:system
* @param {string|null}[dstIp=null] - The target IP address (IPv4 or IPv6) to route to.
* @returns {Promise<GatewayInfo|null>} An object with gateway details, or null if no route is found.
*
* @example
* const { system } = require('over-the-wire');
*
* // Find route to Google's IPv4
* const gw4 = await system.gatewayFor('216.58.209.78');
* console.log(gw4); // { iface: 'eth0', ip: '192.168.1.1', family: 'AF_INET' }
*
* // Find route to Google's IPv6
* const gw6 = await system.gatewayFor('2a00:1450:4001:810::200e');
* console.log(gw6); // { iface: 'eth0', ip: 'fe80::1', family: 'AF_INET6' }
*/
async function gatewayFor(dstIp = null) {
const routes = await getRoutingTable();
const flat = Object.entries(routes).flatMap(([iface, list]) =>
list.map(r => ({
iface,
...r,
af: r.family,
family: [{ name: 'AF_INET', value: 4 }, { name: 'AF_INET6', value: 6 }].find(e => e.name == r.family)?.value ?? 0,
}))
);
const route = selectRoute(dstIp, flat);
if (!route) return null;
const isV4 = route.family === 4;
const gwZero = isV4 ? '0.0.0.0' : '::';
const nextIp = route.onLink || !route.gateway || route.gateway === gwZero ? (dstIp ?? '') : route.gateway;
return { iface: route.iface, ip: nextIp, family: route.af };
}
module.exports = { gatewayFor };
/**
* Selects the best matching route for a given destination IP based on prefix length and metric.
*
* @private
* @param {string|null} dstIp - The target IP address.
* @param {Array<Object>} routes - A flattened array of routing table entries.
* @returns {Object|null} The best route object, or null if no route matches.
*/
function selectRoute(dstIp, routes) {
if (dstIp === null) {
const defaults = routes.filter(r => r.destination === 'default');
if (!defaults.length) return null;
return defaults.reduce((best, r) =>
r.metric < best.metric || (r.metric === best.metric && r.family === 4 && best.family === 6) ? r : best
);
}
const v4 = net.isIP(dstIp) === 4;
const dLng = v4 ? toLong(dstIp) : null;
let best = null, bestLen = -1, bestMetric = Infinity;
for (const r of routes) {
if (v4 !== (r.family === 4)) continue;
if (!inSubnet(dstIp, r.destination, r.prefixLength, v4, dLng)) continue;
if (r.prefixLength > bestLen || (r.prefixLength === bestLen && r.metric < bestMetric)) {
best = r; bestLen = r.prefixLength; bestMetric = r.metric;
}
}
return best;
}
/**
* Checks if an IP address belongs to a specific subnet.
*
* @private
* @param {string} dst - The target IP address.
* @param {string} netIp - The network IP address (or 'default').
* @param {number} preLen - The prefix length (subnet mask).
* @param {boolean} v4 - True if evaluating an IPv4 address, false for IPv6.
* @param {number|null} dLng - The target IP address converted to a long integer (IPv4 only).
* @returns {boolean} True if the IP is within the subnet, false otherwise.
*/
function inSubnet(dst, netIp, preLen, v4, dLng) {
if (netIp === 'default') return true;
if (v4) {
const base = toLong(netIp);
const mask = preLen ? (~0 << (32 - preLen)) >>> 0 : 0;
return (dLng & mask) === (base & mask);
}
return cidrContains(`${netIp.split('%')[0]}/${preLen}`, dst);
}