routing.js

const os = require('node:os');
const fs = require('node:fs');
const readline = require('node:readline');
const { inetNtop } = require('#lib/converters');
const { socket } = require('#lib/bindings');

const platform = os.platform();

/**
 * A single entry in the system routing table.
 * @typedef {Object} RouteEntry
 * @property {string} destination - The destination IP, network, or 'default'.
 * @property {number} prefixLength - The network mask prefix length (e.g., 24, 64).
 * @property {string} gateway - The gateway IP address.
 * @property {number} metric - Route metric value.
 * @property {string[]} flags - Array of route flags (e.g., ['U', 'G', 'H']).
 * @property {string} family - The address family ('AF_INET' or 'AF_INET6').
 */

/**
 * Retrieves the system's IP routing table.
 *
 * @async
 * @function getRoutingTable
 * @memberof module:system
 * @returns {Promise<Object<string, RouteEntry[]>>} An object mapping interface names to arrays of route entries.
 */
if (platform === 'linux') {
  const { RouteFlags } = require('./enums');

  const flagBits = [
    [RouteFlags.RTF_UP, 'U'],
    [RouteFlags.RTF_GATEWAY, 'G'],
    [RouteFlags.RTF_HOST, 'H'],
    [RouteFlags.RTF_REJECT, 'R'],
    [RouteFlags.RTF_DYNAMIC, 'D'],
    [RouteFlags.RTF_MODIFIED, 'M'],
    [RouteFlags.RTF_MULTICAST, 'm'],
  ];

  const parseFlags = v => flagBits.filter(([m]) => (v & m) !== 0).map(([, c]) => c);

  const hexToIPv6 = hex => {
    if (!hex || hex.length !== 32) return '';
    const buf = Buffer.alloc(16);
    for (let i = 0; i < 16; i++) buf[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
    return inetNtop(socket.AF_INET6, buf);
  };

  const withZone = (a, i) => (a && a.startsWith('fe80::') ? `${a}%${i}` : a);

  const prefixFromMask = m =>
    m
      .split('.')
      .map(o => parseInt(o, 10).toString(2))
      .reduce((s, b) => s + [...b].filter(x => x === '1').length, 0);

  async function ipv4Routes() {
    const out = {};
    const rl = readline.createInterface({
      input: fs.createReadStream('/proc/net/route'),
      crlfDelay: Infinity,
    });
    let skip = true;
    for await (const line of rl) {
      if (skip) {
        skip = false;
        continue;
      }
      const [
        iface,
        dstHex,
        gwHex,
        flHex,
        ,
        ,
        metricStr,
        maskHex,
      ] = line.trim().split(/\s+/);

      const dstIp = inetNtop(parseInt(dstHex, 16));
      const gwIp = inetNtop(parseInt(gwHex, 16));
      const maskIp = inetNtop(parseInt(maskHex, 16));

      const flags = parseFlags(parseInt(flHex, 16));
      const gateway = gwIp;
      const prefix = dstIp === '0.0.0.0' ? 0 : prefixFromMask(maskIp);
      const destination = prefix === 0 ? 'default' : dstIp;

      if (!out[iface]) out[iface] = [];
      out[iface].push({
        destination,
        prefixLength: prefix,
        gateway,
        metric: parseInt(metricStr, 10),
        flags,
        family: 'AF_INET',
      });
    }
    return out;
  }

  async function ipv6Routes() {
    if (!fs.promises
      .access('/proc/net/ipv6_route')
      .then(() => true)
      .catch(() => false)
    ) {
      return {};
    }

    const out = {};
    const rl = readline.createInterface({
      input: fs.createReadStream('/proc/net/ipv6_route'),
      crlfDelay: Infinity,
    });

    for await (const line of rl) {
      const p = line.trim().split(/\s+/);
      if (p.length < 10) continue;
      const [dstHex, plenHex, , , gwHex, metricHex, , , flHex, iface] = p;

      const dstIp = hexToIPv6(dstHex);
      const gwIp = hexToIPv6(gwHex);
      const prefix = parseInt(plenHex, 16);
      const flags = parseFlags(parseInt(flHex, 16));
      const gateway = withZone(gwIp, iface);
      const destination = prefix === 0 ? 'default' : withZone(dstIp, iface);

      if (!out[iface]) out[iface] = [];
      out[iface].push({
        destination,
        prefixLength: prefix,
        gateway,
        metric: parseInt(metricHex, 16),
        flags,
        family: 'AF_INET6',
      });
    }
    return out;
  }

  async function getRoutingTable() {
    const [v4, v6] = await Promise.all([ipv4Routes(), ipv6Routes()]);
    const merged = { ...v4 };
    for (const [i, lst] of Object.entries(v6)) {
      merged[i] = merged[i] ? merged[i].concat(lst) : lst;
    }
    return merged;
  }

  module.exports = { getRoutingTable };
} else {
  const { getRoutingTable: getRoutingTableCxx } = require('./bindings');
  module.exports = {
    async getRoutingTable() {
      return new Promise((res, rej) => {
        getRoutingTableCxx(r => (r instanceof Error ? rej(r) : res(r)));
      });
    },
  };
}