Skip to content

Commit 1e01bdc

Browse files
puskin94RafaelGSS
authored andcommitted
net: exclude ipv6 loopback addresses from server.listen
Fixes: #51732 PR-URL: #54264 Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent fffc300 commit 1e01bdc

4 files changed

+189
-4
lines changed

lib/net.js

+36-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const {
6262
UV_ECANCELED,
6363
UV_ETIMEDOUT,
6464
} = internalBinding('uv');
65+
const { convertIpv6StringToBuffer } = internalBinding('cares_wrap');
6566

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

2122+
function isIpv6LinkLocal(ip) {
2123+
if (!isIPv6(ip)) { return false; }
2124+
2125+
const ipv6Buffer = convertIpv6StringToBuffer(ip);
2126+
const firstByte = ipv6Buffer[0]; // The first 8 bits
2127+
const secondByte = ipv6Buffer[1]; // The next 8 bits
2128+
2129+
// The link-local prefix is `1111111010`, which in hexadecimal is `fe80`
2130+
// First 8 bits (firstByte) should be `11111110` (0xfe)
2131+
// The next 2 bits of the second byte should be `10` (0x80)
2132+
2133+
const isFirstByteCorrect = (firstByte === 0xfe); // 0b11111110 == 0xfe
2134+
const isSecondByteCorrect = (secondByte & 0xc0) === 0x80; // 0b10xxxxxx == 0x80
2135+
2136+
return isFirstByteCorrect && isSecondByteCorrect;
2137+
}
2138+
2139+
function filterOnlyValidAddress(addresses) {
2140+
// Return the first non IPV6 link-local address if present
2141+
for (const address of addresses) {
2142+
if (!isIpv6LinkLocal(address.address)) {
2143+
return address;
2144+
}
2145+
}
2146+
2147+
// Otherwise return the first address
2148+
return addresses[0];
2149+
}
2150+
21212151
function lookupAndListen(self, port, address, backlog,
21222152
exclusive, flags) {
21232153
if (dns === undefined) dns = require('dns');
21242154
const listeningId = self._listeningId;
2125-
dns.lookup(address, function doListen(err, ip, addressType) {
2155+
2156+
dns.lookup(address, { all: true }, (err, addresses) => {
21262157
if (listeningId !== self._listeningId) {
21272158
return;
21282159
}
21292160
if (err) {
21302161
self.emit('error', err);
21312162
} else {
2132-
addressType = ip ? addressType : 4;
2133-
listenInCluster(self, ip, port, addressType,
2163+
const validAddress = filterOnlyValidAddress(addresses);
2164+
const family = validAddress?.family || 4;
2165+
2166+
listenInCluster(self, validAddress.address, port, family,
21342167
backlog, undefined, exclusive, flags);
21352168
}
21362169
});

src/cares_wrap.cc

+21
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,24 @@ void CanonicalizeIP(const FunctionCallbackInfo<Value>& args) {
15661566
args.GetReturnValue().Set(val);
15671567
}
15681568

1569+
void ConvertIpv6StringToBuffer(const FunctionCallbackInfo<Value>& args) {
1570+
Isolate* isolate = args.GetIsolate();
1571+
node::Utf8Value ip(isolate, args[0]);
1572+
unsigned char dst[16]; // IPv6 addresses are 128 bits (16 bytes)
1573+
1574+
if (uv_inet_pton(AF_INET6, *ip, dst) != 0) {
1575+
isolate->ThrowException(v8::Exception::Error(
1576+
String::NewFromUtf8(isolate, "Invalid IPv6 address").ToLocalChecked()));
1577+
return;
1578+
}
1579+
1580+
Local<Object> buffer =
1581+
node::Buffer::Copy(
1582+
isolate, reinterpret_cast<const char*>(dst), sizeof(dst))
1583+
.ToLocalChecked();
1584+
args.GetReturnValue().Set(buffer);
1585+
}
1586+
15691587
void GetAddrInfo(const FunctionCallbackInfo<Value>& args) {
15701588
Environment* env = Environment::GetCurrent(args);
15711589

@@ -1902,6 +1920,8 @@ void Initialize(Local<Object> target,
19021920
SetMethod(context, target, "getaddrinfo", GetAddrInfo);
19031921
SetMethod(context, target, "getnameinfo", GetNameInfo);
19041922
SetMethodNoSideEffect(context, target, "canonicalizeIP", CanonicalizeIP);
1923+
SetMethodNoSideEffect(
1924+
context, target, "convertIpv6StringToBuffer", ConvertIpv6StringToBuffer);
19051925

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

@@ -1995,6 +2015,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
19952015
registry->Register(GetAddrInfo);
19962016
registry->Register(GetNameInfo);
19972017
registry->Register(CanonicalizeIP);
2018+
registry->Register(ConvertIpv6StringToBuffer);
19982019
registry->Register(StrError);
19992020
registry->Register(ChannelWrap::New);
20002021

test/parallel/test-net-server-close-before-calling-lookup-callback.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
const common = require('../common');
44
const net = require('net');
55
// Process should exit because it does not create a real TCP server.
6-
// Paas localhost to ensure create TCP handle asynchronously because It causes DNS resolution.
6+
// Pass localhost to ensure create TCP handle asynchronously because it causes DNS resolution.
77
net.createServer().listen(0, 'localhost', common.mustNotCall()).close();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const net = require('net');
5+
const dns = require('dns');
6+
const { mock } = require('node:test');
7+
8+
if (!common.hasIPv6) {
9+
common.printSkipMessage('IPv6 support is required for this test');
10+
return;
11+
}
12+
13+
// Test on IPv6 Server, dns.lookup throws an error
14+
{
15+
mock.method(dns, 'lookup', (hostname, options, callback) => {
16+
callback(new Error('Mocked error'));
17+
});
18+
const host = 'ipv6_link_local';
19+
20+
const server = net.createServer();
21+
22+
server.on('error', common.mustCall((e) => {
23+
assert.strictEqual(e.message, 'Mocked error');
24+
}));
25+
26+
server.listen(common.PORT + 2, host);
27+
}
28+
29+
30+
// Test on IPv6 Server, server.listen throws an error
31+
{
32+
mock.method(dns, 'lookup', (hostname, options, callback) => {
33+
if (hostname === 'ipv6_link_local') {
34+
callback(null, [{ address: 'fe80::1', family: 6 }]);
35+
} else {
36+
dns.lookup.wrappedMethod(hostname, options, callback);
37+
}
38+
});
39+
const host = 'ipv6_link_local';
40+
41+
const server = net.createServer();
42+
43+
server.on('error', common.mustCall((e) => {
44+
assert.strictEqual(e.address, 'fe80::1');
45+
assert.strictEqual(e.syscall, 'listen');
46+
}));
47+
48+
server.listen(common.PORT + 2, host);
49+
}
50+
51+
// Test on IPv6 Server, picks 127.0.0.1 between that and a bunch of link-local addresses
52+
{
53+
54+
mock.method(dns, 'lookup', (hostname, options, callback) => {
55+
if (hostname === 'ipv6_link_local_with_many_entries') {
56+
callback(null, [
57+
{ address: 'fe80::1', family: 6 },
58+
{ address: 'fe80::abcd:1234', family: 6 },
59+
{ address: 'fe80::1ff:fe23:4567:890a', family: 6 },
60+
{ address: 'fe80::200:5aee:feaa:20a2', family: 6 },
61+
{ address: 'fe80::f2de:f1ff:fe2b:3c4b', family: 6 },
62+
{ address: 'fe81::1', family: 6 },
63+
{ address: 'fe82::abcd:1234', family: 6 },
64+
{ address: 'fe83::1ff:fe23:4567:890a', family: 6 },
65+
{ address: 'fe84::200:5aee:feaa:20a2', family: 6 },
66+
{ address: 'fe85::f2de:f1ff:fe2b:3c4b', family: 6 },
67+
{ address: 'fe86::1', family: 6 },
68+
{ address: 'fe87::abcd:1234', family: 6 },
69+
{ address: 'fe88::1ff:fe23:4567:890a', family: 6 },
70+
{ address: 'fe89::200:5aee:feaa:20a2', family: 6 },
71+
{ address: 'fe8a::f2de:f1ff:fe2b:3c4b', family: 6 },
72+
{ address: 'fe8b::1', family: 6 },
73+
{ address: 'fe8c::abcd:1234', family: 6 },
74+
{ address: 'fe8d::1ff:fe23:4567:890a', family: 6 },
75+
{ address: 'fe8e::200:5aee:feaa:20a2', family: 6 },
76+
{ address: 'fe8f::f2de:f1ff:fe2b:3c4b', family: 6 },
77+
{ address: 'fea0::1', family: 6 },
78+
{ address: 'febf::abcd:1234', family: 6 },
79+
{ address: 'febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff', family: 6 },
80+
{ address: '127.0.0.1', family: 4 },
81+
]);
82+
} else {
83+
dns.lookup.wrappedMethod(hostname, options, callback);
84+
}
85+
});
86+
87+
const host = 'ipv6_link_local_with_many_entries';
88+
89+
const server = net.createServer();
90+
91+
server.on('error', common.mustNotCall());
92+
93+
server.listen(common.PORT + 3, host, common.mustCall(() => {
94+
const address = server.address();
95+
assert.strictEqual(address.address, '127.0.0.1');
96+
assert.strictEqual(address.port, common.PORT + 3);
97+
assert.strictEqual(address.family, 'IPv4');
98+
server.close();
99+
}));
100+
}
101+
102+
103+
// Test on IPv6 Server, picks ::1 because the other address is a link-local address
104+
{
105+
106+
const host = 'ipv6_link_local_with_double_entry';
107+
const validIpv6Address = '::1';
108+
109+
mock.method(dns, 'lookup', (hostname, options, callback) => {
110+
if (hostname === 'ipv6_link_local_with_double_entry') {
111+
callback(null, [
112+
{ address: 'fe80::1', family: 6 },
113+
{ address: validIpv6Address, family: 6 },
114+
]);
115+
} else {
116+
dns.lookup.wrappedMethod(hostname, options, callback);
117+
}
118+
});
119+
120+
const server = net.createServer();
121+
122+
server.on('error', common.mustNotCall());
123+
124+
server.listen(common.PORT + 4, host, common.mustCall(() => {
125+
const address = server.address();
126+
assert.strictEqual(address.address, validIpv6Address);
127+
assert.strictEqual(address.port, common.PORT + 4);
128+
assert.strictEqual(address.family, 'IPv6');
129+
server.close();
130+
}));
131+
}

0 commit comments

Comments
 (0)