From 128366ea5933dd612c9ecf9cb32c2678c4f1b919 Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Thu, 8 Jan 2026 15:15:08 +0530 Subject: [PATCH] Add IPv6 CIDR support to no_proxy handling Extend proxy bypass logic to correctly interpret IPv6 CIDR ranges defined in the no_proxy environment variable. Existing IPv4 and hostname behavior remains unchanged. --- src/requests/utils.py | 55 +++++++++++++++++++++++++++++++++++----- tests/test_ipv6_proxy.py | 23 +++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 tests/test_ipv6_proxy.py diff --git a/src/requests/utils.py b/src/requests/utils.py index 8ab55852cc..8c7f863f19 100644 --- a/src/requests/utils.py +++ b/src/requests/utils.py @@ -668,7 +668,6 @@ def requote_uri(uri): # properly quoted so they do not cause issues elsewhere. return quote(uri, safe=safe_without_percent) - def address_in_network(ip, net): """This function allows you to check if an IP belongs to a network subnet @@ -695,6 +694,17 @@ def dotted_netmask(mask): return socket.inet_ntoa(struct.pack(">I", bits)) +def is_ipv6_address(string_ip): + """ + :rtype: bool + """ + try: + socket.inet_pton(socket.AF_INET6, string_ip) + except OSError: + return False + return True + + def is_ipv4_address(string_ip): """ :rtype: bool @@ -705,7 +715,6 @@ def is_ipv4_address(string_ip): return False return True - def is_valid_cidr(string_network): """ Very simple check of the cidr format in no_proxy variable. @@ -718,17 +727,41 @@ def is_valid_cidr(string_network): except ValueError: return False - if mask < 1 or mask > 32: + if mask < 1 or mask > 128: return False - try: - socket.inet_aton(string_network.split("/")[0]) - except OSError: - return False + if is_ipv4_address(string_network.split("/")[0]): + if mask > 32: + return False + return True + if is_ipv6_address(string_network.split("/")[0]): + return True + return False else: return False return True +def address_in_network_ipv6(ip, net): + """This function allows you to check if an IPv6 address belongs to a network subnet + + :rtype: bool + """ + if "/" not in net: + return ip == net + + netaddr, bits = net.split("/") + try: + bits = int(bits) + except ValueError: + return False + + if bits < 1 or bits > 128: + return False + + ipaddr = int.from_bytes(socket.inet_pton(socket.AF_INET6, ip), "big") + network = int.from_bytes(socket.inet_pton(socket.AF_INET6, netaddr), "big") + mask = (1 << 128) - (1 << (128 - bits)) + return (ipaddr & mask) == (network & mask) @contextlib.contextmanager def set_environ(env_name, value): @@ -789,6 +822,14 @@ def get_proxy(key): # If no_proxy ip was defined in plain IP notation instead of cidr notation & # matches the IP of the index return True + elif is_ipv6_address(parsed.hostname): + for proxy_ip in no_proxy: + if is_valid_cidr(proxy_ip): + if address_in_network_ipv6(parsed.hostname, proxy_ip): + return True + elif parsed.hostname == proxy_ip: + return True + else: host_with_port = parsed.hostname if parsed.port: diff --git a/tests/test_ipv6_proxy.py b/tests/test_ipv6_proxy.py new file mode 100644 index 0000000000..a7d0cdc95f --- /dev/null +++ b/tests/test_ipv6_proxy.py @@ -0,0 +1,23 @@ +import pytest +from requests.utils import should_bypass_proxies + +class TestIPv6ProxyBypass: + @pytest.mark.parametrize( + "url, no_proxy, expected", + [ + # Basic IPv6 CIDR match + ("http://[2001:db8::1]", "2001:db8::/32", True), + # IPv6 CIDR mismatch + ("http://[2001:db8::1]", "2001:db9::/32", False), + # Compressed vs Uncompressed + ("http://[2001:0db8:0000:0000:0000:0000:0000:0001]", "2001:db8::/32", True), + # Multiple no_proxy items + ("http://[2001:db8::1]", "example.com, 2001:db8::/32", True), + # IPv4 should still work + ("http://192.168.1.5", "192.168.1.0/24", True), + ] + ) + def test_ipv6_cidr_bypass(self, url, no_proxy, expected, monkeypatch): + monkeypatch.setenv("no_proxy", no_proxy) + monkeypatch.setenv("NO_PROXY", no_proxy) + assert should_bypass_proxies(url, no_proxy=None) == expected