Commit Graph

1294 Commits

Author SHA1 Message Date
Vladimir Dubrovin
982536b389 Windows: an error set where socket errors are read
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
errno is WSAGetLastError() on Windows, but seterrno3() set the C
library's errno there: the EAGAIN of a TLS read or write wanting the
socket again was not seen as one, and the error left was whatever the
socket call before had set. seterrno3() sets the socket error now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:11:24 +03:00
Vladimir Dubrovin
bd4db18413 connlim: a connection's slot given back once
releaseparam() gave the connection limits' slot back without clearing
param->connlim: a second call, made on the event loops' end of a relayed
client, gave it back again, letting one more connection in for each
such end while another held its slot, or, with no connlimconf, followed
param->srv, cleared by the first call. It is cleared now.

A UDP session taken over by the new service on a reload gave its slot
back but kept connlim set, so the new service's authorization did not
count it, and its end gave it back a second time. It is counted again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:03:33 +03:00
Vladimir Dubrovin
e3ef7bb54a ftppr: the server's answer to USER reaches the client (#1284)
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
A server which logs in on USER alone (230, anonymous FTP often does) or
refuses it had its answer read but not handed back: ftplogin() gave the
length of what it read only for the answer to PASS. The client, told
331 by ftppr and waiting for the answer to its PASS, waited until the
time out. The answer to USER is the client's now when there is no PASS.

The test case logs in through ftppr to a server answering 230, a
multiline 230, 331 then 230, 530, and 331 then 530.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 17:39:25 +03:00
Vladimir Dubrovin
359ce5f8b4 -B: the TLS ClientHello split at the SNI, which -s is taken for splice
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
tlspr splits the ClientHello it passes in the middle of the server name
(SNI) when the service has s_option, set by -s. In builds with splice()
(Linux, the default there) -s is splice for every service but UDP ones,
admin and httpsrv, so the split was never made, and the How-To's SNI
break did nothing on Linux.

-B sets it whatever the build: for TCP services but admin and httpsrv,
where s_option has a meaning of its own. -s still does it where it is
not splice. The How-To, 3proxy.cfg(5) and tlspr(8) give -B.

Fixes #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:52:39 +03:00
Vladimir Dubrovin
65213b172d Outgoing connections bound with IP_BIND_ADDRESS_NO_PORT
The socket of a connection to the destination is bound to the external
address before connect(), with port 0 unless a range is given. bind()
then takes an ephemeral port for the socket alone: not one in use
towards another destination, nor one of a connection still closing in
TIME_WAIT, which connect() would reuse. With many connections the range
runs out and bind() fails, result 12, or searches long for a port: 7 ms
a call was seen. The more connections are open at once, the sooner:
with event loops, serving more of them at once, the client of every
tenth connection or so of a saturated relay got its end at once.

Where the system has IP_BIND_ADDRESS_NO_PORT (Linux 4.2), the port is
left to connect() for a socket that is going to connect: chosen for
the pair with the destination, together with IP_LOCAL_PORT_RANGE when
a range is given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:34:14 +03:00
Vladimir Dubrovin
237a4499fa ftppr: the data socket towards the client from the socket functions
It was made with socket(): in the main network namespace rather than
the -ni one the client is reached in, and past the socket functions of
the service, which the rest of its life went through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7718ffa9f7)
2026-10-24 20:55:40 +03:00
Vladimir Dubrovin
2d06b4672a Connect back: a connection one client takes leaves the others waiting
The clients of a -R service all wait for the connection back on the
same listening socket, which was blocking: one connection woke them
all, one took it, and the others were left in accept() until the next,
past their time out and through a reload. The socket is non-blocking,
and an accept() which finds nothing, the connection taken by another
client or gone, is not one of the client's tries. nonblock() makes a
socket non-blocking, acceptagain() tells such an accept().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 126c7d00ec)
2026-10-24 20:55:40 +03:00
Vladimir Dubrovin
f4c5c723bd TLS: a read needing to write, or a write to read, waits for it
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
Update version files / version (push) Has been cancelled
A read TLS could do only once it wrote, or a write once it read, was an
error which ended the connection, as when TLS 1.3 answers a key update
while the socket cannot take more. Each TLS socket remembers what a read
and a write it could not do wait for, and the poll of the TLS layer
waits for it instead of what was asked: whatever that gets is the
caller's chance to try again, revents what it asked for, and its events
are given back as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:15:05 +03:00
Vladimir Dubrovin
6ec8a4d1ac FilePlugin: socksend() given no clientparam, not a TLS state for one
socksend() takes the client's parameters, and was passed the socket
functions' state instead: NULL, the global functions used then, as
intended, but on a TLS connection the state of its TLS layer, which
socksend() read as a clientparam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 718798c7fc)
2026-10-24 20:06:58 +03:00
github-actions[bot]
21b077364a Update version files for 1.1.0-pre1 2026-10-24 13:18:56 +00:00
Vladimir Dubrovin
532408a1b4 Release as 1.1.0-pre1 2026-10-24 16:18:42 +03:00
Vladimir Dubrovin
451b9b4c28 Release builds on Ubuntu 26.04: rpm signing, GNU install for OpenWrt
ubuntu-latest is 26.04 now. Its rpm 6 runs %__gpg_sign_cmd without
taking the word after %{__gpg} for argv[0], as rpm 4 does, so the
command of our own broke there; rpm's own is given the passphrase file
and the digest through %_gpg_sign_cmd_extra_args and %_gpg_digest_algo
instead, which both read. Each rpm is then checked by the rpm of the EL
release it is for, against 3proxy-release-key.asc.

The OpenWrt SDK refuses an install which is not GNU's. 26.04 has the
Rust coreutils, and GNU's as gnuinstall and the like: gnuinstall is
linked as install ahead in PATH. ipk also reads DEVEL when there is no
RELEASE, so it can be run on the development branch.

workflow_dispatch takes a sign input, to sign the packages as a release
does without uploading anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:42:20 +03:00
Vladimir Dubrovin
9ce5de971f Release workflows: a minimal set for a release of the development branch
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
A release whose tree has DEVEL and no RELEASE is one of the development
branch (release-kind.yml, called by the workflows below). For it only
the Windows x64 zip, the x86_64 and arm64 deb packages and the rpm for
EL 9 and 10 (x86_64 and arm64) are built and added to the release: no
Docker images, no source tarball, no ipk, armhf, watcom, win32 or
Windows arm64 builds. docker.yml makes it a prerelease, not "Latest",
which also keeps it out of the package repository 3proxy.github.io
builds from releases.

The packaging reads the version from RELEASE or DEVEL. rpm names the
source archive and its directory after the spec's Version, which for
2.0.0-devel is 2.0.0~devel, so that %setup finds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:10:59 +03:00
Vladimir Dubrovin
7fab9c488d Reattach: a new service's slot for its next client taken before it is found
A TCP service keeps one of its maxconn slots for the next client it
accepts, but took it only once its thread reached the accept loop. A
reload puts the new service on its listener (mainclaim(), in the thread
reading the configuration) before its thread even starts, and the
clients of the old services move to it as soon as they see it there: in
that window they could take every slot, the one it keeps included. The
reattach test, with maxconn 2, then had both of two downloads go on
instead of one, now and then on a loaded runner, every time with a
300 ms sleep put in the window.

mainclaim() takes the slot first; srvrun() takes it over, and
srvdiscard() gives it back for a service which never ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:00:29 +03:00
Vladimir Dubrovin
2052dc0a4f Logs: a thread and a queue for each log; build-time defaults
Once 3proxy runs (after the first configuration is in force, so after a
daemon's fork), each logger has a thread of its own writing from a
queue, and a client never waits for a log: ODBC, RADIUS, a full disk or
a pipe nobody reads hold up only that log's thread.

A queued line is fixed size: the part of struct clientparam logging
reads (everything before the filters: addresses, counters, names and
credentials, which hold no pointer it follows but srv), the time it was
logged, and its text, 256 bytes inline or a copy of its own if longer.
The thread formats it: struct tm is per call now, timechanged() and
dologname() included, and the type's format() takes the time.

minlogqueue (64) slots are allocated as the thread starts and kept,
maxlogqueue (4096) lines are queued at most. A line which finds the
queue full is dropped and counted on the last one queued; once that is
written, "Log X has its queue full, N records dropped" goes to where
errors go. A log which fails is no longer written to the error log
instead: its lines are dropped and counted while it is retried every
minute, and the count is told when it is open again, or when it was
reopened after failed writes.

A service which ended is stopped at once (srvstop(): sockets, listener,
its clients gone), but freed (srvrelease()) through its log's queue,
behind the lines which refer to it and to its configuration: the event
is part of struct srvparam, so a full queue never drops it. logfinish()
writes what is queued, 10 seconds at most, also as 3proxy exits on Unix.
Standalone services write as before, as each line comes.

LOGQUEUEMIN, LOGQUEUEMAX, DEFAULT_MAXCHILD, DEFAULT_POOLMAX,
DEFAULT_POOLTIMEOUT and POOLPERSERVICE (the automatic thread pool cap
for each service, which was 32 in the code) can be set with -D at build
time.

Tests: a log nobody reads (a FIFO) holds no client up, drops past its
queue and says so, and lines queued for a service a reload removed are
written as it formatted them; /dev/full says what it dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:41:48 +03:00
Vladimir Dubrovin
a3c417c82a Logs: the error of opening a log returned, not passed in errno
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
On Windows proxy.h makes errno WSAGetLastError(), which cannot be
assigned, so the Windows build failed, and the error of fopen() would
not have been read there either. A type's init returns the errno value
(the C library's, read with _get_errno() on Windows), and logget() gives
it to its caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:20:56 +03:00
Vladimir Dubrovin
30e2e34dd6 Sockets: sends with MSG_NOSIGNAL where the system has it
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>
2026-10-23 16:23:04 +03:00
Vladimir Dubrovin
83d8138818 Tests: udploop's silence counted from the last answer
The last datagram's time was taken after the sleep following it, half a
second and a slow runner's overshoot late, so a session ending on time
looked early. The test resolver no longer raises when its socket is
closed under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:56:38 +03:00
Vladimir Dubrovin
e619a105b4 MITM: a serial number of its own when wolfSSL cannot copy the source's
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>
2026-10-23 13:56:38 +03:00
Vladimir Dubrovin
f9b56fc15d SIGPIPE ignored before any service starts, and in standalone services
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>
2026-10-23 13:56:38 +03:00
Vladimir Dubrovin
477a2d3822 Logs: an ODBC log named by its data source, user and password its parameters
The string up to the first comma names an ODBC log; the user and the
password are kept apart as its parameters, never shown, and do not tell
logs apart. Another log or -l naming the same data source with other
parameters gives them to the logger, which reconnects with them. The
password is whatever follows the comma after the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 13:08:06 +03:00
Vladimir Dubrovin
e87ffcd7e1 Logs: no ODBC user and password in messages about a log
A log string is shown only up to its first comma, where an ODBC string
goes on with the user and the password: in the message on a log which
failed, and in the errors on a log or -l which cannot be opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:47:23 +03:00
Vladimir Dubrovin
d613750b94 Logs: a logger for each log string, shared and closed once unused
A logger (struct logger) is identified by the string log or -l gives it:
"" stdout, "/dev/null" none, "@IDENT" syslog, "&DSN" ODBC, "radius",
anything else a file. Each type (struct logtype) has init, formatting of
a connection into a buffer, writing, close, and a periodic tick for
rotation and flushing; ODBC and RADIUS register their types from 3proxy,
which alone has them. Services and configurations hold the logger they
use; the last to let go closes it, except the standard ones (none,
stdout, stderr, syslog), which are opened at startup and kept.

-l takes every log type now, and a file named by several services or by
both log and -l is opened once. rotate and archiver are bound to the
logger of the last log command when its configuration comes into force,
and every file logger is rotated by the main loop.

Errors with no connection go to a new stderr logger; after daemon, or as
a Windows service, stdout logging writes nothing and errors go to the
last log configured. A log whose writes keep failing is closed and
reopened; while it cannot be, its lines go to stderr and it is retried
every minute. ODBC's own reconnection throttle gives way to this.

Formatting (logbuf) is separate from counting traffic and clearing the
statistics, done once by logdo(), the services' log function, so a later
step can format or copy a connection's line and write it from a thread
of its own. dologname() and timechanged() move to log.c, which the
standalone services link as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:44:55 +03:00
Vladimir Dubrovin
57818ad497 UDP loop: deadlines in a heap, on a monotonic clock
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
The loop went through every session three times a turn: for the nearest
deadline, to register its sockets with epoll, and for the deadlines come
and the sessions ended. A datagram cost as much as the sessions there
were, idle ones too.

- The deadlines are in a binary heap, the nearest at its top: the wait
  is until the top, and the sessions timed out are taken from the top
  down to the first not due. A datagram only moves a session's deadline
  later, which leaves the heap as it is: the session is looked at again
  when its old deadline comes, and put back where its deadline is then,
  once in its timeout however many datagrams it had. A deadline set
  earlier by a handler goes up the heap.
- A session ended is put on a list and let go at the end of the turn,
  and its sockets are registered with epoll after the loop or a handler
  had it, not every session every turn.
- Deadlines are milliseconds of monotime(), CLOCK_MONOTONIC (the
  performance counter on Windows), which the time of day being set does
  not move: before, a clock set back kept sessions for as long, and one
  set forward ended them all. The loop reads it once a turn, in u->now.

With epoll, a datagram costs the same with 20000 idle sessions as with
none (38-40 us of the proxy's CPU a round trip, ARM), where it took 59
before. poll() elsewhere still builds its set every turn.

Tests: a session heard from every half second lives through two seconds
of idle timeout, and ends two seconds after the last datagram.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:53:06 +03:00
Vladimir Dubrovin
f69eb13e9a sockrecvfrom: poll again when a readable socket has nothing to read
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
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>
2026-10-22 16:03:21 +03:00
Vladimir Dubrovin
2514fb3dbf TLS: a part of a record read ahead is not pending data
SSL_has_pending() is true when only a part of a record has been read
ahead, which is no data yet. ssl_poll() took it for data: poll()
returned at once, over and over and without its timeout, until the rest
came, so a peer sending half a record kept a thread spinning and its
connection from timing out. ssl_pending() is now true only when
decrypted data waits, or when what was read ahead makes a whole record
(an SSL_peek() of one byte, the sockets not blocking); otherwise the
socket is polled for the rest.

splice() over kernel TLS still needs to know of any byte OpenSSL holds,
a part of a record too: ssl_buffered() tells it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:05:02 +03:00
Vladimir Dubrovin
3d20e1104e resolver: per service, defaultresolve, fakeresolve or sysresolve
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
Each service resolves names the way the last of defaultresolve (the name
servers in force, configured or the system's with the hosts file),
fakeresolve (127.0.0.2) and sysresolve (the C library) read before it
says; the last one in the configuration also applies outside services.
defaultresolve is the new command, and the default.

The name servers and the hosts file no longer depend on the mode: nserver
lines, or the system's, are always in force, so dnspr keeps forwarding;
with fakeresolve it does not, as before. fakeresolver moves to common.c,
which the standalone services link without resolve.c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:45:48 +03:00
Vladimir Dubrovin
994526dbc4 resolver: sysresolve, names resolved by the system routines
The command keeps the C library's resolution which a configuration
without nserver had before the system's name servers became the
default: no hosts file imported. nserver lines, if any, are still
there for dnspr to forward to. Of sysresolve and fakeresolve the last
given applies.

Tests: sysdns, with sysresolve dnspr has no hosts file names and a
name is resolved through the system routines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:02:37 +03:00
Vladimir Dubrovin
d5e78a0900 resolver: the system's name servers and hosts file by default
Without nserver (or fakeresolve) names were resolved by the C library.
3proxy's own resolver is now the default, with the system's name
servers: up to 3 from /etc/resolv.conf, on Windows from the registry
(set for the system, set for an interface with an address, Tcpip6
interfaces, then those DHCP gave). With none found, the C library as
before. nserver takes up to 3 as well (MAXNSERVERS), a fourth is
warned about.

The name servers in force belong to the process, not a configuration:
nserver lines are kept aside while a configuration is read and replace
them only once it is committed, so a reload resolves with those in
force until then. Resolutions and dnspr read a copy of them.

The system's are read at start, before the configuration and a chroot
in it, and are in force while it is read, so its host names resolve
with them; read again at a commit, and checked every 5 seconds from
the main loop while the default is in force. What cannot be read then
(within a chroot) stays as last read.

The hosts file, read with them, is imported into tables of its own
while the default is in force: its first address of a family for a
name. A name is looked up as one nsrecord made first, then the hosts
file, then the name cache, then name servers. A changed file replaces
what was imported. No name cache is made for it: without nscache
nothing is cached, as before.

The name cache's keys are host names of any case now: DNS names are.

Tests: sysdns, a name of the hosts file answered by dnspr without
nserver, in any case, and not with one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:49:21 +03:00
Vladimir Dubrovin
8517a5675b proxy: no variable named small, a macro for char on Windows
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
rpcndr.h, which windows.h brings in, defines small as char, so the
buffer the rest of a request is read into, unsigned char small[256],
did not compile with MSVC. Renamed, and the splice heuristic's small
in mainconf.c too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:50:42 +03:00
Vladimir Dubrovin
8cc7d255af connect back: a replaced -r service stops waiting at the reload
A -r service waited up to 60 s for the go-ahead on the connection it
made, without looking at a reload. One replaced by a reload went on
waiting, holding a slot and a thread, and took a client if the far
side's go-ahead came in that time; every reload left another such
connection in the far side's queue. The wait is now a second at a time
(60 s in all, as before): at a reload the service takes a go-ahead
already there, its client going on in the service which replaced it,
and otherwise closes the connection and stops.

The far side (-R) sent its go-ahead to any connection it accepted, and
a closed one cost it a client. A connection the -r side closed, at a
reload or after its wait, is readable before any go-ahead, since that
side sends nothing first: once past the ACL it is passed over, without
counting as one of the tries for as many as the listen queue holds
(1 + maxchild/16), so that connections made to be closed or to send
something cannot keep a client's thread in the loop.

Tests: reattach's far side passes over closed connections as -R does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:28:02 +03:00
Vladimir Dubrovin
63a736c693 proxy: request line logged up to 4 KB, added headers written in place
REQMAX is 4 KB: a log line holds 4000 characters, so a longer copy of
the request line was cut by the log before "..." could show.

The headers this proxy adds are written straight after the header
block, HDRRESERVE (2 KB, what they come to at most) asked to be free
there first: in the side's buffer that is the room hdrread() left, so
nothing is copied, and the buffer on the stack they were built in is
gone.

The scheme and authority of a request are no longer kept in a buffer
of their own: req, the line in absolute form as logged, has them, and
where the authority begins and ends in it is kept instead, for the
next request on the connection to be compared with and for a parent
proxy to be given. An authority beyond what req keeps of the line is
refused. A line a request filter rewrote is logged only when the
rewrite is applied.

Tests: a 5 KB line through a parent arrives whole in absolute form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:20:45 +03:00
Vladimir Dubrovin
96adc6d9cd proxy: the line for a parent put back in the buffer, req only logged
A request to a parent HTTP proxy went as two writes, the line as the
client sent it from req and then the rest of the headers from buf,
held together with TCP_CORK, which does not help over TLS nor with
headers bigger than a segment. The line now goes back to absolute form
where it is: a request's header block is read HDRFRONT (300) bytes
into clibuf, room for http://, a host name of 256 characters and a
port; the method moves down into it and the scheme and authority go
in after it, and line and headers are sent in one piece. Where they
do not fit, or the headers are in a buffer of their own by then, both
are put together in one. TCP_CORK is gone.

The scheme and authority of a request are kept for that (auth), from
its line or, for a request with a path only, its Host header; the last
one is what the next request on the connection is compared with. A
CONNECT line is kept whole in buf. The request filters are given the
line in absolute form in a buffer of their own, made only when there
are some, as req was. req is the copy logged and nothing else: at most
REQMAX (6 KB) of the line, "..." after what is cut, never a reason to
refuse the request.

The headers this proxy adds are built in a buffer on the stack, of
bounded parts, and put after the header block once there is room for
them, in a standalone proxy too, which never asked for it. HDRRESERVE
is 1 KB.

Tests: through a parent a POST arrives with its line in absolute form,
its headers and body whole; user and password in the URL; headers in a
buffer of their own (a data filter) put together with the line; a 7 KB
request line served whole and logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:14:35 +03:00
Vladimir Dubrovin
aee69bf679 proxy: headers read and rewritten in the connection's buffer
The HTTP proxy read each request into a buffer of 64 KB of its own,
cleared it with memset, and read the answer's headers into it again;
the header lines were copied there one by one from clibuf or srvbuf.
Now the header block of a request is read whole into clibuf, and that
of an answer into srvbuf, and worked on where it is (hdrread()):

- the block is moved to one byte into the buffer, and its lines, as
  they are kept, are moved down to its start, so the line being worked
  on, and the 0 written after it, never reach the next one; headers
  taken out are left behind;
- HDRRESERVE is left free after it, for the headers this proxy adds,
  before whatever came after it, a body or another request, from
  where the connection is read on;
- the buffer is grown only for a block bigger than it, up to the
  limits there were (512 KB for a request, 64 KB for an answer);
- the headers are copied to a buffer of their own only when they need
  more room than that, when a filter hands back one, when a body is to
  be read whole into the same buffer for a data filter, and for an ftp
  listing, which is written where the headers were.

Whether buf is the headers' own buffer is kept with it (bufown), not
found from where it points: the side's buffer may be a bigger one by
then let go of. filterswap() leaves alone a buffer in either side's
buffer, which may be one grown by malloc, not only their fixed arrays.

What took buf for a buffer of LINESIZE does no longer: the request is
logged from a copy of its line, and the rest of a request refused
before its headers were all read is drained through a small buffer of
its own, keeping track of where its lines begin.

Tests: a POST whose body comes in the same packet as its headers,
which are rewritten, with a header added and one taken out; 45 KB of
request headers; two requests pipelined in one packet; empty lines
before a request; an answer with a 30 KB header; a 600 KB header block
refused, the proxy answering after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:49:46 +03:00
Vladimir Dubrovin
d9b9d1b9b7 UDP loop: epoll on Linux
The UDP loop gave poll() every socket of its sessions each time round.
On Linux it now keeps them in an epoll set: the service's socket and
each session's socket and parent's control connection are registered
once and changed as the session changes, a socket replaced or other
events waited for, and an event leads straight to its session. A
handler replacing a session's socket marks it to be registered again,
as a closed socket leaves the set and its number may come back for the
new one; dnspr does so moving on to the next name server. Sessions
handed over on a reload leave the set of the old service's loop and are
registered by the one adopting them; the service's socket is left
unwatched while an older service still serves it. Elsewhere, or built
with NOEPOLL, poll() is used as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 18:57:22 +03:00
Vladimir Dubrovin
f5e646ba86 dnspr: only the answer to the query asked is relayed, or kept
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
Answers from the name servers now go into the shared name cache, so
they are checked before they are taken:

- a query goes to a name server under a random id of its own, the
  client's put back only in what is relayed to it;
- an answer over UDP is taken only from the address and port of the
  name server asked;
- the answer must have that id, be a response, and carry the question
  asked, byte for byte, over UDP and TCP alike;
- of its records only one owned by the question's name itself (a
  pointer to it) is kept in the cache;
- a name with a label of other characters than a host's, a '.' or a
  NUL among them, reads as another name once made text, so its answer
  is relayed but not kept.

Tests: an answer with another id is neither relayed nor kept, and a
name with a '.' inside a label is answered but not cached under the
name its text reads as.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:06:42 +03:00
Vladimir Dubrovin
58334cb51c dnspr: name servers asked from the loop, nothing waits in it
dnspr answered an A or AAAA query missing from the name cache with
udpresolve(), which waited for the name servers in place, and a name
server over TCP was waited for in place too, holding up every query of
the service. Now only the name cache is looked at in place; anything it
does not have goes to the name servers from the loop: over UDP the query
is sent on a socket the loop waits on, over TCP the socket is connected
without waiting, the query written once the loop sees it writable, and
the answer read in as many pieces as it comes. A name server silent for
DNS_TO is followed by the next one, as udpresolve() did; the last ends
the query with 821. An address in an answer is kept in the name cache,
unless -s says not to use it.

The UDP loop lets a session say what it waits for on its socket
(events) and a service handle a session's deadline itself (timeout).

Tests: an A query not in the cache answered by the name server and then
from the cache with the server gone, an A query waiting for a silent
name server while another is answered, a name server over TCP whose
answer comes in pieces, and the second name server asked when the
first is silent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:50:34 +03:00
Vladimir Dubrovin
f28552fc6f UDP loop: a session found by hash is checked against the client's address
The sessions of a keyed UDP service were found by a hash of the
client's address with a fixed seed, so an address crafted to collide
with another client's would have its datagrams taken as that client's.
The hash is now seeded with the table's random entropy, and a session
found is used only if its client's address, port and family are the
datagram's; otherwise the datagram is a new client's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:32:49 +03:00
Vladimir Dubrovin
5cbc72d289 Split proxymain.c into its parts
proxymain.c stays the one unit built for 3proxy and included by each
standalone service, and now includes its parts:

  mainconf.c	services from their configuration lines: options,
		srvprepare(), mainprepare(), srvinit(), srvfree(), and
		copying and freeing access lists and authentication
  mainlisten.c	listening sockets, shared and taken over on a reload,
		and reattach()
  mainclient.c	threads for the clients of TCP services, their pool,
		srvspawn(), releaseparam(), and the filters' hooks
  maintcp.c	the main loop of a TCP service, tcploop(), taken out of
		srvrun(), with tcpnext() and cblisten()
  mainudp.c	the main loop of a UDP service

proxymain.c keeps srvstart, srvrun() and main(), and declares the
functions of file scope of the parts so that they can be in any order.
The code is moved, not changed, but for tcploop(). The parts are
dependencies of the objects that include proxymain.c in Makefile.inc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:11:46 +03:00
Vladimir Dubrovin
58424770aa tests: udploop, UDP services in one thread
udppm: 20 clients at once each answered with their own datagrams, -s
ending a session with its first answer, STRING_LONG of silence ending
it (92), maxconn leaving a client without a slot unanswered, and a
session adopted on a reload and ended when the new rules deny its
client. dnspr: a query sent on to the name server and its answer
relayed, and a query answered while another waits for a silent name
server, which ends at DNS_TO (821).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:03:31 +03:00
Vladimir Dubrovin
7cdfd87aa6 UDP services in one thread: udppm and dnspr without client threads
A UDP service now runs in its own thread alone (udploop()): it reads the
service's socket and the sockets of its sessions towards their
destinations, keeps their deadlines, and answers every client from the
service's socket. A session is a struct clientparam as a client thread's
would be, with a slot of maxchild, among the service's children and
counted on its listener, but without a thread. The services supply
handlers (struct udpservice, childdef.udp): udppm keeps a session per
client address, forwarding its datagrams, behind the headers of a
parent chain, from a socket of its own and answering what comes back,
until STRING_L of silence or, with -s, the first answer; dnspr makes a
session of each query, answered at once from the fake address, the
name cache or the resolver, or sent on to the first name server over
UDP, whose answer is waited for in the loop up to DNS_TO (a name server
over TCP is still waited for in place). Authentication stays in the
loop for now; datagrams over a bandwidth limit, or of a client without
a free slot, are dropped.

On a reload the old service goes on serving the socket until it sees
its configuration replaced, then hands its sessions to the listener;
the newest service of its kind there, which until then does not read
the socket so that no client gets two sessions, adopts them: a slot of
its own, the old one given back, its filters and limits, and
re-authentication. A session is ended with 26 if the newest service is
of another kind, 27 without a slot, or with what the authentication
says; sessions nobody is left to adopt are ended by the last service to
stop on the listener.

On Windows a send on a socket waited while another thread polled it,
which delayed udppm's answers by up to a second, and old UDP services
did not exit after a reload; with one thread per socket neither
happens, and the second socket bound to the service's address
(NO_SHARE_UDP_SOCKET) is gone from the code and the build files.

udpinit, the global udp_table, udpnext(), udppmchild() and
dnsprchild() are gone; udpsockmap() serves SOCKS5 UDP ASSOCIATE only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:57:35 +03:00
Vladimir Dubrovin
7efc23cdf2 Revert "udppm: answer through a socket of the session's own without SHARE_UDP_SOCKET"
This reverts commit be2ad06dba.
2026-10-18 13:25:55 +03:00
Vladimir Dubrovin
537d089be8 tests: parent_ports tells an ephemeral port from the window by several
On Linux the port windows lie within net.ipv4.ip_local_port_range, as
the kernel honours no other, so the ephemeral port of a request the
range does not apply to falls into the 50-port window now and then
(CI saw 34001 in 33968-34017). The case now makes five such requests
and wants one of them outside the window: bound to it, all would be in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:58:43 +03:00
Vladimir Dubrovin
be2ad06dba udppm: answer through a socket of the session's own without SHARE_UDP_SOCKET
A build which does not share the UDP listening socket (NO_SHARE_UDP_SOCKET,
which Makefile.win, Makefile.watcom and CMake set for Windows, as Windows
does not take a recv and a send on one socket from two threads) has
dnspr answer from a socket of its own, bound to the service's address,
but udppm still answered through the service's socket. Since sockets
are taken over on a reload, that socket is now also read by the new
services while old sessions write to it. A udppm session in such a
build now makes its own socket too, as dnspr does and as udppm did
before; it is only written to, and closed when the session ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:51:09 +03:00
Vladimir Dubrovin
5c19e6368f Unix sockets: remove on closing only the file this socket bound
When a reload cannot take a listening Unix socket over (a bind-time
-ol option changed), the new service binds the path anew, and the old
listener, closing after it, removed the file, the new socket's by then,
leaving nothing to connect to. The socket's file (device and inode) is
now noted on binding and the path removed on closing only if it is
still that file.

structures.h includes <stddef.h> for offsetof() in SASIZE(), which
needs it wherever the header is used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:38:11 +03:00
Vladimir Dubrovin
9612bbdd22 Unix sockets: no addresses on streams, abstract names, tcppm port 0
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
On Linux a proxy listening on a Unix socket relayed only the first
bytes of an answer (result 95): 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.

A test case covers a proxy on a Unix socket and on an abstract one on
Linux, tcppm to a Unix socket, a parent reached through one, and a
listening Unix socket across reloads: taken over each time, its file
kept, and removed once no service listens on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:05:27 +03:00
Vladimir Dubrovin
6428c4423f tests: reload cases that hold on slow runners
The downloads spanning a reload asked httpsrv for 64 KB blocks 60 ms
apart, meant as two seconds; but httpsrv sends at most 8 KB a block, so
they took 245 waits, some 15 seconds, and on the CI runners, whose
timers stretch, more than the 20 the cases waited. They now ask for
400 KB in 8 KB blocks 40 ms apart, two seconds indeed, and are waited
for up to a minute.

The UDP part of the takeover case slept 1.5 s after the reload adding
its udppm and then counted every datagram unanswered as lost; it now
asks until the service answers first, and wants more than 20
datagrams through the reloads rather than 100, what a slow runner can
send. That none is lost is the check.

On a failure, run.py prints every log line with a result code other
than 0, not only the last lines, so the reason a connection ended is
shown wherever it was logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 11:25:07 +03:00
Vladimir Dubrovin
67a73a4132 Count the traffic of each listener
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
A listener counts the traffic of its clients as it is logged: bytes from
servers (%I) and from clients (%O). clearstat(), which every log writer
calls once a line is logged, as well as lognone and logradius, adds the
counts to the client's listener before clearing them, through
listenertrafficfunc, set where services have listeners and NULL in the
standalone binaries.

The admin services page shows them as listenertrafin and
listenertrafout. The takeover case checks that they go on through three
reloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:02:14 +03:00
Vladimir Dubrovin
6e12c2ce63 Name the HTTPSRV service in the string table
S_HTTPSRV, the type of admin and httpsrv services, was added before
S_ZOMBIE without a name in the string table, so its index held
"ZOMBIE": the admin services page, %N in logs and RADIUS named admin
and httpsrv services and their clients ZOMBIE, and a zombie service had
no name at all (NULL). Index 21 is now "HTTPSRV", 22 "ZOMBIE"; 22 was
unused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 15:36:13 +03:00
Vladimir Dubrovin
113c9eaebe Count the clients of each listener
A listener counts its clients, whichever of its services they came to,
old or new: those connected now, raised when a client is spawned, and
those done with, raised as one leaves its service. As the listener
outlives a reload, so do the counts.

The services page of the admin interface shows them for each service as
listenerclients and listenerprocessed; services sharing a socket show
the same counts. The takeover case checks that the count goes on
through three reloads and that no client is left counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:52:06 +03:00