mirror of
https://github.com/3proxy/3proxy.git
synced 2026-10-17 08:55:49 +08:00
After a reload, unless noforce, a client of an old service goes by the rules of the newest service on its listener, the one accepting there now. It takes one of that service's maxconn slots, which counts it there and keeps that service and its configuration alive until the client is done, and is re-authenticated as its client: access lists, users, authentication, bandwidth limits and traffic counters come from the new configuration. A later reload moves it on, giving the slot back. The client stays its own service's child, which keeps its filters and TLS state. It is dropped if its service was removed or is of another type now (26), or if the newest service has no free slot (27); what the re-authentication refuses ends it as before. A service without a listener (UDP, connecting back) keeps its clients as they are. This restores what force did before the configuration was kept per service: since then a client compared its version with its own service's configuration, which never changes, and was never checked again. Services now take their configuration's version as it is: a reload numbers the new configuration before reading it, and so does startup now, where services were numbered one ahead. A listener now stays with its old service until that service's last client is gone, those clients looking the newest service up through it; the socket itself is still closed as soon as nobody accepts on it. Connection limits are given back in the configuration they were taken in. PSRV() is the service whose rules apply to a client. The reload case checks that a download of a user the new configuration keeps finishes under it, and that one of a user it removes is cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""Reloading the configuration: a file with an error leaves the configuration
|
|
in force, services and users, as it was; a good one replaces it, while the
|
|
connections of the old services finish undisturbed."""
|
|
|
|
import threading
|
|
import time
|
|
|
|
|
|
def config(origin, proxy, adm, user, extra=""):
|
|
return f"""
|
|
log
|
|
auth iponly
|
|
allow *
|
|
http data * /data
|
|
httpsrv -p{origin}
|
|
|
|
flush
|
|
users {user}
|
|
auth strong
|
|
allow *
|
|
proxy -p{proxy}
|
|
|
|
flush
|
|
auth iponly
|
|
allow *
|
|
admin -p{adm}
|
|
{extra}
|
|
"""
|
|
|
|
|
|
def run(t):
|
|
origin = t.free_port()
|
|
first = t.free_port()
|
|
second = t.free_port()
|
|
adm = t.free_port()
|
|
|
|
server = t.start("reload", config(origin, first, adm, "alice:CL:one"),
|
|
ports=[origin, first, adm])
|
|
url = f"http://127.0.0.1:{origin}/data?size=1000"
|
|
proxy = f"127.0.0.1:{first}"
|
|
|
|
def as_user(user, password, through=proxy):
|
|
return t.http(url, proxy=through, proxy_auth=(user, password)).status
|
|
|
|
t.eq(200, as_user("alice", "one"), "the first configuration lets alice in")
|
|
|
|
def reload_and_wait(expect_failure=False):
|
|
before = server.output()
|
|
accepting = before.count("Accepting connections")
|
|
t.http(f"http://127.0.0.1:{adm}/R")
|
|
deadline = time.time() + 10
|
|
while time.time() < deadline:
|
|
out = server.output()
|
|
if expect_failure and "Unknown command" in out[len(before):]:
|
|
return out
|
|
if not expect_failure and out.count("Accepting connections") >= accepting + 3:
|
|
return out
|
|
time.sleep(0.05)
|
|
return server.output()
|
|
|
|
# --- a file with an error: nothing changes --------------------------
|
|
t.write_config("reload", config(origin, first, adm, "bob:CL:two", "nosuchcommand"))
|
|
out = reload_and_wait(expect_failure=True)
|
|
t.contains(out, "Unknown command", "the broken configuration is refused")
|
|
time.sleep(1.5) # what a switch would take: the old services stop within a second
|
|
t.eq(200, as_user("alice", "one"), "after a failed reload alice still gets in")
|
|
t.ne(200, as_user("bob", "two"), "and bob, from the refused file, does not")
|
|
|
|
# --- a good file: the new configuration takes over, and the connections
|
|
# of the old services go by its rules from then on ---------------------
|
|
def download(result):
|
|
# about two seconds: 2 MB in 64 KB blocks, 60 ms apart
|
|
r = t.http(f"http://127.0.0.1:{origin}/data?size=2000000&block=65536&delay=60",
|
|
proxy=proxy, proxy_auth=("alice", "one"))
|
|
result["status"], result["length"] = r.status, r.length
|
|
|
|
def during_reload(users, port):
|
|
result = {}
|
|
th = threading.Thread(target=download, args=(result,))
|
|
th.start()
|
|
time.sleep(0.3)
|
|
t.write_config("reload", config(origin, port, adm, users))
|
|
reload_and_wait()
|
|
return th, result
|
|
|
|
# alice stays: her download goes on under the new service
|
|
th, kept = during_reload("alice:CL:one bob:CL:two", first)
|
|
t.eq(200, as_user("bob", "two"), "the new configuration lets bob in")
|
|
th.join(20)
|
|
t.eq(2000000, kept.get("length"),
|
|
"a download of a user the new configuration keeps finishes whole")
|
|
|
|
# alice is removed: her download is cut, being re-authenticated
|
|
th, cut = during_reload("bob:CL:two", first)
|
|
th.join(20)
|
|
t.ok("a download of a user the new configuration removes is cut") \
|
|
if cut.get("length", 0) < 2000000 else \
|
|
t.fail("a download of a user the new configuration removes is cut",
|
|
"less than 2000000 bytes", cut.get("length"))
|
|
t.ne(200, as_user("alice", "one"), "and alice cannot come back")
|
|
|
|
# the service moves to another port: the old one closes
|
|
t.write_config("reload", config(origin, second, adm, "bob:CL:two"))
|
|
reload_and_wait()
|
|
t.ok("the moved service listens on its new port") if t.wait_port(second) else \
|
|
t.fail("the moved service listens on its new port", f"port {second} open", "closed")
|
|
t.eq(200, as_user("bob", "two", f"127.0.0.1:{second}"), "bob gets in there")
|
|
deadline = time.time() + 5
|
|
while time.time() < deadline and t.wait_port(first, timeout=0.2):
|
|
time.sleep(0.2)
|
|
t.ok("the old port is closed") if not t.wait_port(first, timeout=0.2) else \
|
|
t.fail("the old port is closed", f"port {first} closed", "open")
|