Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net: exclude ipv6 loopback addresses from server.listen #54264

39 changes: 36 additions & 3 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const {
UV_ECANCELED,
UV_ETIMEDOUT,
} = internalBinding('uv');
const { convertIpv6StringToBuffer } = internalBinding('cares_wrap');

const { Buffer } = require('buffer');
const { ShutdownWrap } = internalBinding('stream_wrap');
Expand Down Expand Up @@ -2118,19 +2119,51 @@ Server.prototype.listen = function(...args) {
throw new ERR_INVALID_ARG_VALUE('options', options);
};

function isIpv6LinkLocal(ip) {
if (!isIPv6(ip)) { return false; }

const ipv6Buffer = convertIpv6StringToBuffer(ip);
const firstByte = ipv6Buffer[0]; // The first 8 bits
const secondByte = ipv6Buffer[1]; // The next 8 bits

// The link-local prefix is `1111111010`, which in hexadecimal is `fe80`
// First 8 bits (firstByte) should be `11111110` (0xfe)
// The next 2 bits of the second byte should be `10` (0x80)

const isFirstByteCorrect = (firstByte === 0xfe); // 0b11111110 == 0xfe
const isSecondByteCorrect = (secondByte & 0xc0) === 0x80; // 0b10xxxxxx == 0x80

return isFirstByteCorrect && isSecondByteCorrect;
}

function filterOnlyValidAddress(addresses) {
// Return the first non IPV6 link-local address if present
for (const address of addresses) {
if (!isIpv6LinkLocal(address.address)) {
return address;
}
}

// Otherwise return the first address
return addresses[0];
}

function lookupAndListen(self, port, address, backlog,
exclusive, flags) {
if (dns === undefined) dns = require('dns');
const listeningId = self._listeningId;
dns.lookup(address, function doListen(err, ip, addressType) {

dns.lookup(address, { all: true }, (err, addresses) => {
if (listeningId !== self._listeningId) {
return;
}
if (err) {
self.emit('error', err);
} else {
addressType = ip ? addressType : 4;
listenInCluster(self, ip, port, addressType,
const validAddress = filterOnlyValidAddress(addresses);
const family = validAddress?.family || 4;

listenInCluster(self, validAddress.address, port, family,
pimterry marked this conversation as resolved.
Show resolved Hide resolved
backlog, undefined, exclusive, flags);
}
});
Expand Down
21 changes: 21 additions & 0 deletions src/cares_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,24 @@ void CanonicalizeIP(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(val);
}

void ConvertIpv6StringToBuffer(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
node::Utf8Value ip(isolate, args[0]);
unsigned char dst[16]; // IPv6 addresses are 128 bits (16 bytes)

if (uv_inet_pton(AF_INET6, *ip, dst) != 0) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "Invalid IPv6 address").ToLocalChecked()));
return;
}

Local<Object> buffer =
node::Buffer::Copy(
isolate, reinterpret_cast<const char*>(dst), sizeof(dst))
.ToLocalChecked();
args.GetReturnValue().Set(buffer);
}

void GetAddrInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

Expand Down Expand Up @@ -1902,6 +1920,8 @@ void Initialize(Local<Object> target,
SetMethod(context, target, "getaddrinfo", GetAddrInfo);
SetMethod(context, target, "getnameinfo", GetNameInfo);
SetMethodNoSideEffect(context, target, "canonicalizeIP", CanonicalizeIP);
SetMethodNoSideEffect(
context, target, "convertIpv6StringToBuffer", ConvertIpv6StringToBuffer);

SetMethod(context, target, "strerror", StrError);

Expand Down Expand Up @@ -1995,6 +2015,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetAddrInfo);
registry->Register(GetNameInfo);
registry->Register(CanonicalizeIP);
registry->Register(ConvertIpv6StringToBuffer);
registry->Register(StrError);
registry->Register(ChannelWrap::New);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
const common = require('../common');
const net = require('net');
// Process should exit because it does not create a real TCP server.
// Paas localhost to ensure create TCP handle asynchronously because It causes DNS resolution.
// Pass localhost to ensure create TCP handle asynchronously because it causes DNS resolution.
net.createServer().listen(0, 'localhost', common.mustNotCall()).close();
131 changes: 131 additions & 0 deletions test/sequential/test-net-server-listen-ipv6-link-local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const net = require('net');
const dns = require('dns');
const { mock } = require('node:test');

if (!common.hasIPv6) {
common.printSkipMessage('IPv6 support is required for this test');
return;
}

// Test on IPv6 Server, dns.lookup throws an error
{
mock.method(dns, 'lookup', (hostname, options, callback) => {
callback(new Error('Mocked error'));
});
const host = 'ipv6_link_local';

const server = net.createServer();

server.on('error', common.mustCall((e) => {
assert.strictEqual(e.message, 'Mocked error');
}));

server.listen(common.PORT + 2, host);
}


// Test on IPv6 Server, server.listen throws an error
{
mock.method(dns, 'lookup', (hostname, options, callback) => {
if (hostname === 'ipv6_link_local') {
callback(null, [{ address: 'fe80::1', family: 6 }]);
} else {
dns.lookup.wrappedMethod(hostname, options, callback);
}
});
const host = 'ipv6_link_local';

const server = net.createServer();

server.on('error', common.mustCall((e) => {
assert.strictEqual(e.address, 'fe80::1');
assert.strictEqual(e.syscall, 'listen');
}));

server.listen(common.PORT + 2, host);
}

// Test on IPv6 Server, picks 127.0.0.1 between that and a bunch of link-local addresses
{

mock.method(dns, 'lookup', (hostname, options, callback) => {
if (hostname === 'ipv6_link_local_with_many_entries') {
callback(null, [
{ address: 'fe80::1', family: 6 },
{ address: 'fe80::abcd:1234', family: 6 },
{ address: 'fe80::1ff:fe23:4567:890a', family: 6 },
{ address: 'fe80::200:5aee:feaa:20a2', family: 6 },
{ address: 'fe80::f2de:f1ff:fe2b:3c4b', family: 6 },
{ address: 'fe81::1', family: 6 },
{ address: 'fe82::abcd:1234', family: 6 },
{ address: 'fe83::1ff:fe23:4567:890a', family: 6 },
{ address: 'fe84::200:5aee:feaa:20a2', family: 6 },
{ address: 'fe85::f2de:f1ff:fe2b:3c4b', family: 6 },
{ address: 'fe86::1', family: 6 },
{ address: 'fe87::abcd:1234', family: 6 },
{ address: 'fe88::1ff:fe23:4567:890a', family: 6 },
{ address: 'fe89::200:5aee:feaa:20a2', family: 6 },
{ address: 'fe8a::f2de:f1ff:fe2b:3c4b', family: 6 },
{ address: 'fe8b::1', family: 6 },
{ address: 'fe8c::abcd:1234', family: 6 },
{ address: 'fe8d::1ff:fe23:4567:890a', family: 6 },
{ address: 'fe8e::200:5aee:feaa:20a2', family: 6 },
{ address: 'fe8f::f2de:f1ff:fe2b:3c4b', family: 6 },
{ address: 'fea0::1', family: 6 },
{ address: 'febf::abcd:1234', family: 6 },
{ address: 'febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff', family: 6 },
{ address: '127.0.0.1', family: 4 },
]);
} else {
dns.lookup.wrappedMethod(hostname, options, callback);
}
});

const host = 'ipv6_link_local_with_many_entries';

const server = net.createServer();

server.on('error', common.mustNotCall());

server.listen(common.PORT + 3, host, common.mustCall(() => {
const address = server.address();
assert.strictEqual(address.address, '127.0.0.1');
assert.strictEqual(address.port, common.PORT + 3);
assert.strictEqual(address.family, 'IPv4');
server.close();
}));
}


// Test on IPv6 Server, picks ::1 because the other address is a link-local address
{

const host = 'ipv6_link_local_with_double_entry';
const validIpv6Address = '::1';

mock.method(dns, 'lookup', (hostname, options, callback) => {
if (hostname === 'ipv6_link_local_with_double_entry') {
callback(null, [
{ address: 'fe80::1', family: 6 },
{ address: validIpv6Address, family: 6 },
]);
} else {
dns.lookup.wrappedMethod(hostname, options, callback);
}
});

const server = net.createServer();

server.on('error', common.mustNotCall());

server.listen(common.PORT + 4, host, common.mustCall(() => {
const address = server.address();
assert.strictEqual(address.address, validIpv6Address);
assert.strictEqual(address.port, common.PORT + 4);
assert.strictEqual(address.family, 'IPv6');
server.close();
}));
}
Loading