From 6e5aea63da8e4821dbbdd38903dd71454ef840f2 Mon Sep 17 00:00:00 2001 From: Neal Turett Date: Mon, 24 Aug 2026 16:01:55 -0600 Subject: [PATCH 1/4] gh-149746: Handle CIDRs in NO_PROXY env var --- Doc/library/urllib.request.rst | 3 +- Lib/test/test_urllib.py | 23 +++++++++++ Lib/test/test_urlparse.py | 1 + Lib/urllib/parse.py | 9 +++++ Lib/urllib/request.py | 72 +++++++++++++++++++++++++++------- 5 files changed, 92 insertions(+), 16 deletions(-) diff --git a/Doc/library/urllib.request.rst b/Doc/library/urllib.request.rst index 9274a0c88ac4c86..5c3b4ed018bbdfc 100644 --- a/Doc/library/urllib.request.rst +++ b/Doc/library/urllib.request.rst @@ -361,7 +361,8 @@ 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``. + ``cern.ch,ncsa.uiuc.edu,some.host:8080``. IP CIDR notation is also + supported. .. note:: diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index ab59727a8fd1820..da95eba4e0f0b48 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -263,6 +263,29 @@ def test_proxy_bypass_environment_newline(self): self.assertFalse(bypass('anotherdomain.com:8888\n')) self.assertFalse(bypass('newdomain.com:1234\n')) + def test_proxy_bypass_environment_cidrs(self): + bypass = lambda a, b: urllib.request.proxy_bypass_environment(a, {'no': b}) + + # IPv4 CIDRs + self.assertTrue(bypass('192.168.0.5', 'asdf.com,192.168.0.0/24')) + self.assertTrue(bypass('192.168.0.5', 'asdf.com,192.168.0.10/24')) + self.assertTrue(bypass('192.168.0.5:8443', 'asdf.com,192.168.0.0/24')) + self.assertTrue(bypass('192.168.0.5', 'asdf.com,192.168.0.5')) + self.assertTrue(bypass('192.168.0.5', 'asdf.com,192.168.0.5/32')) + self.assertFalse(bypass('10.1.2.3', 'asdf.com,192.168.0.0/24')) + + # IPv6 CIDRs + self.assertTrue(bypass('2001:db8:85a3:1::10', 'asdf.com,2001:db8:85a3:1::/64')) + self.assertTrue(bypass('[2001:db8:85a3:1::10]', 'asdf.com,2001:db8:85a3:1::/64')) + self.assertTrue(bypass('[2001:db8:85a3:1::10]:443', 'asdf.com,2001:db8:85a3:1::/64')) + self.assertTrue(bypass('2001:db8:85a3:1::10', 'asdf.com,2001:db8:85a3:1::10')) + self.assertTrue(bypass('2001:db8:85a3:1::10', 'asdf.com,2001:db8:85a3:1::10/128')) + self.assertFalse(bypass('1001:db8:85a3:1::10', 'asdf.com,2001:db8:85a3:1::10/128')) + + # Invalid CIDRs should be ignored + self.assertFalse(bypass('192.168.0.5', 'asdf.com,192.168.0.5/40')) + self.assertFalse(bypass('2001:db8:85a3:1::10', 'asdf.com,2001:db8:85a3:1::/150')) + class ProxyTests_withOrderedEnv(unittest.TestCase): diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py index a5b7966c7780e9e..0b3fc85ac7a114a 100644 --- a/Lib/test/test_urlparse.py +++ b/Lib/test/test_urlparse.py @@ -1843,6 +1843,7 @@ def test_splitport(self): self.assertEqual(splitport('parrot:cheese'), ('parrot:cheese', None)) self.assertEqual(splitport('[::1]:88'), ('[::1]', '88')) self.assertEqual(splitport('[::1]'), ('[::1]', None)) + self.assertEqual(splitport('::1'), ('::1', None)) self.assertEqual(splitport(':88'), ('', '88')) def test_splitnport(self): diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py index 4247b9a4b07fa3f..4fd28789697e6ff 100644 --- a/Lib/urllib/parse.py +++ b/Lib/urllib/parse.py @@ -1314,6 +1314,15 @@ def splitport(host): _portprog = None def _splitport(host): """splitport('host:port') --> 'host', 'port'.""" + # Handle bare IPv6 addresses with : (e.g. ::1) + if host.count(':') > 1 and not host.startswith('['): + try: + ipaddress.ip_address(host) + except ValueError: + pass + else: + return host, None + global _portprog if _portprog is None: _portprog = re.compile('(.*):([0-9]*)', re.DOTALL) diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 9fa92659a255ed4..428fc4b48f36b30 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -96,6 +96,8 @@ import time import tempfile +from functools import lru_cache +from ipaddress import ip_address, ip_network from urllib.error import URLError, HTTPError, ContentTooShortError from urllib.parse import ( @@ -1924,7 +1926,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 and IP CIDRs, or + '*' for all hosts. """ if proxies is None: @@ -1937,22 +1940,61 @@ def proxy_bypass_environment(host, proxies=None): # '*' is special case for always bypass if no_proxy == '*': return True - host = host.lower() - # strip port off host - hostonly, port = _splitport(host) - # check if the host ends with any of the DNS suffixes - for name in no_proxy.split(','): - name = name.strip() - if name: - name = name.lstrip('.') # ignore leading dots - name = name.lower() - if hostonly == name or host == name: + + return _ProxyBypassEnvSettings.cached(no_proxy).should_bypass(host) + + +class _ProxyBypassEnvSettings: + def __init__(self, data): + self.matches = set() + self.cidrs = [] + + for elem in data.split(','): + elem = elem.strip() + + if not elem: + continue + + try: + block = ip_network(elem, strict=False) + except ValueError: + self.matches.add(elem.lstrip('.').lower()) + else: + self.cidrs.append(block) + + def should_bypass(self, host): + host = host.lower() + hostonly, _ = _splitport(host) + + try: + ip = ip_address(hostonly.strip('[]')) + except ValueError: + pass + else: + if any(ip in cidr for cidr in self.cidrs): return True - name = '.' + name - if hostonly.endswith(name) or host.endswith(name): + + if host in self.matches or hostonly in self.matches: + return True + + for match in self.matches: + dot_match = '.' + match + if host.endswith(dot_match) or hostonly.endswith(dot_match): return True - # otherwise, don't bypass - return False + + return False + + @classmethod + @lru_cache + def cached(cls, data): + """Initializer with cache. + + The vast majority of the time, NO_PROXY won't change. + + This function caches the string manipulations so they don't repeat on + every single proxy check. + """ + return cls(data) # This code tests an OSX specific data structure but is testable on all From 7619936ff33b1ac7ec53459f2680abb20157f84e Mon Sep 17 00:00:00 2001 From: Neal Turett Date: Mon, 24 Aug 2026 16:32:28 -0600 Subject: [PATCH 2/4] add blurb --- .../next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst b/Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst new file mode 100644 index 000000000000000..0b71e966ca9082f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst @@ -0,0 +1,2 @@ +urllib now allows IP CIDRs to be excluded from proxying by including the +CIDR in the `NO_PROXY` env var. Contributed by Neal Turett. From 01b1e576a8fb582a1bdd04100d5532281ee0f1da Mon Sep 17 00:00:00 2001 From: Neal Turett Date: Mon, 24 Aug 2026 16:34:32 -0600 Subject: [PATCH 3/4] lint --- Lib/urllib/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 428fc4b48f36b30..f930374edd3e44b 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1948,7 +1948,7 @@ class _ProxyBypassEnvSettings: def __init__(self, data): self.matches = set() self.cidrs = [] - + for elem in data.split(','): elem = elem.strip() From 46818b228d3968b8244c1c0271182af5ce3ce05f Mon Sep 17 00:00:00 2001 From: Neal Turett Date: Mon, 24 Aug 2026 17:39:35 -0600 Subject: [PATCH 4/4] backticks --- .../next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst b/Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst index 0b71e966ca9082f..daf99e7129521a7 100644 --- a/Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst +++ b/Misc/NEWS.d/next/Library/2026-08-24-16-32-20.gh-issue-149746.ctIq-G.rst @@ -1,2 +1,2 @@ urllib now allows IP CIDRs to be excluded from proxying by including the -CIDR in the `NO_PROXY` env var. Contributed by Neal Turett. +CIDR in the ``no_proxy`` env var. Contributed by Neal Turett.