[pull] master from 3proxy:master - #53
Open
pull[bot] wants to merge 386 commits into
Open
Conversation
z3APA3A
force-pushed
the
master
branch
4 times, most recently
from
April 8, 2026 18:16
c5d9bac to
2a07b70
Compare
z3APA3A
force-pushed
the
master
branch
3 times, most recently
from
April 12, 2026 10:59
2822017 to
2d6eeff
Compare
…n cmake Allow to set prefix in cmake, 3proxy_ by default
The cmake Windows build links vcpkg's pcre2, whose DLL lives in c:/vcpkg/installed/x64-windows/bin. Running the tests without that on PATH started a 3proxy that died at load with 0xC0000135 before opening a socket, which the suite could only report as "never listened". Report the reason instead of leaving it blank: name the exit code, and say when a body is empty because the request failed rather than because the reply carried nothing.
Three arrangements, with key material generated for the run rather than kept in the tree: a proxy wrapped in TLS, a proxy that reaches a TLS parent and verifies it against the CA, and MITM. The MITM case checks what interception is for: the decrypted request line, URI and all, reaches the log, where the same request through a plain CONNECT tunnel leaves only the host and port. The origin runs in its own process there so the proxy log holds only what the proxy saw, and log assertions wait, since a record is written when the connection finishes rather than when the reply arrives. Verification of the spoofed certificate is deliberately not strict: 3proxy issues those without an Authority Key Identifier, which Python rejects under its 3.13 defaults.
The MITM case reached its origin by address, so it depended on the certificate 3proxy spoofs carrying an IP alternative name. It does when the upstream certificate is copied, which is what happens on Linux and macOS but not on Windows, where the client then refused the connection. Point a name at 127.0.0.1 with nsrecord instead, and check the chain rather than the name: an intercepted certificate names the upstream host, not the one that was asked for. The log assertions gain from it too, since the name is better evidence than a port that the request was seen. nsrecord needs nserver as well as nscache, and has to follow nscache, so say that in the manual: the record goes into the table nscache allocates, and the table is only consulted when nserver is set. The port-range fallback was exercised with a range the same case had already used, so on a busy machine it could fail to bind for the ordinary reason rather than the one under test. Use privileged ports, which nothing can take. When a case fails, print what its servers wrote: the reason usually goes to the server's stderr, which was captured and then thrown away.
…ably httpsrv parsed Content-Length and never read what followed. The reply is followed by a close, and closing a socket that still holds unread data resets the connection instead of ending it, so a POST could cost the client the reply it was about to read. Windows does that reliably; the same test passes on Linux and macOS, which is why it looked flaky. Drain the body, bounded at a megabyte. The test CA was built with -addext, which LibreSSL - the openssl on a stock macOS - does not apply the same way, leaving a certificate that is not usable as a CA and a client that cannot build a chain to it. Put the extensions in a file both accept, and verify the generated chain before any of it is handed to a proxy, so a failure there is not read as a fault in the proxy.
OpenSSL 3 adds a subject and authority key identifier when it signs; LibreSSL, which is the openssl on a stock macOS, does not. Python has verified strictly since 3.13 and rejects a chain whose certificate has no Authority Key Identifier, so the macOS runners refused a certificate the Linux ones accepted. Ask for both by name, and make the self-check strict so the next such gap is caught before a handshake.
The CA was created with no extensions, so it is not usable as a CA and clients report that they cannot get the local issuer certificate. Add basicConstraints, keyCertSign and a subject key identifier, in a file rather than through -addext, which LibreSSL - the openssl on macOS and some BSDs - does not apply the same way. Ask for the key identifiers on the signed certificates too: OpenSSL 3 adds them when it signs and LibreSSL does not, and Python has verified strictly since 3.13, refusing a chain whose certificate carries no authorityKeyIdentifier. Finish with openssl verify -x509_strict, which is the check the client will make. Both recipes were run against OpenSSL 3.6 and LibreSSL 3.3: the old one fails strict verification, the new one passes on both.
The filter walked its list of access rules but tested the first entry each time round, so anything pcre_extend appended was never consulted and the command did nothing at all. Test the entry the loop is on. pcre_extend takes an ACE and no FILTER_ACTION - the rule keeps the action it was given - so correct the manual, which documented an argument the command does not read. The tests cover matching and denial, rule order, client headers, matching options, the access rule a pcre rule carries, pcre_extend, and rewriting a reply header and reply data. Request rewriting is covered through an HTTP parent, which is the path where it reaches the wire.
A rewrite only reached the copy of the request kept for logging. On a direct connection the server is sent the request line held in the header buffer, which was parsed and reduced to its path before the filters ran, so the rewrite showed up in the log and nowhere else. Through an HTTP parent the rewritten copy is what goes out, which is why it appeared to work there. Splice the new path back into that buffer. The destination was chosen, and the access rules applied to it, before the rewrite happened, so a rewrite that changes the method or the authority is left alone: acting on it would send the request somewhere the rules never saw. Say so in the manual. The copy needed to notice a rewrite is only taken when a request filter exists, so a proxy without one does no extra work.
tcppm and udppm forward a port each, so test both directions of each: a request through the TCP mapper reaching its target, a datagram through the UDP one coming back answered, and a mapper whose rules deny the client answering nothing. tlspr takes its destination from the name in the handshake, so point that name at 127.0.0.1 with nsrecord and give the certificate the same name: the name then both chooses where the request goes and is what the client checks. dnspr answers from its cache, including the documented 0.0.0.0 record, which is handed out as an address rather than withheld. auto is asked to serve an HTTP proxy request, SOCKS4, SOCKS5 and a TLS handshake on one port, and to make nothing of a request that is none of them. Its protocols reach different places, so there are two origins. The SOCKS UDP association was only checked for the port it binds. Send datagrams through it as well, large and small, and check a second association gets its own port - and that one bound inside an intport range still relays. A UDP service has no socket to connect to, so readiness is found by asking until it answers rather than racing it.
41 of 112 configuration commands appear in a test. Record the rest as a plan, ordered by how much of the product each gap covers against how much of a fixture it needs, so the next person does not have to redo the survey. Includes the two limitations left deliberately unasserted, and why pinning the current behaviour there would be wrong.
…pport added (cherry picked from commit 88b3225)
(cherry picked from commit 9c74a9c)
(cherry picked from commit 845df7d)
(cherry picked from commit a194f20)
(cherry picked from commit d13be1b)
(cherry picked from commit 7bc3400)
op_rewrite checked what it built with targetunsafe(), which describes a path on this machine: on Windows it must name a drive or a share, so a rewritten request path was refused and every rewrite rule failed there. A rewrite produces a request path and is checked as one. The header loop stopped at HTTPSRV_MAXHDR and answered anyway, leaving the rest of the request in the stream for the next one to be read out of. It refuses the request instead. A header longer than the buffer arrives as several lines, so the count bounds what is read rather than what a client may send in one header. The test for the buffer growth after a PCRE rewrite sent its request through to an httpsrv origin, which stops reading at that same cap; it uses an origin which reads whatever it is sent, since what is under test is the proxy in the middle.
bindwithrange() honours a range by setting IP_LOCAL_PORT_RANGE where it exists, which leaves the kernel to pick a port inside it and skip the ones still closing. Everywhere else it binds a port out of the range at random, ten times, and lets the system choose when all ten are taken. A port which carried a connection cannot be bound again until that close completes - four minutes of it on Windows - so a window of fifty ports shared by three services in one configuration can have too few left, and the connection then binds outside the range and the case fails. The window is five hundred ports there instead. The Linux one is unchanged: the kernel picks from it and needs no room to spare.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )