JavaScript Basics: Validate IP Literals and Match Subnets with node:net
A configuration field containing an IP address needs different handling from a hostname, a URL, or an address-and-port pair. The node:net module includes local utilities for these distinctions. You can classify literals, parse endpoint text, and check sample ranges without opening a socket.
These three independent examples were executed on Linux with Node.js v24.19.0. Use Node.js 24 for this tutorial; only that installed version was tested. They use built-in modules and assertions, with no packages or data files. They make no DNS lookups or network connections and do not change firewall settings. The output below is the observed output.
Prepare a scratch directory
You need Bash, Node.js on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep this terminal open. These commands stop the shell if setup fails; save no sources until both directory commands succeed. Save each named file as UTF-8 in the new directory. The .mjs extension selects an ES module.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Classify literals rather than guessing with a regular expression
Save this as classify-ip-literals.mjs:
import assert from 'node:assert/strict';
import { isIP, isIPv4, isIPv6 } from 'node:net';
const inputs = [
'192.0.2.8',
'2001:db8::5',
'example.com',
'192.0.2.0/24',
'192.000.2.8',
'[2001:db8::5]',
];
const families = inputs.map(input => isIP(input));
assert.deepEqual(families, [4, 6, 0, 0, 0, 0]);
assert.equal(isIPv4(inputs[0]), true);
assert.equal(isIPv6(inputs[1]), true);
assert.equal(isIP(' 192.0.2.8 '), 0);
for (let index = 0; index < inputs.length; index += 1) {
const family = families[index];
console.log(`${inputs[index]}: ${family === 0 ? 'not an IP literal' : `IPv${family}`}`);
}
console.log('surrounding spaces: rejected');Run it:
node classify-ip-literals.mjsOutput:
192.0.2.8: IPv4
2001:db8::5: IPv6
example.com: not an IP literal
192.0.2.0/24: not an IP literal
192.000.2.8: not an IP literal
[2001:db8::5]: not an IP literal
surrounding spaces: rejectedThe values 4, 6, and 0 are the classification results, not booleans or reachability tests. A hostname can be meaningful to an application and still not be an IP literal. Similarly, a CIDR suffix and IPv6 endpoint brackets belong to other input formats. Node’s isIP documentation also specifies that dotted IPv4 input must not contain leading zeroes.
This example rejects spaces instead of silently trimming them. If your configuration format permits surrounding whitespace, normalize it deliberately before classification and keep that rule consistent wherever the value is used.
2. Parse an address-and-port pair as one unit
Save this as parse-ip-endpoints.mjs:
import assert from 'node:assert/strict';
import { SocketAddress } from 'node:net';
const ipv4 = SocketAddress.parse('192.0.2.8:8080');
const ipv6 = SocketAddress.parse('[2001:0db8:0:0:0:0:0:5]:443');
assert.ok(ipv4);
assert.ok(ipv6);
assert.equal(ipv4.address, '192.0.2.8');
assert.equal(ipv4.port, 8080);
assert.equal(ipv4.family, 'ipv4');
assert.equal(ipv6.address, '2001:db8::5');
assert.equal(ipv6.port, 443);
assert.equal(ipv6.family, 'ipv6');
assert.equal(SocketAddress.parse('example.com:443'), undefined);
assert.equal(SocketAddress.parse('192.0.2.8:70000'), undefined);
for (const endpoint of [ipv4, ipv6]) {
console.log(`${endpoint.family}: address=${endpoint.address}, port=${endpoint.port}`);
}
console.log('hostname endpoint: not parsed');
console.log('port 70000: not parsed');Run it:
node parse-ip-endpoints.mjsOutput:
ipv4: address=192.0.2.8, port=8080
ipv6: address=2001:db8::5, port=443
hostname endpoint: not parsed
port 70000: not parsedSplitting on a colon would break the IPv6 input because the address itself contains colons. SocketAddress.parse handles this bracketed endpoint form and returns a structured result, or undefined on a failed parse. The example checks success before reading fields. See SocketAddress.parse.
The expanded IPv6 spelling becomes a compressed address in this result. That normalization is separate from the first example’s boolean or numeric validation. A parsed port can still be inappropriate for your application, so impose any product-specific constraints after parsing. Parsing does not try to contact the endpoint.
3. Match sample subnets and an inclusive range
Save this as match-ip-ranges.mjs:
import assert from 'node:assert/strict';
import { BlockList, isIP } from 'node:net';
const rules = new BlockList();
rules.addSubnet('192.0.2.0', 24, 'ipv4');
rules.addRange('198.51.100.10', '198.51.100.20', 'ipv4');
rules.addSubnet('2001:db8:1234::', 48, 'ipv6');
function matchesExampleRules(address) {
if (typeof address !== 'string') throw new TypeError('Expected address text');
const family = isIP(address);
if (family === 0) throw new Error('Expected an IP literal');
return rules.check(address, family === 4 ? 'ipv4' : 'ipv6');
}
const cases = [
['192.0.2.8', true],
['192.0.3.8', false],
['198.51.100.10', true],
['198.51.100.20', true],
['198.51.100.21', false],
['2001:db8:1234::5', true],
['2001:db8:1235::5', false],
['::ffff:192.0.2.8', true],
];
for (const [address, expected] of cases) {
const matched = matchesExampleRules(address);
assert.equal(matched, expected);
console.log(`${address}: ${matched}`);
}
assert.throws(() => matchesExampleRules('example.com'), /Expected an IP literal/);
assert.throws(() => rules.addSubnet('192.0.2.0', 33, 'ipv4'), RangeError);
console.log('non-IP input and invalid IPv4 prefix: rejected');Run it:
node match-ip-ranges.mjsOutput:
192.0.2.8: true
192.0.3.8: false
198.51.100.10: true
198.51.100.20: true
198.51.100.21: false
2001:db8:1234::5: true
2001:db8:1235::5: false
::ffff:192.0.2.8: true
non-IP input and invalid IPv4 prefix: rejectedThe range includes both endpoints, which the two boundary assertions verify. The helper chooses the family explicitly rather than relying on the IPv4 default. The mapped IPv6 spelling also matches the IPv4 subnet rule, so string-prefix matching would not be an adequate substitute. These behaviors are described by BlockList.
Here BlockList is only an in-memory collection queried by check. A true result means “matches one of our sample rules,” not “the operating system blocked a connection.” Nothing installs a firewall rule or attaches this list to a server. The sample ranges are fixtures, not a complete security policy.
Practical limits
Syntax, range membership, reachability, and authorization are separate questions. None of these examples resolves hostnames, verifies a remote host’s identity, or proves an address is safe to contact. They also do not provide a complete private/public address classifier. If you use address checks around real connections, account for the actual connected peer, DNS changes, proxy behavior, and the application’s threat model. Use these utilities to avoid hand-written parsing mistakes, not as a stand-alone network security boundary.
Clean up
In the same terminal, remove only the three source files, return to your starting directory, and remove the empty scratch directory:
rm -- classify-ip-literals.mjs parse-ip-endpoints.mjs match-ip-ranges.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.