From 679719b6f2b33e9ba84453b0684ba7cb3f002bdc Mon Sep 17 00:00:00 2001 From: Conlan Cesar Date: Tue, 12 May 2026 16:27:52 -0400 Subject: [PATCH 1/6] Support CIDR no_proxy entries --- Doc/library/urllib.request.rst | 5 +++-- Lib/test/test_urllib.py | 22 ++++++++++++++++++++++ Lib/urllib/request.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Doc/library/urllib.request.rst b/Doc/library/urllib.request.rst index 64e915d042d4a0..490e35ad681022 100644 --- a/Doc/library/urllib.request.rst +++ b/Doc/library/urllib.request.rst @@ -360,8 +360,9 @@ The following classes are provided: The :envvar:`no_proxy` environment variable can be used to specify hosts which shouldn't be reached via proxy; if set, it should be a comma-separated - list of hostname suffixes, optionally with ``:port`` appended, for example - ``cern.ch,ncsa.uiuc.edu,some.host:8080``. + list of hostname suffixes, optionally with ``:port`` appended, and IP + address CIDR ranges, for example + ``cern.ch,ncsa.uiuc.edu,some.host:8080,192.168.0.0/16``. .. note:: diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 2dd739b77b8e4d..c9b5e339678458 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -245,6 +245,28 @@ def test_proxy_bypass_environment_host_match(self): self.assertFalse(bypass('newdomain.com')) # no port self.assertFalse(bypass('newdomain.com:1235')) # wrong port + def test_proxy_bypass_environment_cidr_match(self): + bypass = urllib.request.proxy_bypass_environment + self.env.set('NO_PROXY', + '192.168.0.0/16, 2001:db8::/32, 172.16.1.1/24') + self.assertTrue(bypass('192.168.1.1')) + self.assertTrue(bypass('192.168.1.1:1234')) + self.assertTrue(bypass('2001:db8::1')) + self.assertTrue(bypass('[2001:db8::1]:1234')) + self.assertTrue(bypass('172.16.1.255')) + self.assertFalse(bypass('192.169.1.1')) + self.assertFalse(bypass('2001:db9::1')) + self.assertFalse(bypass('172.16.2.1')) + self.assertFalse(bypass('python.org')) + + def test_proxy_bypass_environment_invalid_cidr(self): + bypass = urllib.request.proxy_bypass_environment + self.env.set('NO_PROXY', + '192.168.0.0/33, 2001:db8::/129, anotherdomain.com') + self.assertFalse(bypass('192.168.1.1')) + self.assertFalse(bypass('2001:db8::1')) + self.assertTrue(bypass('anotherdomain.com')) + def test_proxy_bypass_environment_always_match(self): bypass = urllib.request.proxy_bypass_environment self.env.set('NO_PROXY', '*') diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index f5f17f223a4585..84977ba72c546a 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1912,7 +1912,8 @@ def proxy_bypass_environment(host, proxies=None): """Test if proxies should not be used for a particular host. Checks the proxy dict for the value of no_proxy, which should - be a list of comma separated DNS suffixes, or '*' for all hosts. + be a list of comma separated DNS suffixes, IP address CIDR ranges, + or '*' for all hosts. """ if proxies is None: @@ -1928,14 +1929,40 @@ def proxy_bypass_environment(host, proxies=None): host = host.lower() # strip port off host hostonly, port = _splitport(host) - # check if the host ends with any of the DNS suffixes + host_ip = None + checked_host_ip = False + # for every entry in no_proxy... for name in no_proxy.split(','): name = name.strip() if name: name = name.lstrip('.') # ignore leading dots name = name.lower() + + # check for exact match if hostonly == name or host == name: return True + + # check if the IP is within CIDR range + if '/' in name: + if not checked_host_ip: + from ipaddress import ip_address + for candidate in (hostonly, host): + candidate = candidate.strip('[]') + try: + host_ip = ip_address(candidate) + break + except ValueError: + pass + checked_host_ip = True + if host_ip is not None: + from ipaddress import ip_network + try: + if host_ip in ip_network(name, strict=False): + return True + except ValueError: + pass + + # check if the host ends with any of the DNS suffixes name = '.' + name if hostonly.endswith(name) or host.endswith(name): return True From 34de6316bc1d9f53f3f72e0ad0e39ed4690489d7 Mon Sep 17 00:00:00 2001 From: Conlan Cesar Date: Tue, 12 May 2026 16:32:27 -0400 Subject: [PATCH 2/6] Share proxy CIDR matching with macOS --- Lib/test/test_urllib2.py | 12 ++++++++++ Lib/urllib/request.py | 51 ++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 3a77b9e5ab7928..005c80118ea4a0 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -1586,6 +1586,18 @@ def test_osx_proxy_bypass(self): self.assertFalse(_proxy_bypass_macosx_sysconf(host, bypass), 'expected bypass of %s to be False' % host) + # Check IPv6 CIDR ranges + bypass = { + 'exclude_simple': False, + 'exceptions': ['2001:db8::/32'] + } + for host in ('2001:db8::1', '[2001:db8::1]:443'): + self.assertTrue(_proxy_bypass_macosx_sysconf(host, bypass), + 'expected bypass of %s to be True' % host) + host = '2001:db9::1' + self.assertFalse(_proxy_bypass_macosx_sysconf(host, bypass), + 'expected bypass of %s to be False' % host) + def check_basic_auth(self, headers, realm): with self.subTest(realm=realm, headers=headers): opener = OpenerDirector() diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 84977ba72c546a..613fb06b25ecf1 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1908,6 +1908,28 @@ def getproxies_environment(): proxies.pop(proxy_name, None) return proxies +def _ip_address_from_host(host): + from ipaddress import ip_address + + hostonly, port = _splitport(host) + for candidate in (hostonly, host): + candidate = candidate.strip('[]') + try: + return ip_address(candidate) + except ValueError: + pass + return None + +def _is_ip_address_in_network(ip_address, network): + if ip_address is None: + return False + from ipaddress import ip_network + + try: + return ip_address in ip_network(network, strict=False) + except ValueError: + return False + def proxy_bypass_environment(host, proxies=None): """Test if proxies should not be used for a particular host. @@ -1945,22 +1967,10 @@ def proxy_bypass_environment(host, proxies=None): # check if the IP is within CIDR range if '/' in name: if not checked_host_ip: - from ipaddress import ip_address - for candidate in (hostonly, host): - candidate = candidate.strip('[]') - try: - host_ip = ip_address(candidate) - break - except ValueError: - pass + host_ip = _ip_address_from_host(host) checked_host_ip = True - if host_ip is not None: - from ipaddress import ip_network - try: - if host_ip in ip_network(name, strict=False): - return True - except ValueError: - pass + if _is_ip_address_in_network(host_ip, name): + return True # check if the host ends with any of the DNS suffixes name = '.' + name @@ -1985,9 +1995,9 @@ def _proxy_bypass_macosx_sysconf(host, proxy_settings): } """ from fnmatch import fnmatch - from ipaddress import AddressValueError, IPv4Address hostonly, port = _splitport(host) + host_ip = _ip_address_from_host(host) def ip2num(ipAddr): parts = ipAddr.split('.') @@ -2002,15 +2012,16 @@ def ip2num(ipAddr): return True hostIP = None - try: - hostIP = int(IPv4Address(hostonly)) - except AddressValueError: - pass + if host_ip is not None and host_ip.version == 4: + hostIP = int(host_ip) for value in proxy_settings.get('exceptions', ()): # Items in the list are strings like these: *.local, 169.254/16 if not value: continue + if '/' in value and _is_ip_address_in_network(host_ip, value): + return True + m = re.match(r"(\d+(?:\.\d+)*)(/\d+)?", value) if m is not None and hostIP is not None: base = ip2num(m.group(1)) From b1278c6107aedea159146715b485ee06b4728367 Mon Sep 17 00:00:00 2001 From: Conlan Cesar Date: Tue, 12 May 2026 17:21:27 -0400 Subject: [PATCH 3/6] Support CIDR proxy overrides on Windows --- Lib/test/test_urllib2.py | 11 +++++++++++ Lib/urllib/request.py | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 005c80118ea4a0..0e30a2b7d1d74a 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -1552,6 +1552,17 @@ def test_winreg_proxy_bypass(self): "expect to bypass intranet address '%s'" % host) + # check IP CIDR bypass + proxy_override = "192.168.0.0/16; 2001:db8::/32" + for host in ("192.168.1.1", "192.168.1.1:443", + "2001:db8::1", "[2001:db8::1]:443"): + self.assertTrue(proxy_bypass(host, proxy_override), + "expected bypass of %s to be true" % host) + + for host in ("192.169.1.1", "2001:db9::1"): + self.assertFalse(proxy_bypass(host, proxy_override), + "expected bypass of %s to be False" % host) + @unittest.skipUnless(sys.platform == 'darwin', "only relevant for OSX") def test_osx_proxy_bypass(self): bypass = { diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 613fb06b25ecf1..1e910a2ea74041 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -2054,10 +2054,11 @@ def _proxy_bypass_winreg_override(host, override): Internet settings proxy override registry value. An example of a proxy override value is: - "www.example.com;*.example.net; 192.168.0.1" + "www.example.com;*.example.net; 192.168.0.1; 192.168.0.0/16" """ from fnmatch import fnmatch + host_ip = _ip_address_from_host(host) host, _ = _splitport(host) proxy_override = override.split(';') for test in proxy_override: @@ -2066,6 +2067,8 @@ def _proxy_bypass_winreg_override(host, override): if test == '': if '.' not in host: return True + elif '/' in test and _is_ip_address_in_network(host_ip, test): + return True elif fnmatch(host, test): return True return False From 7635f0749cb595d0209d6c399a7f334a1886fe4a Mon Sep 17 00:00:00 2001 From: Conlan Cesar Date: Tue, 12 May 2026 17:31:24 -0400 Subject: [PATCH 4/6] Test no_proxy literal IP addresses --- Lib/test/test_urllib.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index c9b5e339678458..16887611c684cc 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -267,6 +267,17 @@ def test_proxy_bypass_environment_invalid_cidr(self): self.assertFalse(bypass('2001:db8::1')) self.assertTrue(bypass('anotherdomain.com')) + def test_proxy_bypass_ip_address(self): + bypass = urllib.request.proxy_bypass_environment + self.env.set('NO_PROXY', '169.254.169.254') + self.assertTrue(bypass('169.254.169.254')) + self.assertTrue(bypass('169.254.169.254:1234')) + self.assertFalse(bypass('169.254.169:254')) + self.assertFalse(bypass('169.254.169.254.org')) + self.assertFalse(bypass('2001:db9::1')) + self.assertFalse(bypass('172.16.2.1')) + self.assertFalse(bypass('python.org')) + def test_proxy_bypass_environment_always_match(self): bypass = urllib.request.proxy_bypass_environment self.env.set('NO_PROXY', '*') From e369da6726f40b14baf735fd9cdb4449103094c1 Mon Sep 17 00:00:00 2001 From: Conlan Cesar Date: Tue, 12 May 2026 17:33:26 -0400 Subject: [PATCH 5/6] Add NEWS entry --- .../Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst diff --git a/Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst b/Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst new file mode 100644 index 00000000000000..db978dc81a99b9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst @@ -0,0 +1,3 @@ +Add support for CIDR-notated IP addresses in urllib Proxy Bypass lists. This +includes the `NO_PROXY` environment variable, as well as SystemConfig on +MacOS and the Windows registry. From bfb4ba690501999f9f8b9408b114c7214e851a99 Mon Sep 17 00:00:00 2001 From: Conlan Cesar Date: Tue, 12 May 2026 17:35:29 -0400 Subject: [PATCH 6/6] Fix RST --- .../next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst b/Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst index db978dc81a99b9..ed5ba0edef1270 100644 --- a/Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst +++ b/Misc/NEWS.d/next/Library/2026-05-12-17-32-56.gh-issue-149746.vfXD8b.rst @@ -1,3 +1,3 @@ Add support for CIDR-notated IP addresses in urllib Proxy Bypass lists. This -includes the `NO_PROXY` environment variable, as well as SystemConfig on +includes the ``NO_PROXY`` environment variable, as well as SystemConfig on MacOS and the Windows registry.