From ca07343b78f3e05324d4b2b2b4d70efae8526dc1 Mon Sep 17 00:00:00 2001 From: Malak El Kouri Date: Mon, 6 Jan 2025 18:33:37 +0100 Subject: [PATCH 1/5] fix: support CIDR blocks in no_proxy env variable --- doc/environment_variables.md | 3 +- packages/grpc-js/src/http_proxy.ts | 60 ++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/doc/environment_variables.md b/doc/environment_variables.md index 8dafefa8..1259332e 100644 --- a/doc/environment_variables.md +++ b/doc/environment_variables.md @@ -14,6 +14,7 @@ can be set. A comma separated list of hostnames to connect to without using a proxy even if a proxy is set. These variables are checked in order, and the first one that has a value is used. + The `no_proxy` environment variable could be a comma-separated list of hostnames, IP addresses, or CIDR blocks (no_proxy=example.com,192.168.0.1,192.168.0.0/16). * GRPC_SSL_CIPHER_SUITES A colon separated list of cipher suites to use with OpenSSL @@ -66,4 +67,4 @@ can be set. * GRPC_NODE_USE_ALTERNATIVE_RESOLVER Allows changing dns resolve behavior and parse DNS server authority as described in https://github.com/grpc/grpc/blob/master/doc/naming.md - true - use alternative resolver - - false - use default resolver (default) \ No newline at end of file + - false - use default resolver (default) diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index 88d621bd..a38e546b 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -119,6 +119,56 @@ function getNoProxyHostList(): string[] { } } +interface CIDRNotation { + ip: number; + prefixLength: number; +} + +/* + * The groups correspond to CIDR parts as follows: + * 1. ip + * 2. prefixLength + */ +const CIDR_REGEX = /^([0-9.]+)(?:\/([0-9]+))?$/; + +export function parseCIDR(cidrString: string): CIDRNotation | null { + const parsedCIDR = CIDR_REGEX.exec(cidrString); + if (parsedCIDR && parsedCIDR.length === 3) { + return { + ip: ipToInt(parsedCIDR[1]), + prefixLength: parseInt(parsedCIDR[2] ?? '32', 10), + }; + } + return null; +} + +function ipToInt(ip: string) { + return ip.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0); +} + +function isIpInCIDR(cidr: CIDRNotation, serverHost: string) { + const ip = cidr.ip; + const mask = -1 << (32 - cidr.prefixLength); + const hostIP = ipToInt(serverHost); + + const networkAddress = ip & mask; + const broadcastAddress = networkAddress | ~mask; + + return hostIP >= networkAddress && hostIP <= broadcastAddress; +} + +function checkHostInNoProxyHostList(serverHost: string): boolean { + for (const host of getNoProxyHostList()) { + const parsedCIDR = parseCIDR(host); + // host is a single IP address or a CIDR notation or a domain + if (parsedCIDR && isIpInCIDR(parsedCIDR, serverHost) || serverHost.endsWith(host)) { + trace('Not using proxy for target in no_proxy list: ' + serverHost); + return true; + } + } + return false; +} + export interface ProxyMapResult { target: GrpcUri; extraOptions: ChannelOptions; @@ -147,13 +197,9 @@ export function mapProxyName( return noProxyResult; } const serverHost = hostPort.host; - for (const host of getNoProxyHostList()) { - if (host === serverHost) { - trace( - 'Not using proxy for target in no_proxy list: ' + uriToString(target) - ); - return noProxyResult; - } + if (checkHostInNoProxyHostList(serverHost)) { + trace('Not using proxy for target in no_proxy list: ' + uriToString(target)); + return noProxyResult; } const extraOptions: ChannelOptions = { 'grpc.http_connect_target': uriToString(target), From ef510d9e9afcb2024f45722b8fb37f9018893104 Mon Sep 17 00:00:00 2001 From: Malak El Kouri Date: Thu, 9 Jan 2025 10:06:37 +0100 Subject: [PATCH 2/5] refactor --- doc/environment_variables.md | 7 ++++--- packages/grpc-js/src/http_proxy.ts | 9 +++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/doc/environment_variables.md b/doc/environment_variables.md index 1259332e..8ade5018 100644 --- a/doc/environment_variables.md +++ b/doc/environment_variables.md @@ -11,10 +11,11 @@ can be set. checked in order, and the first one that has a value is used. * no_grpc_proxy, no_proxy - A comma separated list of hostnames to connect to without using a proxy even - if a proxy is set. These variables are checked in order, and the first one + A comma separated list of comma-separated list of hostnames, IP addresses, + or CIDR blocks to connect to without using a proxy even + if a proxy is set, for example: no_proxy=example.com,192.168.0.1,192.168.0.0/16. + These variables are checked in order, and the first one that has a value is used. - The `no_proxy` environment variable could be a comma-separated list of hostnames, IP addresses, or CIDR blocks (no_proxy=example.com,192.168.0.1,192.168.0.0/16). * GRPC_SSL_CIPHER_SUITES A colon separated list of cipher suites to use with OpenSSL diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index a38e546b..7a7e3494 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -151,13 +151,10 @@ function isIpInCIDR(cidr: CIDRNotation, serverHost: string) { const mask = -1 << (32 - cidr.prefixLength); const hostIP = ipToInt(serverHost); - const networkAddress = ip & mask; - const broadcastAddress = networkAddress | ~mask; - - return hostIP >= networkAddress && hostIP <= broadcastAddress; + return (hostIP & mask) === (ip & mask); } -function checkHostInNoProxyHostList(serverHost: string): boolean { +function hostMatchesNoProxyList(serverHost: string): boolean { for (const host of getNoProxyHostList()) { const parsedCIDR = parseCIDR(host); // host is a single IP address or a CIDR notation or a domain @@ -197,7 +194,7 @@ export function mapProxyName( return noProxyResult; } const serverHost = hostPort.host; - if (checkHostInNoProxyHostList(serverHost)) { + if (hostMatchesNoProxyList(serverHost)) { trace('Not using proxy for target in no_proxy list: ' + uriToString(target)); return noProxyResult; } From 2fa886109b9bd3174059b3caad6e0b18c64444a9 Mon Sep 17 00:00:00 2001 From: Malak El Kouri Date: Thu, 9 Jan 2025 12:11:12 +0100 Subject: [PATCH 3/5] define no_proxy cases in hostMatchesNoProxyList --- packages/grpc-js/src/http_proxy.ts | 34 +++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index 7a7e3494..e4edfe01 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -17,7 +17,7 @@ import { log } from './logging'; import { LogVerbosity } from './constants'; -import { Socket } from 'net'; +import { isIPv4, Socket } from 'net'; import * as http from 'http'; import * as logging from './logging'; import { @@ -129,17 +129,20 @@ interface CIDRNotation { * 1. ip * 2. prefixLength */ -const CIDR_REGEX = /^([0-9.]+)(?:\/([0-9]+))?$/; export function parseCIDR(cidrString: string): CIDRNotation | null { - const parsedCIDR = CIDR_REGEX.exec(cidrString); - if (parsedCIDR && parsedCIDR.length === 3) { - return { - ip: ipToInt(parsedCIDR[1]), - prefixLength: parseInt(parsedCIDR[2] ?? '32', 10), - }; - } - return null; + const splitRange = cidrString.split('/'); + if (splitRange.length !== 2) { + return null; + } + const prefixLength = parseInt(splitRange[1], 10); + if (!isIPv4(splitRange[0]) || Number.isNaN(prefixLength) || prefixLength < 0 || prefixLength > 32) { + return null; + } + return { + ip: ipToInt(splitRange[0]), + prefixLength: prefixLength + }; } function ipToInt(ip: string) { @@ -157,8 +160,15 @@ function isIpInCIDR(cidr: CIDRNotation, serverHost: string) { function hostMatchesNoProxyList(serverHost: string): boolean { for (const host of getNoProxyHostList()) { const parsedCIDR = parseCIDR(host); - // host is a single IP address or a CIDR notation or a domain - if (parsedCIDR && isIpInCIDR(parsedCIDR, serverHost) || serverHost.endsWith(host)) { + // host is a single IP or a domain name suffix + if (!parsedCIDR) { + if (host === serverHost || serverHost.includes(host)) { + trace('Not using proxy for target in no_proxy list: ' + serverHost); + return true; + } + } + // host is a CIDR or a domain + else if (isIpInCIDR(parsedCIDR, serverHost)) { trace('Not using proxy for target in no_proxy list: ' + serverHost); return true; } From 5f20dc91f097e4af10cdc59c610919778515934f Mon Sep 17 00:00:00 2001 From: Malak El Kouri Date: Fri, 10 Jan 2025 10:39:20 +0100 Subject: [PATCH 4/5] fix case where serverHost is not an IP address + refactor --- doc/environment_variables.md | 2 +- packages/grpc-js/src/http_proxy.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/environment_variables.md b/doc/environment_variables.md index 8ade5018..04bb6374 100644 --- a/doc/environment_variables.md +++ b/doc/environment_variables.md @@ -11,7 +11,7 @@ can be set. checked in order, and the first one that has a value is used. * no_grpc_proxy, no_proxy - A comma separated list of comma-separated list of hostnames, IP addresses, + A comma separated list of hostnames, IP addresses, or CIDR blocks to connect to without using a proxy even if a proxy is set, for example: no_proxy=example.com,192.168.0.1,192.168.0.0/16. These variables are checked in order, and the first one diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index e4edfe01..ceeaa174 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -160,18 +160,18 @@ function isIpInCIDR(cidr: CIDRNotation, serverHost: string) { function hostMatchesNoProxyList(serverHost: string): boolean { for (const host of getNoProxyHostList()) { const parsedCIDR = parseCIDR(host); + // host is a CIDR and serverHost is an IP address + if (isIPv4(serverHost) && parsedCIDR && isIpInCIDR(parsedCIDR, serverHost)) { + trace('Not using proxy for target in no_proxy list: ' + serverHost); + return true; + } // host is a single IP or a domain name suffix - if (!parsedCIDR) { - if (host === serverHost || serverHost.includes(host)) { + else { + if (serverHost.endsWith(host)) { trace('Not using proxy for target in no_proxy list: ' + serverHost); return true; } } - // host is a CIDR or a domain - else if (isIpInCIDR(parsedCIDR, serverHost)) { - trace('Not using proxy for target in no_proxy list: ' + serverHost); - return true; - } } return false; } From 5e7cd85290aef65fe2bd7d228a4603e63666f1cc Mon Sep 17 00:00:00 2001 From: Malak El Kouri Date: Wed, 15 Jan 2025 10:02:14 +0100 Subject: [PATCH 5/5] remove trace --- packages/grpc-js/src/http_proxy.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/grpc-js/src/http_proxy.ts b/packages/grpc-js/src/http_proxy.ts index ceeaa174..c40d207a 100644 --- a/packages/grpc-js/src/http_proxy.ts +++ b/packages/grpc-js/src/http_proxy.ts @@ -162,15 +162,10 @@ function hostMatchesNoProxyList(serverHost: string): boolean { const parsedCIDR = parseCIDR(host); // host is a CIDR and serverHost is an IP address if (isIPv4(serverHost) && parsedCIDR && isIpInCIDR(parsedCIDR, serverHost)) { - trace('Not using proxy for target in no_proxy list: ' + serverHost); return true; - } - // host is a single IP or a domain name suffix - else { - if (serverHost.endsWith(host)) { - trace('Not using proxy for target in no_proxy list: ' + serverHost); - return true; - } + } else if (serverHost.endsWith(host)) { + // host is a single IP or a domain name suffix + return true; } } return false;