Apply the new configuration to UDP sessions and kept-alive requests

UDP sessions check for a reload in udpsockmap() as TCP connections do
in sockmap(): unless noforce, a session goes by the newest service on
its listener, re-authenticated as its client, or is dropped.

A request on a connection that outlives a reload, an HTTP one kept
alive, is authorized by clientauth(): it moves the connection to the
newest service on its listener first if a reload came since, then
authorizes as the service whose rules apply (PSRV). The proxy uses it
for every request, so a request of a user the new configuration
removed is answered 407 rather than authorized by the old rules and
cut while relayed. reattach() takes whether to authenticate: sockmap()
and udpsockmap() move and authenticate at once, clientauth() moves and
then authorizes the request.

A test case covers the drops: with one slot free in the new service one
of two downloads moves on and the other is cut (27; a service keeps one
of its maxconn slots for the next connection it accepts), and a proxy
download is cut when a socks service takes its port (26). The reload
case checks the kept-alive request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vladimir Dubrovin 2026-10-15 00:46:20 +03:00
parent df09565fe4
commit ae27a5cf90
8 changed files with 137 additions and 13 deletions

View File

@ -233,7 +233,26 @@ struct extparam confboot = {
whatever runs meanwhile without a service of its own. */
struct extparam *conf = &confboot;
struct extparam *runconf = &confboot;
int (*reattachfunc)(struct clientparam *param) = NULL;
int (*reattachfunc)(struct clientparam *param, int auth) = NULL;
/* Authorizes a request on a connection which may outlive a reload, an HTTP
one kept alive: as a client of the service whose rules apply now, the
newest one on its listener after a reload (unless noforce). */
int clientauth(struct clientparam *param){
struct srvparam *orig;
int res;
if(param->version < runconf->version){
if(param->srv->noforce || !reattachfunc) param->version = runconf->version;
else if((res = (*reattachfunc)(param, 0))) return res;
}
if(!param->newsrv) return (*param->srv->authfunc)(param);
orig = param->srv;
param->srv = param->newsrv;
res = (*param->srv->authfunc)(param);
param->srv = orig;
return res;
}
_3proxy_mutex_t confref_mutex;

View File

@ -641,7 +641,7 @@ for(;;){
if(param->srv->needuser > 1 && !*param->username) {RETURN(4);}
if((res = (*param->srv->authfunc)(param))) {
if((res = clientauth(param))) {
if (res <= 10 || haveconnection || param->transparent) RETURN(res);
param->srv->so._closesocket(param->sostate, param->remsock);
param->remsock = INVALID_SOCKET;
@ -649,7 +649,7 @@ for(;;){
param->redirtype = 0;
memset(&param->sinsl, 0, sizeof(param->sinsl));
memset(&param->sinsr, 0, sizeof(param->sinsr));
if((res = (*param->srv->authfunc)(param))) RETURN(res);
if((res = clientauth(param))) RETURN(res);
}

View File

@ -400,9 +400,10 @@ void freeauth(struct auth *);
void freefilter(struct filter *filter);
void freeconf(struct extparam *confp);
void confbegin(void);
int reattach(struct clientparam *param);
int reattach(struct clientparam *param, int auth);
/* reattach() where configurations are reloaded, NULL elsewhere */
extern int (*reattachfunc)(struct clientparam *param);
extern int (*reattachfunc)(struct clientparam *param, int auth);
int clientauth(struct clientparam *param);
struct passwords * copypwl (struct passwords *pwl);
void freepwl(struct passwords *pw);
int copyfilter(struct filter *, struct srvparam *srv);

View File

@ -1653,13 +1653,13 @@ static void slotrelease(struct srvparam *srv){
/* After a reload, unless noforce: a client goes by the rules of the newest
service on its listener, the one now accepting there, re-authenticated
as it. It takes one of that service's maxconn slots, which counts it
there and keeps the service, and its configuration, until the client is
done; a slot it held in a service before is given back. 0 to go on, or
the code to end the connection with: 26 if its service was removed, or
is another one now, 27 if the newest has no slot free, or what the
authentication says. */
int reattach(struct clientparam *param){
as it if auth is set. It takes one of that service's maxconn slots,
which counts it there and keeps the service, and its configuration,
until the client is done; a slot it held in a service before is given
back. 0 to go on, or the code to end the connection with: 26 if its
service was removed, or is another one now, 27 if the newest has no slot
free, or what the authentication says. */
int reattach(struct clientparam *param, int auth){
struct srvparam *cur = PSRV(param), *newest, *orig;
int version = runconf->version, res;
@ -1689,6 +1689,7 @@ int reattach(struct clientparam *param){
if(param->newsrv) slotrelease(param->newsrv);
param->newsrv = newest;
param->version = version;
if(!auth) return 0;
/* authenticated as the newest service's client: its access lists, users,
limits and counters */
orig = param->srv;

View File

@ -271,7 +271,7 @@ log(logbuf);
/* a reload: the client goes by the newest configuration's rules */
if(param->version < runconf->version){
if(param->srv->noforce || !reattachfunc) param->version = runconf->version;
else if((res = (*reattachfunc)(param)) && res != 2) {RETURN(res);}
else if((res = (*reattachfunc)(param, 1)) && res != 2) {RETURN(res);}
}
if((param->maxtrafin64 && param->statssrv64 >= param->maxtrafin64) || (param->maxtrafout64 && param->statscli64 >= param->maxtrafout64)){

View File

@ -215,6 +215,13 @@ int udpsockmap(struct clientparam *param, int timeo)
if (res < 0) return 481;
if (res == 0) return 92;
/* a reload: the session goes by the newest configuration's rules,
as a TCP connection does in sockmap() */
if (param->version < runconf->version) {
if (param->srv->noforce || !reattachfunc) param->version = runconf->version;
else if ((len = (*reattachfunc)(param, 1)) && len != 2) return len;
}
/* datagram from client */
if (p.cli >= 0 && p.fds[p.cli].revents) {
unsigned char *base = param->srvbuf + UDPHDROFF;

83
tests/cases/reattach.py Normal file
View File

@ -0,0 +1,83 @@
"""Connections of old services after a reload (unless noforce): each goes by
the rules of the newest service on its listener, taking one of its maxconn
slots, and is dropped where that cannot be."""
import threading
import time
def run(t):
origin = t.free_port()
proxy = t.free_port()
adm = t.free_port()
def config(service_line, maxconn=50):
return f"""
log
auth iponly
allow *
http data * /data
httpsrv -p{origin}
flush
maxconn {maxconn}
auth iponly
allow *
{service_line}
flush
maxconn 50
auth iponly
allow *
admin -p{adm}
"""
server = t.start("reattach", config(f"proxy -p{proxy}"), ports=[origin, proxy, adm])
def reload(conf):
before = server.output().count("Accepting connections")
t.write_config("reattach", conf)
t.http(f"http://127.0.0.1:{adm}/R")
deadline = time.time() + 10
while time.time() < deadline and server.output().count("Accepting connections") < before + 3:
time.sleep(0.05)
def downloads(n):
"""n downloads of about two seconds each through the proxy, started
now; the results fill in as they finish."""
results = [{} for _ in range(n)]
def one(r):
got = t.http(f"http://127.0.0.1:{origin}/data?size=2000000&block=65536&delay=60",
proxy=f"127.0.0.1:{proxy}")
r["length"] = got.length
threads = [threading.Thread(target=one, args=(r,)) for r in results]
for th in threads:
th.start()
time.sleep(0.4)
return threads, results
# --- the newest service has one slot free: one connection moves, one is
# cut. A service holds one of its maxconn slots for the next connection
# it accepts, so maxconn 2 leaves one.
threads, results = downloads(2)
reload(config(f"proxy -p{proxy}", maxconn=2))
for th in threads:
th.join(20)
whole = sum(1 for r in results if r.get("length") == 2000000)
t.eq(1, whole, "with one slot free in the new service one download goes on")
t.contains(server.output(), " 00027 ",
"the other is cut for want of a slot (27)")
# --- the service on the port is of another type now: cut (26)
reload(config(f"proxy -p{proxy}"))
threads, results = downloads(1)
reload(config(f"socks -p{proxy}"))
for th in threads:
th.join(20)
t.ok("a proxy download is cut when a socks service takes the port") \
if results[0].get("length", 0) < 2000000 else \
t.fail("a proxy download is cut when a socks service takes the port",
"less than 2000000 bytes", results[0].get("length"))
t.contains(server.output(), " 00026 ", "as its service is another one now (26)")

View File

@ -90,6 +90,19 @@ def run(t):
t.eq(2000000, kept.get("length"),
"a download of a user the new configuration keeps finishes whole")
# a connection kept alive: each request is authorized by the rules in
# force, so alice's next request after she is removed is refused
keep = t.connection("127.0.0.1", origin, proxy=proxy)
t.eq(200, t.http(url, proxy=proxy, proxy_auth=("alice", "one"), conn=keep).status,
"a kept-alive connection serves alice")
t.write_config("reload", config(origin, first, adm, "bob:CL:two"))
reload_and_wait()
r = t.http(url, proxy=proxy, proxy_auth=("alice", "one"), conn=keep)
t.eq(407, r.status, "after she is removed, her next request on it is refused "
"with a request for credentials, authorized by the new rules")
t.write_config("reload", config(origin, first, adm, "alice:CL:one bob:CL:two"))
reload_and_wait()
# alice is removed: her download is cut, being re-authenticated
th, cut = during_reload("bob:CL:two", first)
th.join(20)