Fix IPv6 literal addresses bypassing no_proxy when outbound proxy is set
What does this MR do and why?
Fix IPv6 literal addresses bypassing no_proxy when outbound proxy is set
Two bugs caused webhook test to return 500 for IPv6 targets when an outbound proxy was configured:
-
URI::Generic.use_proxy?uses a regex that splits on colons, so it cannot parse IPv6 literal addresses inno_proxy(e.g.2001:db8::1gets split and only matches"1").Fix: detect IPv6 hostnames via
Resolv::IPv6::Regexand route them through a newipv6_excluded_by_no_proxy?helper that parses all three validno_proxyformats (bare, bracketed, bracketed+port) and usesIPAddrfor comparison so compressed and expanded representations match. -
connect_patch.rbsent unbracketed IPv6 in the CONNECT tunnel request (CONNECT 2001:db8::1:8080), which is malformed per RFC 7230 and causes real proxies (Squid, Nginx) to respond with 400.Fix: bracket the address when it matches
Resolv::IPv6::Regex.
References
How to set up and validate locally
This section describes how to manually test the IPv6 proxy fix using a minimal Node.js proxy server.
Step 1: Create the proxy server
Save the following Node.js script as test-proxy.js:
NodeJS Proxy Server Code
#!/usr/bin/env node
// Minimal HTTP/HTTPS proxy for manual testing.
// No dependencies — requires only Node.js.
//
// Usage:
// node mr-test-proxy.js # listens on port 3128
// PORT=3128 node mr-test-proxy.js
const http = require('http');
const net = require('net');
const { URL } = require('url');
const PORT = parseInt(process.env.PORT || '3128', 10);
const server = http.createServer((req, res) => {
console.log(`>> ${req.method} ${req.url}`);
let target;
try { target = new URL(req.url); } catch {
res.writeHead(400); res.end('Bad Request'); return;
}
const hostname = target.hostname.replace(/^\[|\]$/g, '');
const upstream = http.request(
{ hostname, port: target.port || 80, path: target.pathname + target.search, method: req.method, headers: req.headers },
(upstream_res) => { res.writeHead(upstream_res.statusCode, upstream_res.headers); upstream_res.pipe(res); }
);
upstream.on('error', () => { res.writeHead(502); res.end('Bad Gateway'); });
req.pipe(upstream);
});
server.on('connect', (req, client, head) => {
console.log(`>> CONNECT ${req.url}`);
// Parse host:port — IPv6 must be bracketed per RFC 7230: [::1]:443
const m = req.url.match(/^\[([^\]]+)\]:(\d+)$/) || req.url.match(/^([^:]+):(\d+)$/);
if (!m) {
console.log(`!! CONNECT ${req.url} — 400 Bad Request (malformed, unbracketed IPv6?)`);
client.write('HTTP/1.1 400 Bad Request\r\n\r\n'); client.destroy(); return;
}
const [, host, port] = m;
const upstream = net.connect(parseInt(port), host, () => {
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
if (head?.length) upstream.write(head);
upstream.pipe(client); client.pipe(upstream);
console.log(`<< CONNECT ${req.url} 200 OK`);
});
upstream.on('error', (e) => { console.log(`!! ${e.message}`); client.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); client.destroy(); });
client.on('error', () => upstream.destroy());
});
server.listen(PORT, () => console.log(`Proxy listening on http://[::1]:${PORT}\n`));Step 2: Start the proxy server
node test-proxy.jsThe proxy will listen on http://[::1]:3128.
Step 3: Test with proxy (request should go through proxy)
Start a Rails console with the proxy configured:
http_proxy=http://[::1]:3128 https_proxy=http://[::1]:3128 bin/rails cMake a request to an IPv6 address:
Gitlab::HTTP.get('https://[2607:f8b0:400a:800::200e]', verify: false).bodyExpected behavior:
- With fix: Request goes through the proxy and returns the response body
- Without fix: Request returns 400 "Bad Request" (malformed IPv6 in CONNECT request)
Step 4: Test with no_proxy (request should bypass proxy)
Start a Rails console with both proxy and no_proxy configured:
http_proxy=http://[::1]:3128 https_proxy=http://[::1]:3128 no_proxy=[2607:f8b0:400a:800::200e] bin/rails cMake the same request:
Gitlab::HTTP.get('https://[2607:f8b0:400a:800::200e]', verify: false).bodyExpected behavior:
- With fix: Request bypasses the proxy and returns the response body
- Without fix: Request goes through the proxy and returns 400 "Bad Request"
Note
The proxy server simulates real proxy behavior (Nginx, Squid) by returning 400 Bad Request when it receives a malformed IPv6 address in the CONNECT tunnel request.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.