A peer gone is then an error of the send rather than SIGPIPE. SIGPIPE
stays ignored for what cannot pass the flag: TLS writes and splice().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wolfSSL built with WOLFSSL_QT, as Ubuntu 26.04's 5.9.1 is, returns a
serial number without the ASN.1 header its X509_set_serialNumber()
requires, so every certificate copy failed. The copy then gets a
positive serial number from the hash of the source certificate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3proxy ignored SIGPIPE only after readconfig(), which starts the
services: a client gone while written to in the meantime killed the
process (as auto's case saw on macOS). Standalone services never ignored
it. Sends pass no MSG_NOSIGNAL, so this is the only guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sockrecvfrom() polled once and then read in a loop for as long as the
read failed with EAGAIN. Over TLS a socket is readable when only a part
of a record has come, and the read fails with EAGAIN until the rest
does: a client sending half a record and then nothing kept a thread
taking all of a CPU, and the read never timed out. On EAGAIN the socket
is now polled again, the read repeated at once only for EINTR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dnspr relayed to the client whatever came first to the socket it asked
the name server from, from any address, under any id. Now:
- the query goes to the name server under a random id of its own, the
client's put back in what is relayed to it;
- an answer over UDP is taken only from the address and port of the
name server asked, anything else read past until DNS_TO runs out;
- the answer must have that id, be a response, and carry the question
asked byte for byte, over UDP and TCP alike (a TCP answer that does
not is error 834).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux a proxy listening on a Unix socket relayed only the first
bytes of an answer: sockmap() read with recvfrom() into the client's
address, which for a Unix stream socket Linux fills with an empty
AF_UNIX one, and then sent with sendto() to that address, which a
connected Unix stream socket refuses. A TCP connection has no use for
addresses: sockmap() now sends and receives with NULL ones, and
connect-back sends its command with socksend() and reads the answer
with sockrecvfrom() without an address, which a NULL sin now allows.
An abstract socket name (-iunix:@NAME) was bound with the whole size of
struct sockaddr_un, padded with zeros, so a client naming it by its
bytes, as most do, reached another name. SASIZE() now gives an abstract
address its exact size; getsockname() calls that took SASIZE() of the
old contents as the buffer size take the buffer's size instead.
tcppm refused port 0 for a unix: target, though the manual gives
"tcppm 8080 unix:/var/run/app.sock 0" as the example: the port of a
Unix socket target is ignored, so 0 is allowed.
On a reload the new service bound the path anew and the old one,
closing after it, removed the file, the new service's by then, leaving
nothing to connect to. A service now notes the file it bound (device
and inode) and removes the path on closing only if it is still that
file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- readconfig() kept its line buffer and argument array on every error
return, so each refused reload leaked them.
- A filter refusing a connection in filter_client (TLS: a failed
handshake) was not kept, so its filter_clear never ran and the state
it made was lost. makefilters() now clears it, and empties the slot
first, which is not cleared when it is allocated.
- ssl_filter_close() freed the client CA paths but not the server ones,
for every TLS service set up.
(cherry picked from commit 796e9f8, without what 0.9 does not have:
compiled patterns in ACLs and httpsrv rules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ssl_filter_open() copies ssl_client_mode into the service's
configuration, but the filters deciding when to begin TLS towards the
server read the global value instead: the last ssl_client_mode in the
configuration applied to every service, and a reload changed it for the
services still running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit bab80ef684)
A socket which hung up and of which nothing is asked was given to poll()
as INVALID_SOCKET, so that poll() would not return at once for the
hang-up every time. poll() ignores a negative descriptor, but SOCKET is
unsigned on Windows, and WSAPoll fails the whole call for it: sockmap()
ended the session as soon as a client half-closed. Such a socket is now
not in the array at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 98fd0f329f)
sockmap() took a hang-up on a socket it had data to write to for a peer
it could no longer write to. With poll() a hang-up comes once both sides
are shut down, so that held. WSAPoll reports one as soon as the peer has
half-closed, still reading, and a half-closed client got nothing on
Windows: the test added with the previous fix failed there.
The write is now tried, and a peer really gone makes it fail, which
ends the connection as before. WSAPoll keeps reporting the hang-up
whether the socket has room or not, so on Windows room is waited for
with select() first unless poll() reported it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ddefa40affb76de267a8be1cf59a183a8eb77e32)
When the client shuts down its sending side, sockmap() shuts down the
server's in turn, and once the server has answered and closed, poll()
reports a hang-up on its socket. A socket sockmap() had nothing to ask
of at that moment, its buffer being full for a slow client, was still
polled, and a hang-up there ended both directions: whatever of the
answer was still unread in the kernel was dropped and the client got an
end of stream in the middle of it, megabytes short with large answers.
A hang-up on such a socket now only marks it: what is left is read
once the buffer has room, up to the end of the stream as usual. Errors
still end the connection. Since poll() reports a hang-up whatever is
asked, the socket is left out of poll() while nothing is asked of it.
The same holds for the client's side, for an upload cut short after the
server half-closes.
(cherry picked from commit 8c87b43, without its test: 0.9 has no test
suite)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A kept-alive client connection reuses the connection to the server while
requests go to the same host. The check compared the start of the request
line up to the end of the host, method included, so a POST after a GET to
the same host, or a GET after a POST, closed the server connection and
opened another one. Only the host and port are compared now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The C library reads /etc/localtime on the first use of local time and
connects to /dev/log on the first syslog() call. With a chroot before
either, every local time became UTC and every syslog message was lost.
The time zone is now loaded before chroot() and openlog() connects at
once. The chroot entry of 3proxy.cfg(5) lists what PAM, LDAP and ODBC
still need inside the chroot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without -Ne, the reply to CONNECT and BIND named the local address of the
client's connection to the proxy instead of the socket towards the
destination: the branch meant for UDP ASSOCIATE also took these. A BIND
client was told the proxy's own port and the peer could never reach the
listener. Introduced with -Ne / -Ni (0c8be90).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On receive, the struct sockaddr is zeroed and does not have an address
family until recvfrom fills it in. SASIZE assumes that if there is no
address family, it is INET. This means that for IPv6 UDP sockets, the
sockaddr is truncated and the reply is forwarded to the wrong address.
Instead, use sizeof(PROXYSOCKADDRTYPE).
A verifier following RFC 5280 strictly looks for the issuer through a key
identifier and refuses a certificate carrying none: OpenSSL does with
x509_strict, and Python has since 3.13, so a current client rejects an
intercepted connection outright. Working around that by turning verification
off in the client removes the protection interception was meant to preserve.
Generate the identifiers rather than copying them, so they name the CA
signing here and not the one that signed upstream. wolfSSL cannot generate
extensions, so this is in the branch that already depends on that.
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.
A UDP service on Windows binds a second socket to the address it listens on,
so a reply leaves the port the client sent to. Windows only allows that when
both sockets ask for it, and the listening socket was never asked, so the
bind failed and dnspr answered nothing at all on Windows.
Ask for it on the listening socket of a UDP service, and there only: the
reason it is withheld otherwise, that another local process could take a
listening port, is unchanged for everything else.
wolfSSL reserves about 48K of static thread-local storage. glibc counts
that against the thread stack, so pthread_create() was refused with EINVAL
and no service thread was ever created: the process started, listened on
nothing, logged nothing and waited forever on the semaphore the thread was
supposed to release. Every wolfSSL build on glibc was affected.
Add that reserve to the thread stack when built against wolfSSL. musl puts
the block beside the stack rather than inside it and needs nothing extra,
and OpenSSL has no static TLS at all, so neither pays for it.
Check the pthread_create() return value as well, so a thread that cannot be
created reports itself and suggests stacksize instead of hanging.
ss pointed into the buf request buffer and was never updated when buf
was realloc()ed while reading request headers (both in the header loop
and in handlehdrfilterscli()). The ftp:// branch then dereferenced the
stale pointer.
Store ss as an offset into buf and recompute it before use.
Requires authenticated access to proxy or configuration with
authentication disabled.
Reported by Calif.io in collaboration with Anthropic
(ref ANT-2026-FHVKZ7VQ).