diff --git a/src/common.c b/src/common.c index 6df4f19..1d1f534 100644 --- a/src/common.c +++ b/src/common.c @@ -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; diff --git a/src/proxy.c b/src/proxy.c index e58e31a..b87a1df 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -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(¶m->sinsl, 0, sizeof(param->sinsl)); memset(¶m->sinsr, 0, sizeof(param->sinsr)); - if((res = (*param->srv->authfunc)(param))) RETURN(res); + if((res = clientauth(param))) RETURN(res); } diff --git a/src/proxy.h b/src/proxy.h index 751d546..997893f 100644 --- a/src/proxy.h +++ b/src/proxy.h @@ -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); diff --git a/src/proxymain.c b/src/proxymain.c index 9386e81..bf32058 100644 --- a/src/proxymain.c +++ b/src/proxymain.c @@ -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; diff --git a/src/sockmap.c b/src/sockmap.c index 57082b8..509fb44 100644 --- a/src/sockmap.c +++ b/src/sockmap.c @@ -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)){ diff --git a/src/udpsockmap.c b/src/udpsockmap.c index 10f90cd..e5769e8 100644 --- a/src/udpsockmap.c +++ b/src/udpsockmap.c @@ -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; diff --git a/tests/cases/reattach.py b/tests/cases/reattach.py new file mode 100644 index 0000000..05da5c1 --- /dev/null +++ b/tests/cases/reattach.py @@ -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)") diff --git a/tests/cases/reload.py b/tests/cases/reload.py index 3cf3048..05ded98 100644 --- a/tests/cases/reload.py +++ b/tests/cases/reload.py @@ -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)