Skip to content
Merged
39 changes: 36 additions & 3 deletions lib/net.js
Original file line numberDiff line numberDiff 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 DownExpand 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,
backlog, undefined, exclusive, flags);
}
});
Expand Down
21 changes: 21 additions & 0 deletions src/cares_wrap.cc
Original file line numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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();
}));
}