diff --git a/doc/api/os.md b/doc/api/os.md index 3b4b9526cb4..9dc0fcf3087 100644 --- a/doc/api/os.md +++ b/doc/api/os.md @@ -253,6 +253,9 @@ The properties available on the assigned network address object include: similar interface that is not remotely accessible; otherwise `false` * `scopeid` {number} The numeric IPv6 scope ID (only specified when `family` is `IPv6`) +* `cidr` {string} The assigned IPv4 or IPv6 address with the routing prefix + in CIDR notation. If the `netmask` is invalid, this property is set + to `null` ```js @@ -263,14 +266,16 @@ The properties available on the assigned network address object include: netmask: '255.0.0.0', family: 'IPv4', mac: '00:00:00:00:00:00', - internal: true + internal: true, + cidr: '127.0.0.1/8' }, { address: '::1', netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', family: 'IPv6', mac: '00:00:00:00:00:00', - internal: true + internal: true, + cidr: '::1/128' } ], eth0: [ @@ -279,14 +284,16 @@ The properties available on the assigned network address object include: netmask: '255.255.255.0', family: 'IPv4', mac: '01:02:03:0a:0b:0c', - internal: false + internal: false, + cidr: '192.168.1.108/24' }, { address: 'fe80::a00:27ff:fe4e:66a1', netmask: 'ffff:ffff:ffff:ffff::', family: 'IPv6', mac: '01:02:03:0a:0b:0c', - internal: false + internal: false, + cidr: 'fe80::a00:27ff:fe4e:66a1/64' } ] } diff --git a/lib/internal/os.js b/lib/internal/os.js new file mode 100644 index 00000000000..74ed6e767ee --- /dev/null +++ b/lib/internal/os.js @@ -0,0 +1,41 @@ +'use strict'; + +function getCIDRSuffix(mask, protocol = 'ipv4') { + const isV6 = protocol === 'ipv6'; + const bitsString = mask + .split(isV6 ? ':' : '.') + .filter((v) => !!v) + .map((v) => pad(parseInt(v, isV6 ? 16 : 10).toString(2), isV6)) + .join(''); + + if (isValidMask(bitsString)) { + return countOnes(bitsString); + } else { + return null; + } +} + +function pad(binaryString, isV6) { + const groupLength = isV6 ? 16 : 8; + const binLen = binaryString.length; + + return binLen < groupLength ? + `${'0'.repeat(groupLength - binLen)}${binaryString}` : binaryString; +} + +function isValidMask(bitsString) { + const firstIndexOfZero = bitsString.indexOf(0); + const lastIndexOfOne = bitsString.lastIndexOf(1); + + return firstIndexOfZero < 0 || firstIndexOfZero > lastIndexOfOne; +} + +function countOnes(bitsString) { + return bitsString + .split('') + .reduce((acc, bit) => acc += parseInt(bit, 10), 0); +} + +module.exports = { + getCIDRSuffix +}; diff --git a/lib/os.js b/lib/os.js index 4a99cab81e3..078dba3fcca 100644 --- a/lib/os.js +++ b/lib/os.js @@ -24,6 +24,7 @@ const pushValToArrayMax = process.binding('util').pushValToArrayMax; const constants = process.binding('constants').os; const deprecate = require('internal/util').deprecate; +const getCIDRSuffix = require('internal/os').getCIDRSuffix; const isWindows = process.platform === 'win32'; const { @@ -121,6 +122,21 @@ function endianness() { } endianness[Symbol.toPrimitive] = () => kEndianness; +function networkInterfaces() { + const interfaceAddresses = getInterfaceAddresses(); + + return Object.entries(interfaceAddresses).reduce((acc, [key, val]) => { + acc[key] = val.map((v) => { + const protocol = v.family.toLowerCase(); + const suffix = getCIDRSuffix(v.netmask, protocol); + const cidr = suffix ? `${v.address}/${suffix}` : null; + + return Object.assign({}, v, { cidr }); + }); + return acc; + }, {}); +} + module.exports = exports = { arch, cpus, @@ -130,7 +146,7 @@ module.exports = exports = { homedir: getHomeDirectory, hostname: getHostname, loadavg, - networkInterfaces: getInterfaceAddresses, + networkInterfaces, platform, release: getOSRelease, tmpdir, diff --git a/node.gyp b/node.gyp index 81f549f8b63..040e38223da 100644 --- a/node.gyp +++ b/node.gyp @@ -91,6 +91,7 @@ 'lib/internal/linkedlist.js', 'lib/internal/net.js', 'lib/internal/module.js', + 'lib/internal/os.js', 'lib/internal/process/next_tick.js', 'lib/internal/process/promises.js', 'lib/internal/process/stdio.js', diff --git a/test/parallel/test-internal-os.js b/test/parallel/test-internal-os.js new file mode 100644 index 00000000000..c4014abc5b1 --- /dev/null +++ b/test/parallel/test-internal-os.js @@ -0,0 +1,32 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const assert = require('assert'); +const getCIDRSuffix = require('internal/os').getCIDRSuffix; + +const specs = [ + // valid + ['128.0.0.0', 'ipv4', 1], + ['255.0.0.0', 'ipv4', 8], + ['255.255.255.128', 'ipv4', 25], + ['255.255.255.255', 'ipv4', 32], + ['ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', 'ipv6', 128], + ['ffff:ffff:ffff:ffff::', 'ipv6', 64], + ['ffff:ffff:ffff:ff80::', 'ipv6', 57], + // invalid + ['255.0.0.1', 'ipv4', null], + ['255.255.9.0', 'ipv4', null], + ['255.255.1.0', 'ipv4', null], + ['ffff:ffff:43::', 'ipv6', null], + ['ffff:ffff:ffff:1::', 'ipv6', null] +]; + +specs.forEach(([mask, protocol, expectedSuffix]) => { + const actualSuffix = getCIDRSuffix(mask, protocol); + + assert.strictEqual( + actualSuffix, expectedSuffix, + `Mask: ${mask}, expected: ${expectedSuffix}, actual: ${actualSuffix}` + ); +}); diff --git a/test/parallel/test-os.js b/test/parallel/test-os.js index 180d869001f..afff23b2e4f 100644 --- a/test/parallel/test-os.js +++ b/test/parallel/test-os.js @@ -24,6 +24,7 @@ const common = require('../common'); const assert = require('assert'); const os = require('os'); const path = require('path'); +const { inspect } = require('util'); const is = { string: (value) => { assert.strictEqual(typeof value, 'string'); }, @@ -121,7 +122,7 @@ switch (platform) { const actual = interfaces.lo.filter(filter); const expected = [{ address: '127.0.0.1', netmask: '255.0.0.0', mac: '00:00:00:00:00:00', family: 'IPv4', - internal: true }]; + internal: true, cidr: '127.0.0.1/8' }]; assert.deepStrictEqual(actual, expected); break; } @@ -131,11 +132,31 @@ switch (platform) { const actual = interfaces['Loopback Pseudo-Interface 1'].filter(filter); const expected = [{ address: '127.0.0.1', netmask: '255.0.0.0', mac: '00:00:00:00:00:00', family: 'IPv4', - internal: true }]; + internal: true, cidr: '127.0.0.1/8' }]; assert.deepStrictEqual(actual, expected); break; } } +function flatten(arr) { + return arr.reduce( + (acc, c) => acc.concat(Array.isArray(c) ? flatten(c) : c), + [] + ); +} +const netmaskToCIDRSuffixMap = new Map(Object.entries({ + '255.0.0.0': 8, + '255.255.255.0': 24, + 'ffff:ffff:ffff:ffff::': 64, + 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff': 128 +})); +flatten(Object.values(interfaces)) + .map((v) => ({ v, mask: netmaskToCIDRSuffixMap.get(v.netmask) })) + .forEach(({ v, mask }) => { + assert.ok('cidr' in v, `"cidr" prop not found in ${inspect(v)}`); + if (mask) { + assert.strictEqual(v.cidr, `${v.address}/${mask}`); + } + }); const EOL = os.EOL; assert.ok(EOL.length > 0);