Skip to content

Commit 554294e

Browse files
authored
Change default host to be IPv6-friendly (#4357)
* Change default host from 0.0.0.0 to node's ipv6-friendly default (:: or 0.0.0.0) * Normalize IPv4-mapped IPv6 addresses in request.info.remoteAddress * Document IPv6-friendly default host in API docs
1 parent a034673 commit 554294e

File tree

10 files changed

+143
-53
lines changed

10 files changed

+143
-53
lines changed

API.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ All options are optionals.
2626

2727
#### <a name="server.options.address" /> `server.options.address`
2828

29-
Default value: `'0.0.0.0'` (all available network interfaces).
29+
Default value: `'::'` if IPv6 is available, otherwise `'0.0.0.0'` (i.e. all available network interfaces).
3030

3131
Sets the hostname or IP address the server will listen on. If not configured, defaults to
3232
[`host`](#server.options.host) if present, otherwise to all available network interfaces. Set to
33-
`'127.0.0.1'` or `'localhost'` to restrict the server to only those coming from the same host.
33+
`'127.0.0.1'`, `'::1'`, or `'localhost'` to restrict the server to only those coming from the same host.
3434

3535
#### <a name="server.options.app" /> `server.options.app`
3636

lib/core.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,8 @@ exports = module.exports = internals.Core = class {
336336
this.listener.listen(this.settings.port, finalize);
337337
}
338338
else {
339-
const address = this.settings.address || this.settings.host || '0.0.0.0';
339+
// Default is the unspecified address, :: if IPv6 is available or otherwise the IPv4 address 0.0.0.0
340+
const address = this.settings.address || this.settings.host || null;
340341
this.listener.listen(this.settings.port, address, finalize);
341342
}
342343
});

lib/request.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,15 @@ internals.Info = class {
656656
get remoteAddress() {
657657

658658
if (!this._remoteAddress) {
659-
this._remoteAddress = this._request.raw.req.socket.remoteAddress;
659+
const ipv6Prefix = '::ffff:';
660+
const socketAddress = this._request.raw.req.socket.remoteAddress;
661+
if (socketAddress.startsWith(ipv6Prefix) && socketAddress.includes('.', ipv6Prefix.length)) {
662+
// Normalize IPv4-mapped IPv6 address, e.g. ::ffff:127.0.0.1 -> 127.0.0.1
663+
this._remoteAddress = socketAddress.slice(ipv6Prefix.length);
664+
}
665+
else {
666+
this._remoteAddress = socketAddress;
667+
}
660668
}
661669

662670
return this._remoteAddress;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
},
4545
"devDependencies": {
4646
"@hapi/code": "9.0.0-beta.0",
47-
"@hapi/eslint-plugin": "*",
47+
"@hapi/eslint-plugin": "^5.0.0",
4848
"@hapi/inert": "^6.0.2",
4949
"@hapi/joi-legacy-test": "npm:@hapi/joi@^15.0.0",
5050
"@hapi/lab": "25.0.0-beta.0",

test/common.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use strict';
22

33
const ChildProcess = require('child_process');
4-
const Dns = require('dns');
4+
const Http = require('http');
5+
const Net = require('net');
56

67
const internals = {};
78

@@ -17,11 +18,15 @@ internals.hasLsof = () => {
1718
return true;
1819
};
1920

20-
exports.hasLsof = internals.hasLsof();
21+
internals.hasIPv6 = () => {
2122

22-
exports.setDefaultDnsOrder = () => {
23-
// Resolve localhost to ipv4 address on node v17
24-
if (Dns.setDefaultResultOrder) {
25-
Dns.setDefaultResultOrder('ipv4first');
26-
}
23+
const server = Http.createServer().listen();
24+
const { address } = server.address();
25+
server.close();
26+
27+
return Net.isIPv6(address);
2728
};
29+
30+
exports.hasLsof = internals.hasLsof();
31+
32+
exports.hasIPv6 = internals.hasIPv6();

test/core.js

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const ChildProcess = require('child_process');
4+
const Events = require('events');
45
const Fs = require('fs');
56
const Http = require('http');
67
const Https = require('https');
@@ -27,14 +28,12 @@ const Common = require('./common');
2728
const internals = {};
2829

2930

30-
const { describe, it, before } = exports.lab = Lab.script();
31+
const { describe, it } = exports.lab = Lab.script();
3132
const expect = Code.expect;
3233

3334

3435
describe('Core', () => {
3536

36-
before(Common.setDefaultDnsOrder);
37-
3837
it('sets app settings defaults', () => {
3938

4039
const server = Hapi.server();
@@ -99,38 +98,58 @@ describe('Core', () => {
9998
}).to.throw('Cannot specify port when autoListen is false');
10099
});
101100

102-
it('defaults address to 0.0.0.0 or :: when no host is provided', async () => {
101+
it('defaults address to 0.0.0.0 or :: when no host is provided', async (flags) => {
103102

104103
const server = Hapi.server();
105104
await server.start();
105+
flags.onCleanup = () => server.stop();
106106

107-
let expectedBoundAddress = '0.0.0.0';
108-
if (Net.isIPv6(server.listener.address().address)) {
109-
expectedBoundAddress = '::';
110-
}
107+
const expectedBoundAddress = Common.hasIPv6 ? '::' : '0.0.0.0';
111108

112109
expect(server.info.address).to.equal(expectedBoundAddress);
113-
await server.stop();
114110
});
115111

116-
it('uses address when present instead of host', async () => {
112+
it('is accessible on localhost when using default host', async (flags) => {
113+
// With hapi v20 this would fail on ipv6 machines on node v18+ due to DNS resolution changes in node (see nodejs/node#40537).
114+
// To address this in hapi v21 we bind to :: if available, otherwise the former default of 0.0.0.0.
115+
116+
const server = Hapi.server();
117+
server.route({ method: 'get', path: '/', handler: () => 'ok' });
118+
119+
await server.start();
120+
flags.onCleanup = () => server.stop();
121+
122+
const req = Http.get(`http://localhost:${server.info.port}`);
123+
const [res] = await Events.once(req, 'response');
124+
125+
let result = '';
126+
for await (const chunk of res) {
127+
result += chunk.toString();
128+
}
129+
130+
expect(result).to.equal('ok');
131+
});
132+
133+
it('uses address when present instead of host', async (flags) => {
117134

118135
const server = Hapi.server({ host: 'no.such.domain.hapi', address: 'localhost' });
119136
await server.start();
137+
flags.onCleanup = () => server.stop();
138+
120139
expect(server.info.host).to.equal('no.such.domain.hapi');
121-
expect(server.info.address).to.equal('127.0.0.1');
122-
await server.stop();
140+
expect(server.info.address).to.match(/^127\.0\.0\.1|::1$/); // ::1 on node v18 with ipv6 support
123141
});
124142

125-
it('uses uri when present instead of host and port', async () => {
143+
it('uses uri when present instead of host and port', async (flags) => {
126144

127145
const server = Hapi.server({ host: 'no.such.domain.hapi', address: 'localhost', uri: 'http://uri.example.com:8080' });
128146
expect(server.info.uri).to.equal('http://uri.example.com:8080');
129147
await server.start();
148+
flags.onCleanup = () => server.stop();
149+
130150
expect(server.info.host).to.equal('no.such.domain.hapi');
131-
expect(server.info.address).to.equal('127.0.0.1');
151+
expect(server.info.address).to.match(/^127\.0\.0\.1|::1$/); // ::1 on node v18 with ipv6 support
132152
expect(server.info.uri).to.equal('http://uri.example.com:8080');
133-
await server.stop();
134153
});
135154

136155
it('throws on uri ending with /', () => {

test/payload.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,15 @@ const Hoek = require('@hapi/hoek');
1111
const Lab = require('@hapi/lab');
1212
const Wreck = require('@hapi/wreck');
1313

14-
const Common = require('./common');
15-
1614
const internals = {};
1715

1816

19-
const { describe, it, before } = exports.lab = Lab.script();
17+
const { describe, it } = exports.lab = Lab.script();
2018
const expect = Code.expect;
2119

2220

2321
describe('Payload', () => {
2422

25-
before(Common.setDefaultDnsOrder);
26-
2723
it('sets payload', async () => {
2824

2925
const payload = '{"x":"1","y":"2","z":"3"}';

test/request.js

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,12 @@ const Common = require('./common');
2020
const internals = {};
2121

2222

23-
const { describe, it, before } = exports.lab = Lab.script();
23+
const { describe, it } = exports.lab = Lab.script();
2424
const expect = Code.expect;
2525

2626

2727
describe('Request.Generator', () => {
2828

29-
before(Common.setDefaultDnsOrder);
30-
3129
it('decorates request multiple times', async () => {
3230

3331
const server = Hapi.server();
@@ -125,35 +123,103 @@ describe('Request.Generator', () => {
125123

126124
describe('Request', () => {
127125

128-
it('sets client address', async () => {
126+
it('sets client address (default)', async (flags) => {
129127

130128
const server = Hapi.server();
131129

132130
const handler = (request) => {
133131

134-
let expectedClientAddress = '127.0.0.1';
135-
if (Net.isIPv6(server.listener.address().address)) {
136-
expectedClientAddress = '::ffff:127.0.0.1';
132+
// Call twice to reuse cached values
133+
134+
if (Common.hasIPv6) {
135+
// 127.0.0.1 on node v14 and v16, ::1 on node v18 since DNS resolved to IPv6.
136+
expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/);
137+
expect(request.info.remoteAddress).to.match(/^127\.0\.0\.1|::1$/);
138+
}
139+
else {
140+
expect(request.info.remoteAddress).to.equal('127.0.0.1');
141+
expect(request.info.remoteAddress).to.equal('127.0.0.1');
137142
}
138143

139-
expect(request.info.remoteAddress).to.equal(expectedClientAddress);
140144
expect(request.info.remotePort).to.be.above(0);
141-
142-
// Call twice to reuse cached values
143-
144-
expect(request.info.remoteAddress).to.equal(expectedClientAddress);
145145
expect(request.info.remotePort).to.be.above(0);
146146

147147
return 'ok';
148148
};
149149

150-
server.route({ method: 'GET', path: '/', handler });
150+
server.route({ method: 'get', path: '/', handler });
151151

152152
await server.start();
153+
flags.onCleanup = () => server.stop();
153154

154155
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
155156
expect(payload.toString()).to.equal('ok');
156-
await server.stop();
157+
});
158+
159+
it('sets client address (ipv4)', async (flags) => {
160+
161+
const server = Hapi.server();
162+
163+
const handler = (request) => {
164+
165+
Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
166+
value: '100.100.100.100'
167+
});
168+
169+
return request.info.remoteAddress;
170+
};
171+
172+
server.route({ method: 'get', path: '/', handler });
173+
174+
await server.start();
175+
flags.onCleanup = () => server.stop();
176+
177+
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
178+
expect(payload.toString()).to.equal('100.100.100.100');
179+
});
180+
181+
it('sets client address (ipv6)', async (flags) => {
182+
183+
const server = Hapi.server();
184+
185+
const handler = (request) => {
186+
187+
Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
188+
value: '::ffff:0:0:0:0:1'
189+
});
190+
191+
return request.info.remoteAddress;
192+
};
193+
194+
server.route({ method: 'get', path: '/', handler });
195+
196+
await server.start();
197+
flags.onCleanup = () => server.stop();
198+
199+
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
200+
expect(payload.toString()).to.equal('::ffff:0:0:0:0:1');
201+
});
202+
203+
it('sets client address (ipv4-mapped ipv6)', async (flags) => {
204+
205+
const server = Hapi.server();
206+
207+
const handler = (request) => {
208+
209+
Object.defineProperty(request.raw.req.socket, 'remoteAddress', {
210+
value: '::ffff:100.100.100.100'
211+
});
212+
213+
return request.info.remoteAddress;
214+
};
215+
216+
server.route({ method: 'get', path: '/', handler });
217+
218+
await server.start();
219+
flags.onCleanup = () => server.stop();
220+
221+
const { payload } = await Wreck.get('http://localhost:' + server.info.port);
222+
expect(payload.toString()).to.equal('100.100.100.100');
157223
});
158224

159225
it('sets port to nothing when not available', async () => {

test/server.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,18 @@ const Lab = require('@hapi/lab');
1414
const Vision = require('@hapi/vision');
1515
const Wreck = require('@hapi/wreck');
1616

17-
const Common = require('./common');
1817
const Pkg = require('../package.json');
1918

2019

2120
const internals = {};
2221

2322

24-
const { describe, it, before } = exports.lab = Lab.script();
23+
const { describe, it } = exports.lab = Lab.script();
2524
const expect = Code.expect;
2625

2726

2827
describe('Server', () => {
2928

30-
before(Common.setDefaultDnsOrder);
31-
3229
describe('auth', () => {
3330

3431
it('adds auth strategy via plugin', async () => {

test/transmit.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ const Common = require('./common');
2424
const internals = {};
2525

2626

27-
const { describe, it, before } = exports.lab = Lab.script();
27+
const { describe, it } = exports.lab = Lab.script();
2828
const expect = Code.expect;
2929

3030

3131
describe('transmission', () => {
3232

33-
before(Common.setDefaultDnsOrder);
34-
3533
describe('send()', () => {
3634

3735
it('handlers invalid headers in error', async () => {

0 commit comments

Comments
 (0)