diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5 index 57e7615..7e6f188 100644 --- a/man/3proxy.cfg.5 +++ b/man/3proxy.cfg.5 @@ -527,7 +527,10 @@ If not specified, nserver is used. The syntax is the same as for nserver. .BR nsrecord \fI\fR \fI\fR .br - Adds static record to nscache. \fBnscache\fR must be enabled. If 0.0.0.0 + Adds static record to nscache. \fBnscache\fR must be enabled and must come +first, because the record is placed in the table it allocates, and +\fBnserver\fR must be set as well: without it the system resolver is used and +static records are never consulted. If 0.0.0.0 is used as a hostaddr host will never resolve, it can be used to blacklist something or together with .B dialer diff --git a/tests/cases/parent_ports.py b/tests/cases/parent_ports.py index a6d0788..12bf1e9 100644 --- a/tests/cases/parent_ports.py +++ b/tests/cases/parent_ports.py @@ -26,8 +26,11 @@ def _windows(): (LOW, HIGH), (ILOW, IHIGH) = _windows() -# below the Linux window on purpose: the kernel ignores such a range -UNHONOURED = (21400, 21449) +# Privileged ports: the kernel ignores such a range on Linux, since it is +# outside net.ipv4.ip_local_port_range, and binding them fails outright +# without privileges. Either way nothing in the range can be taken, which +# is the case the fallback exists for. +UNHONOURED = (1, 99) def run(t): diff --git a/tests/cases/ssl.py b/tests/cases/ssl.py index 1404832..77d01ee 100644 --- a/tests/cases/ssl.py +++ b/tests/cases/ssl.py @@ -129,6 +129,9 @@ def run(t): plain = t.free_port() proxies = t.start("ssl_mitm", f""" log + nserver 127.0.0.1 + nscache 1024 + nsrecord intercepted.test 127.0.0.1 ssl_server_ca_file {certs.ca} ssl_server_ca_key {certs.ca_key} ssl_certcache {certs.cache} @@ -146,12 +149,18 @@ def run(t): proxy -p{plain} """, ports=[mitm, plain]) - target = f"https://127.0.0.1:{origin}/secret/page" + # A name the proxy resolves itself through nsrecord, so the request + # carries a hostname the way a real one would, without depending on + # what the machine running the tests puts in its hosts file. + target = f"https://intercepted.test:{origin}/secret/page" # The client trusts our CA, which is what signs the spoofed certificate. # Verification is not strict: 3proxy issues those certificates without an # Authority Key Identifier, which Python rejects under its 3.13 defaults. - r = t.https(target, proxy=f"127.0.0.1:{mitm}", ca=certs.ca, strict=False) + # The spoofed certificate names the upstream host rather than the one + # asked for, so the chain is checked but the name is not. + r = t.https(target, proxy=f"127.0.0.1:{mitm}", ca=certs.ca, strict=False, + verify_name=False) t.eq(200, r.status, "MITM passes the request through") t.contains(r, "path=/secret/page", "the intercepted request reaches the origin") @@ -159,19 +168,20 @@ def run(t): log = t.wait_output(proxies, "/secret/page") t.contains(log, "/secret/page", "MITM puts the request URI in the log") t.contains(log, "GET", "MITM logs the method") - t.contains(log, str(origin), "MITM logs the destination") + t.contains(log, "intercepted.test", "MITM logs the host that was asked for") # a client that does not trust the CA sees the substitution refused = t.https(target, proxy=f"127.0.0.1:{mitm}", ca=certs.other, - strict=False) + strict=False, verify_name=False) t.ne(200, refused.status, "MITM is visible to a client with another CA") # Without interception the same request is opaque: the proxy logs the # CONNECT target and nothing from inside the tunnel. before = len(proxies.output()) - r = t.https(target, proxy=f"127.0.0.1:{plain}", ca=certs.ca, strict=False) + r = t.https(target, proxy=f"127.0.0.1:{plain}", ca=certs.ca, strict=False, + verify_name=False) t.eq(200, r.status, "the plain proxy tunnels the same request") - tunnelled = t.wait_output(proxies, str(origin), since=before) - t.contains(tunnelled, str(origin), "the tunnel logs the CONNECT target") + tunnelled = t.wait_output(proxies, "intercepted.test", since=before) + t.contains(tunnelled, "intercepted.test", "the tunnel logs the CONNECT target") t.not_contains(tunnelled, "/secret/page", "a tunnelled request keeps its URI out of the log") diff --git a/tests/harness.py b/tests/harness.py index 0ad4e7e..8109051 100644 --- a/tests/harness.py +++ b/tests/harness.py @@ -122,6 +122,7 @@ class Tester: self.timeout = 10 self._skipped = 0 self._certs = None + self.logs = [] # ---- servers ----------------------------------------------------- @@ -201,8 +202,10 @@ class Tester: time.sleep(0.05) def stop_all(self): + """Stop the servers, keeping what they printed for the report.""" for server in self.servers: server.stop() + self.logs.append((server.name, server.output())) self.servers = [] # ---- requests ---------------------------------------------------- @@ -464,14 +467,21 @@ class Tester: self._certs = c return c - def _context(self, ca=None, strict=True): - """A client context. strict=False drops the RFC 5280 checks Python - turns on by default from 3.13, which reject a certificate with no - Authority Key Identifier.""" + def _context(self, ca=None, strict=True, verify_name=True): + """A client context. + + strict=False drops the RFC 5280 checks Python turns on by default + from 3.13, which reject a certificate with no Authority Key + Identifier. verify_name=False keeps the chain check but ignores + which host the certificate names, for the intercepted connections + where that is the upstream identity rather than the one asked for. + """ if ca: context = ssl.create_default_context(cafile=ca) if not strict: context.verify_flags &= ~getattr(ssl, "VERIFY_X509_STRICT", 0) + if not verify_name: + context.check_hostname = False return context context = ssl.create_default_context() context.check_hostname = False @@ -503,11 +513,11 @@ class Tester: finally: conn.close() - def https(self, url, proxy=None, ca=None, strict=True, method="GET", - headers=None): + def https(self, url, proxy=None, ca=None, strict=True, verify_name=True, + method="GET", headers=None): """An https:// request, optionally tunnelled through a proxy.""" host, port, path = self._split(url, default_port=443) - context = self._context(ca, strict) + context = self._context(ca, strict, verify_name) try: if proxy: phost, pport = self._hostport(proxy) diff --git a/tests/run.py b/tests/run.py index 7caa83b..5b6cf09 100644 --- a/tests/run.py +++ b/tests/run.py @@ -113,6 +113,15 @@ def main(): if actual is not None: print(f" actual: {actual}") + if tester.checks and any(status is False for status, _, _, _ in tester.checks): + for name, text in tester.logs: + lines = [line for line in text.splitlines() if line.strip()] + if not lines: + continue + print(f" --- {name} said ---") + for line in lines[-12:]: + print(f" {line}") + if error: failed += 1 failures.append(f"{name}: case aborted")