From 09a474fb8030c06b2bf86b96494721f9f172ee56 Mon Sep 17 00:00:00 2001 From: haramjeong <04harams77@gmail.com> Date: Sun, 30 Aug 2026 21:19:10 +0900 Subject: [PATCH 1/2] http: add IPv6 range support for NO_PROXY Signed-off-by: haramjeong <04harams77@gmail.com> --- doc/api/http.md | 2 + lib/internal/http.js | 34 +++-- .../test-http-proxy-request-no-proxy-ipv6.mjs | 116 ++++++++++++++++++ 3 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs diff --git a/doc/api/http.md b/doc/api/http.md index 4c1c5ad1321a..97acbc23730b 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -4620,6 +4620,8 @@ The `NO_PROXY` environment variable supports several formats: * `*.example.com` - Wildcard domain match * `192.168.1.100` - Exact IP address match * `192.168.1.1-192.168.1.100` - IP address range +* `::1` or `[::1]` - Exact IPv6 address match +* `::1-::100` - IPv6 address range * `example.com:8080` - Hostname with specific port Multiple entries should be separated by commas. diff --git a/lib/internal/http.js b/lib/internal/http.js index 304ef04d6638..d92a2a2971fe 100644 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -19,7 +19,8 @@ const { const { URL } = require('internal/url'); const { Buffer } = require('buffer'); -const { isIPv4 } = require('internal/net'); +const { BlockList } = require('internal/blocklist'); +const { isIPv4, isIPv6 } = require('internal/net'); const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes; let utcCache; @@ -70,6 +71,11 @@ function ipToInt(ip) { return result >>> 0; } +function removeIPv6Brackets(host) { + return host[0] === '[' && host[host.length - 1] === ']' ? + host.slice(1, -1) : host; +} + // There are two factors in play when proxying the request: // 1. What the request protocol is, that is, whether users are sending it via // http.request or https.request, or whether they are sending @@ -155,13 +161,18 @@ class ProxyConfig { } const host = hostname.toLowerCase(); - const hostWithPort = port ? `${host}:${port}` : host; + const normalizedHost = removeIPv6Brackets(host); + const hostWithPort = port ? + (isIPv6(normalizedHost) ? `[${normalizedHost}]:${port}` : `${host}:${port}`) : + host; for (let i = 0; i < bypassList.length; i++) { const entry = bypassList[i]; + const normalizedEntry = removeIPv6Brackets(entry); if (entry === '*') return false; // * bypasses all hosts. - if (entry === host || entry === hostWithPort) return false; // Matching host and host:port + // Matching host and host:port. + if (entry === host || entry === hostWithPort || normalizedEntry === normalizedHost) return false; // Follow curl's behavior: strip leading dot before matching suffixes. if (entry[0] === '.') { @@ -173,16 +184,23 @@ class ProxyConfig { if (entry.startsWith('*.') && host.endsWith(entry.substring(1))) return false; // Handle IP ranges (simple format like 192.168.1.0-192.168.1.255) - // TODO(joyeecheung): support IPv6. - if (entry.includes('-') && isIPv4(host)) { + if (entry.includes('-')) { let { 0: startIP, 1: endIP } = entry.split('-'); - startIP = startIP.trim(); - endIP = endIP.trim(); - if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP)) { + startIP = removeIPv6Brackets(startIP.trim()); + endIP = removeIPv6Brackets(endIP.trim()); + if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP) && isIPv4(normalizedHost)) { const hostInt = ipToInt(host); const startInt = ipToInt(startIP); const endInt = ipToInt(endIP); if (hostInt >= startInt && hostInt <= endInt) return false; + } else if (startIP && endIP && isIPv6(startIP) && isIPv6(endIP) && isIPv6(normalizedHost)) { + const blockList = new BlockList(); + try { + blockList.addRange(startIP, endIP, 'ipv6'); + } catch { + continue; + } + if (blockList.check(normalizedHost, 'ipv6')) return false; } } diff --git a/test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs b/test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs new file mode 100644 index 000000000000..a6db3f90081d --- /dev/null +++ b/test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs @@ -0,0 +1,116 @@ +// This tests that NO_PROXY environment variable supports IPv6 ranges. + +import * as common from '../common/index.mjs'; +import assert from 'node:assert'; +import { once } from 'events'; +import http from 'node:http'; +import { runProxiedRequest } from '../common/proxy-server.js'; + +if (!common.hasIPv6) { + common.skip('missing IPv6 support'); +} + +// Start a server to process the final request. +const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello IPv6\n'); +}, 3)); +server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); +server.listen(0, '::1'); +await once(server, 'listening'); + +// Start a proxy server that should be used only when NO_PROXY does not match. +const proxy = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Proxied Hello IPv6\n'); +}, 2)); +proxy.listen(0, '::1'); +await once(proxy, 'listening'); + +// Test NO_PROXY with a bracketed exact IPv6 address. +{ + const { code, signal, stderr, stdout } = await runProxiedRequest({ + NODE_USE_ENV_PROXY: 1, + REQUEST_URL: `http://[::1]:${server.address().port}/test`, + HTTP_PROXY: `http://[::1]:${proxy.address().port}`, + NO_PROXY: '[::1]', + }); + + // The request should succeed and bypass proxy + assert.match(stdout, /Status Code: 200/); + assert.match(stdout, /Hello IPv6/); + assert.strictEqual(stderr.trim(), ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +} + +// Test NO_PROXY with an IPv6 host and port. +{ + const { code, signal, stderr, stdout } = await runProxiedRequest({ + NODE_USE_ENV_PROXY: 1, + REQUEST_URL: `http://[::1]:${server.address().port}/test`, + HTTP_PROXY: `http://[::1]:${proxy.address().port}`, + NO_PROXY: `[::1]:${server.address().port}`, + }); + + // The request should succeed and bypass proxy + assert.match(stdout, /Status Code: 200/); + assert.match(stdout, /Hello IPv6/); + assert.strictEqual(stderr.trim(), ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +} + +// Test NO_PROXY with IPv6 range (::1-::100 includes ::1). +{ + const { code, signal, stderr, stdout } = await runProxiedRequest({ + NODE_USE_ENV_PROXY: 1, + REQUEST_URL: `http://[::1]:${server.address().port}/test`, + HTTP_PROXY: `http://[::1]:${proxy.address().port}`, + NO_PROXY: '::1-::100', + }); + + // The request should succeed and bypass proxy + assert.match(stdout, /Status Code: 200/); + assert.match(stdout, /Hello IPv6/); + assert.strictEqual(stderr.trim(), ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +} + +// Test NO_PROXY with an IPv6 address outside the range. +{ + const { code, signal, stderr, stdout } = await runProxiedRequest({ + NODE_USE_ENV_PROXY: 1, + REQUEST_URL: `http://[::1]:${server.address().port}/test`, + HTTP_PROXY: `http://[::1]:${proxy.address().port}`, + NO_PROXY: '::50-::100', + }); + + // The request should be proxied + assert.match(stdout, /Status Code: 200/); + assert.match(stdout, /Proxied Hello IPv6/); + assert.strictEqual(stderr.trim(), ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +} + +// Test NO_PROXY with an invalid IPv6 range. +{ + const { code, signal, stderr, stdout } = await runProxiedRequest({ + NODE_USE_ENV_PROXY: 1, + REQUEST_URL: `http://[::1]:${server.address().port}/test`, + HTTP_PROXY: `http://[::1]:${proxy.address().port}`, + NO_PROXY: '::100-::1', + }); + + // The request should be proxied + assert.match(stdout, /Status Code: 200/); + assert.match(stdout, /Proxied Hello IPv6/); + assert.strictEqual(stderr.trim(), ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); +} + +proxy.close(); +server.close(); From 9a1159990722b1869a5dfa5137187a85bf90b24b Mon Sep 17 00:00:00 2001 From: haramjeong <04harams77@gmail.com> Date: Mon, 7 Sep 2026 13:34:15 +0900 Subject: [PATCH 2/2] ci: re-run tests Signed-off-by: haramjeong <04harams77@gmail.com>