Compare commits

...

7 Commits

Author SHA1 Message Date
Vladimir Dubrovin
94feaf2728 Fix -s for httpsrv
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
2026-10-11 21:20:58 +03:00
Vladimir Dubrovin
c827f77137 Support = in socket options, make TCP_NODELAY default 2026-10-11 20:56:49 +03:00
Vladimir Dubrovin
0e8d757464 Return splice support in default builds 2026-10-11 20:48:49 +03:00
Vladimir Dubrovin
f128fd2aa7 Fix socket draining / lingering 2026-10-11 19:04:53 +03:00
Vladimir Dubrovin
baea44477e Add benchmarks comparing 3proxy versions and other proxies
bench/build.sh builds git refs or the working tree, each with the
Makefile it shipped with. bench/bench.py runs the client, the proxy and
the origin in network namespaces of their own, linked by veth pairs,
and in cgroups pinned to separate CPUs, then measures TCP relaying
(iperf3) and HTTP proxying (wrk) for 3proxy builds, nginx, haproxy and
the kernel forwarding alone, reporting throughput, latency and the
proxy's CPU time and peak memory per unit of work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 19:03:09 +03:00
Vladimir Dubrovin
9ca0b84774 Tests: wait for the reload requested in the admin case
The reload asked for through /R happens on the next tick of the main
loop, and between the old services stopping and the new ones starting a
request is refused: on Windows the new listener cannot bind the port
until the old one has closed it, and retries with a growing delay. When
the tick fell inside the case, every request after it failed with
ConnectionRefusedError (MSVC run of 98e8dfb). The case now waits for
both services to have restarted after each /R.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:26:02 +03:00
Vladimir Dubrovin
36382bda96 Remove sleeps on proxy termination 2026-10-10 23:28:19 +03:00
19 changed files with 1039 additions and 108 deletions

View File

@ -63,7 +63,7 @@ option(3PROXY_USE_OPENSSL "Enable TLS/SSL support via OpenSSL (fallback when wol
option(3PROXY_USE_PCRE2 "Enable PCRE2 regex filtering" ON) option(3PROXY_USE_PCRE2 "Enable PCRE2 regex filtering" ON)
option(3PROXY_USE_PAM "Enable PAM/PamAuth" ON) option(3PROXY_USE_PAM "Enable PAM/PamAuth" ON)
option(3PROXY_USE_ODBC "Enable ODBC support (Unix only, always ON on Windows)" OFF) option(3PROXY_USE_ODBC "Enable ODBC support (Unix only, always ON on Windows)" OFF)
option(3PROXY_USE_SPLICE "Build Linux splice() support, slower than read/write for most traffic (Linux only)" OFF) option(3PROXY_USE_SPLICE "Build Linux splice() support, used by services given -s (Linux only)" ON)
option(3PROXY_USE_POLL "Use poll() instead of select() (Unix only)" ON) option(3PROXY_USE_POLL "Use poll() instead of select() (Unix only)" ON)
option(3PROXY_USE_WSAPOLL "Use WSAPoll instead of select() (Windows only)" ON) option(3PROXY_USE_WSAPOLL "Use WSAPoll instead of select() (Windows only)" ON)
option(3PROXY_USE_NETFILTER "Enable Linux netfilter support (Linux only)" ON) option(3PROXY_USE_NETFILTER "Enable Linux netfilter support (Linux only)" ON)

View File

@ -4,9 +4,9 @@
# add -DWITH_ODBC to CFLAGS and -lodbc to LIBS to compile with ODBC # add -DWITH_ODBC to CFLAGS and -lodbc to LIBS to compile with ODBC
# library support. Add -DSAFESQL for poorely written ODBC library / drivers. # library support. Add -DSAFESQL for poorely written ODBC library / drivers.
# #
# Linux splice() support is not built by default, it is slower than the # Linux splice() support is built, but used only by services given the -s
# read/write path for most traffic. Add -DWITHSPLICE to CFLAGS to build it, # option: it is faster for large transfers and slower for small requests.
# it must additionally be enabled per service with the -s option. # Remove -DWITHSPLICE from CFLAGS to build without it.
BUILDDIR = ../bin/ BUILDDIR = ../bin/
PREFIX ?= 3proxy_ PREFIX ?= 3proxy_
@ -14,7 +14,7 @@ CRYPT_PREFIX ?= $(PREFIX)
CC ?= gcc CC ?= gcc
CFLAGS ?= -O3 -flto CFLAGS ?= -O3 -flto
CFLAGS += -fno-strict-aliasing -c -pthread -D_GNU_SOURCE -DGETHOSTBYNAME_R -D_THREAD_SAFE -D_REENTRANT -DFD_SETSIZE=4096 -DWITH_POLL -DWITH_NETFILTER -D WITH_UN CFLAGS += -fno-strict-aliasing -c -pthread -DWITHSPLICE -D_GNU_SOURCE -DGETHOSTBYNAME_R -D_THREAD_SAFE -D_REENTRANT -DFD_SETSIZE=4096 -DWITH_POLL -DWITH_NETFILTER -D WITH_UN
COUT = -o COUT = -o
LN ?= ${CC} LN ?= ${CC}
DCFLAGS ?= -fPIC DCFLAGS ?= -fPIC

2
bench/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin/
results/

97
bench/README.md Normal file
View File

@ -0,0 +1,97 @@
# Benchmarks
Compares the speed and the cost of 3proxy versions, and of other proxies doing
the same job, on Linux.
## How the measurement is isolated
The client, the proxy and the origin each run in a network namespace of
their own, linked by veth pairs:
```
[bcli] 10.0.1.2 --veth-- 10.0.1.1 [bprx] 10.0.2.1 --veth-- 10.0.2.2 [bsrv]
wrk, iperf3 the proxy nginx (files), iperf3 -s
```
so they share no loopback, and a packet crosses the same path whichever proxy
is measured. Each role also runs in a cgroup of its own pinned to separate
CPUs (`--cpus`, default `cli=0-3,prx=4-5,srv=6-7`), so one does not take CPU
from another, and RPS steers what each veth receives to the receiver's CPUs.
The proxy's cgroup is created anew for every run, which is what makes its
CPU time and peak memory readable on their own.
Every proxy is given the same CPUs: nginx gets as many workers and haproxy as
many threads as the proxy cgroup has CPUs; 3proxy runs a thread per
connection inside the same cpuset. Logging is off everywhere.
`none` is no proxy at all: the client reaches the origin through the proxy
namespace with the kernel forwarding, the upper bound for every scenario.
## What is reported
For every run, in a CSV under `bench/results/`:
| column | meaning |
|---|---|
| `gbit`, `rate` | throughput, requests per second for HTTP |
| `p50_ms`, `p99_ms` | latency from wrk (closed loop: compare at equal load only) |
| `prx_cpu_s` | CPU seconds the proxy used, all its threads and processes |
| `cpu_s_per_gb`, `cpu_us_per_req` | the proxy's cost per unit of work: the fairest figure, least affected by which side was the limit |
| `peak_mb` | the proxy's peak memory |
| `cli_util`, `prx_util`, `srv_util` | how busy each role's CPUs were: a client or origin near 100% means the result says nothing about the proxy |
| `errors`, `retrans` | socket errors and non-2xx answers; TCP retransmissions in the stream scenarios |
Repetitions rotate the order of the proxies, so drift over time (heat, the
host) falls on all of them alike; the summary gives medians.
## Scenarios
| name | load |
|---|---|
| `stream1`, `stream1-down` | one iperf3 stream through a TCP relay (3proxy `tcppm`, nginx `stream`, haproxy `mode tcp`), upload and download |
| `stream16` | sixteen streams |
| `http-ka-1k`, `http-ka-1m` | HTTP over kept-alive connections through an HTTP proxy (absolute URIs), 1 KB and 1 MB answers |
| `http-close-1k` | a new connection for every request |
nginx is not a forward proxy: it answers absolute URIs by passing them to its
upstream, which is the origin here, and it has no CONNECT or SOCKS, so only
these scenarios compare like with like.
## Running
Root, `iproute2`, `iperf3`, `wrk`, `nginx` (with the stream module:
`libnginx-mod-stream` on Debian and Ubuntu) and, to compare with it,
`haproxy`.
```sh
bench/build.sh 0.9.9.1 1.0.1 # tags into bench/bin/
NAME=devel bench/build.sh . # the working tree
CC="gcc -fcommon" bench/build.sh 0.8.13 # before 0.9, newer gcc
sudo bench/bench.py \
--proxy none \
--proxy 3proxy:bench/bin/3proxy-1.0.1 \
--proxy 3proxy:bench/bin/3proxy-devel \
--proxy "3proxy:bench/bin/3proxy-0.9.4:-s0" \
--proxy nginx --proxy haproxy
```
After the binary, a 3proxy entry can carry options for both its services:
`-s0` keeps 0.9.0 to 0.9.8, which use splice() by default, from using it, and
`-ocTCP_NODELAY -osTCP_NODELAY` turns Nagle's algorithm off on the client and
server sockets.
`--scenario` picks scenarios, `--reps` and `--duration` set the repetitions
and their length, `--netem "delay 1ms rate 10gbit"` puts a slower, longer link
between the client and the proxy, `--mtu` changes the links' MTU.
## Caveats
* A virtual machine cannot pin its virtual CPUs to physical ones, so its
absolute numbers move between sessions: compare within one session, and
keep absolute figures for real hardware.
* wrk measures latency in a closed loop, so a slower proxy is also offered
less load; compare latencies only between runs of similar throughput.
* Versions are built with the Makefile they shipped with, so compiler flags
differ between them (0.8 builds with `-O2`, 0.9.9 and later with
`-O3 -flto`) as they did for their users.

575
bench/bench.py Normal file
View File

@ -0,0 +1,575 @@
#!/usr/bin/env python3
"""Compares the speed and the cost of proxies on Linux: 3proxy versions, and
other servers doing the same job.
The client, the proxy and the origin each run in a network namespace of their
own, linked by veth pairs, and in a cgroup of their own pinned to separate
CPUs, so they neither share a loopback nor take CPU from each other, and the
proxy's CPU time and memory can be read on their own. See README.md.
Needs root, iproute2, iperf3, wrk and nginx (the origin); haproxy only when
it is one of the proxies compared.
sudo bench/bench.py --proxy 3proxy:bench/bin/3proxy-1.0.1 \\
--proxy 3proxy:bench/bin/3proxy-devel --proxy nginx --proxy none
"""
import argparse
import csv
import datetime
import json
import os
import re
import resource
import shutil
import signal
import statistics
import subprocess
import sys
import tempfile
import time
CG = "/sys/fs/cgroup/3proxy-bench"
# client 10.0.1.2 -- 10.0.1.1 proxy 10.0.2.1 -- 10.0.2.2 origin
CLI_ADDR, PRX_IN, PRX_OUT, SRV_ADDR = "10.0.1.2", "10.0.1.1", "10.0.2.1", "10.0.2.2"
HTTP_PORT, TCP_PORT, ORIGIN_HTTP, ORIGIN_TCP = 3128, 5201, 8080, 5201
FILES = {"1k": 1024, "100k": 100 * 1024, "1m": 1024 * 1024}
# name: (kind, parameters)
SCENARIOS = {
"stream1": ("stream", {"streams": 1}),
"stream16": ("stream", {"streams": 16}),
"stream1-down": ("stream", {"streams": 1, "reverse": True}),
"http-ka-1k": ("http", {"file": "1k", "conns": 64}),
"http-ka-1m": ("http", {"file": "1m", "conns": 16}),
"http-close-1k": ("http", {"file": "1k", "conns": 64, "close": True}),
}
def sh(cmd, check=True, **kw):
return subprocess.run(cmd, shell=True, check=check, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kw).stdout
def cpulist(spec):
cpus = []
for part in spec.split(","):
a, _, b = part.partition("-")
cpus.extend(range(int(a), int(b or a) + 1))
return cpus
def cpumask(spec):
return "%x" % sum(1 << c for c in cpulist(spec))
# --- namespaces --------------------------------------------------------------
def net_up(args):
net_down()
for ns in ("bcli", "bprx", "bsrv"):
sh(f"ip netns add {ns}")
sh(f"ip -n {ns} link set lo up")
sh("ip link add bc0 netns bcli type veth peer name bp0 netns bprx")
sh("ip link add bp1 netns bprx type veth peer name bs0 netns bsrv")
for ns, dev, addr in (("bcli", "bc0", CLI_ADDR), ("bprx", "bp0", PRX_IN),
("bprx", "bp1", PRX_OUT), ("bsrv", "bs0", SRV_ADDR)):
sh(f"ip -n {ns} addr add {addr}/24 dev {dev}")
sh(f"ip -n {ns} link set {dev} mtu {args.mtu} up")
# the direct baseline goes through the proxy namespace, routed by the
# kernel
sh(f"ip -n bcli route add 10.0.2.0/24 via {PRX_IN}")
sh(f"ip -n bsrv route add 10.0.1.0/24 via {PRX_OUT}")
sh("ip netns exec bprx sysctl -qw net.ipv4.ip_forward=1")
for ns in ("bcli", "bprx", "bsrv"):
sh(f"ip netns exec {ns} sysctl -qw net.core.somaxconn=65535 "
"net.ipv4.tcp_max_syn_backlog=65535 net.ipv4.tcp_tw_reuse=1 "
"net.ipv4.ip_local_port_range='1024 65535'")
# A veth hands what it receives to the CPU that sent it; steer it to the
# receiver's own CPUs instead.
for ns, dev, role in (("bcli", "bc0", "cli"), ("bprx", "bp0", "prx"),
("bprx", "bp1", "prx"), ("bsrv", "bs0", "srv")):
sh(f"ip netns exec {ns} sh -c 'echo {cpumask(args.cpus[role])} "
f"> /sys/class/net/{dev}/queues/rx-0/rps_cpus'", check=False)
if args.netem:
for ns, dev in (("bcli", "bc0"), ("bprx", "bp0")):
sh(f"ip netns exec {ns} tc qdisc add dev {dev} root netem {args.netem}")
def net_down():
for ns in ("bcli", "bprx", "bsrv"):
sh(f"ip netns del {ns}", check=False)
# --- cgroups -----------------------------------------------------------------
def cg_write(path, value):
with open(path, "w") as f:
f.write(value)
def cg_read(path):
with open(path) as f:
return f.read()
def cg_up(args):
os.makedirs(CG, exist_ok=True)
cg_write("/sys/fs/cgroup/cgroup.subtree_control", "+cpuset +cpu +memory")
cg_write(f"{CG}/cgroup.subtree_control", "+cpuset +cpu +memory")
for role in ("cli", "srv"):
cg_make(role, args.cpus[role])
def cg_make(role, cpus):
path = f"{CG}/{role}"
os.makedirs(path, exist_ok=True)
cg_write(f"{path}/cpuset.cpus", cpus)
return path
def cg_remove(role):
path = f"{CG}/{role}"
if not os.path.isdir(path):
return
for _ in range(50):
try:
os.rmdir(path)
return
except OSError:
time.sleep(0.1)
def cg_down():
for role in ("cli", "prx", "srv"):
cg_remove(role)
try:
os.rmdir(CG)
except OSError:
pass
def cg_usage(role):
"""CPU seconds used so far by everything in the cgroup."""
for line in cg_read(f"{CG}/{role}/cpu.stat").splitlines():
name, value = line.split()
if name == "usage_usec":
return int(value) / 1e6
return 0.0
def cg_peak(role):
try:
return int(cg_read(f"{CG}/{role}/memory.peak")) / 2**20
except OSError:
return float("nan")
# --- processes ---------------------------------------------------------------
def spawn(ns, role, argv, log):
"""Start a process in a namespace and a cgroup; threads it starts stay
there with it."""
procs = f"{CG}/{role}/cgroup.procs"
def enter():
cg_write(procs, str(os.getpid()))
resource.setrlimit(resource.RLIMIT_NOFILE, (1048576, 1048576))
os.setsid()
return subprocess.Popen(["ip", "netns", "exec", ns] + argv, stdout=log,
stderr=subprocess.STDOUT, preexec_fn=enter)
def run_in(ns, role, argv, timeout):
p = spawn(ns, role, argv, subprocess.PIPE)
out, _ = p.communicate(timeout=timeout)
return out.decode("utf-8", "replace")
def stop(p):
if p and p.poll() is None:
try:
os.killpg(p.pid, signal.SIGTERM)
p.wait(timeout=5)
except (ProcessLookupError, subprocess.TimeoutExpired):
try:
os.killpg(p.pid, signal.SIGKILL)
except ProcessLookupError:
pass
p.wait()
def wait_port(ns, addr, port, timeout=10):
deadline = time.time() + timeout
while time.time() < deadline:
# /dev/tcp is bash's
if subprocess.run(["ip", "netns", "exec", ns, "bash", "-c",
f"exec 3<>/dev/tcp/{addr}/{port}"],
stderr=subprocess.DEVNULL).returncode == 0:
return True
time.sleep(0.1)
return False
# --- the origin --------------------------------------------------------------
def origin_up(args, work):
www = os.path.join(work, "www")
os.makedirs(www, exist_ok=True)
for name, size in FILES.items():
with open(os.path.join(www, name), "wb") as f:
f.write(b"x" * size)
n = len(cpulist(args.cpus["srv"]))
conf = os.path.join(work, "origin.conf")
with open(conf, "w") as f:
f.write(f"""daemon off;
worker_processes {n};
pid {work}/origin.pid;
error_log {work}/origin.err;
worker_rlimit_nofile 1048576;
events {{ worker_connections 65536; }}
http {{
access_log off;
sendfile on;
tcp_nopush on;
keepalive_requests 1000000;
keepalive_timeout 120s;
server {{
listen {SRV_ADDR}:{ORIGIN_HTTP} backlog=65535 reuseport;
root {www};
default_type application/octet-stream;
}}
}}
""")
log = open(os.path.join(work, "origin.out"), "w")
procs = [spawn("bsrv", "srv", ["nginx", "-c", conf, "-p", work], log),
spawn("bsrv", "srv", ["iperf3", "-s", "-B", SRV_ADDR, "-p", str(ORIGIN_TCP)], log)]
if not (wait_port("bsrv", SRV_ADDR, ORIGIN_HTTP) and wait_port("bsrv", SRV_ADDR, ORIGIN_TCP)):
sys.exit("the origin did not start, see " + work)
return procs
# --- proxies -----------------------------------------------------------------
def proxy_conf(kind, work, ncpu, label, opts=""):
"""Configuration for one proxy: an HTTP proxy on HTTP_PORT and a TCP relay
on TCP_PORT, both towards the origin. Returns the argv. opts are added to
both 3proxy services, e.g. -s0 to keep a version from using splice()."""
if kind == "3proxy":
conf = os.path.join(work, f"{label}.cfg")
with open(conf, "w") as f:
f.write(f"""maxconn 100000
auth none
proxy {opts} -i{PRX_IN} -e{PRX_OUT} -p{HTTP_PORT}
tcppm {opts} -i{PRX_IN} -e{PRX_OUT} {TCP_PORT} {SRV_ADDR} {ORIGIN_TCP}
""")
return [conf]
if kind == "nginx":
conf = os.path.join(work, f"{label}.conf")
with open(conf, "w") as f:
# distributions build the stream module as a dynamic one
stream = "/usr/lib/nginx/modules/ngx_stream_module.so"
load = f"load_module {stream};\n" if os.path.exists(stream) else ""
f.write(f"""{load}daemon off;
worker_processes {ncpu};
pid {work}/{label}.pid;
error_log {work}/{label}.err;
worker_rlimit_nofile 1048576;
events {{ worker_connections 65536; }}
http {{
access_log off;
upstream origin {{ server {SRV_ADDR}:{ORIGIN_HTTP}; keepalive 1024; }}
server {{
listen {PRX_IN}:{HTTP_PORT} backlog=65535 reuseport;
keepalive_requests 1000000;
location / {{
proxy_pass http://origin;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_bind {PRX_OUT};
}}
}}
}}
stream {{
server {{
listen {PRX_IN}:{TCP_PORT} backlog=65535 reuseport;
proxy_pass {SRV_ADDR}:{ORIGIN_TCP};
proxy_bind {PRX_OUT};
}}
}}
""")
return ["-c", conf, "-p", work]
if kind == "haproxy":
conf = os.path.join(work, f"{label}.cfg")
with open(conf, "w") as f:
f.write(f"""global
nbthread {ncpu}
maxconn 200000
defaults
timeout connect 5s
timeout client 60s
timeout server 60s
frontend http
mode http
bind {PRX_IN}:{HTTP_PORT}
option http-keep-alive
default_backend origin
backend origin
mode http
http-reuse always
source {PRX_OUT}
server origin {SRV_ADDR}:{ORIGIN_HTTP}
listen tcp
mode tcp
bind {PRX_IN}:{TCP_PORT}
source {PRX_OUT}
server origin {SRV_ADDR}:{ORIGIN_TCP}
""")
return ["-f", conf]
raise ValueError(kind)
class Proxy:
def __init__(self, spec):
kind, _, path = spec.partition(":")
self.kind = kind
self.opts = ""
if kind == "3proxy":
path, _, self.opts = path.partition(":")
if not path:
sys.exit("--proxy 3proxy:<binary>[:<service options>]")
self.binary = os.path.abspath(path)
self.label = os.path.basename(path) + (self.opts.replace(" ", "") if self.opts else "")
elif kind in ("nginx", "haproxy"):
self.binary = path or shutil.which(kind)
self.label = kind
elif kind == "none":
self.binary = None
self.label = "direct"
else:
sys.exit("unknown proxy " + spec)
def start(self, args, work):
cg_remove("prx")
cg_make("prx", args.cpus["prx"])
if self.kind == "none":
return None
argv = [self.binary] + proxy_conf(self.kind, work, len(cpulist(args.cpus["prx"])),
self.label, self.opts)
log = open(os.path.join(work, self.label + ".out"), "a")
p = spawn("bprx", "prx", argv, log)
if not (wait_port("bcli", PRX_IN, HTTP_PORT) and wait_port("bcli", PRX_IN, TCP_PORT)):
stop(p)
sys.exit(f"{self.label} did not start, see {work}")
return p
# --- load --------------------------------------------------------------------
WRK_SCRIPT = """
local path, close = "%s", %s
local headers = {["Host"] = "%s:%d"}
if close then headers["Connection"] = "close" end
local req = wrk.format("GET", path, headers)
request = function() return req end
"""
UNITS = {"": 1, "k": 1e3, "K": 1e3, "M": 1e6, "G": 1e9, "KB": 2**10, "MB": 2**20,
"GB": 2**30, "B": 1}
def to_ms(text):
m = re.match(r"([\d.]+)(us|ms|s|m)", text)
v, u = float(m.group(1)), m.group(2)
return v * {"us": 1e-3, "ms": 1, "s": 1e3, "m": 6e4}[u]
def run_http(proxy, params, args, work):
direct = proxy.kind == "none"
host = SRV_ADDR if direct else PRX_IN
port = ORIGIN_HTTP if direct else HTTP_PORT
path = "/" + params["file"]
if not direct:
path = f"http://{SRV_ADDR}:{ORIGIN_HTTP}{path}"
script = os.path.join(work, "wrk.lua")
with open(script, "w") as f:
f.write(WRK_SCRIPT % (path, "true" if params.get("close") else "false",
SRV_ADDR, ORIGIN_HTTP))
threads = len(cpulist(args.cpus["cli"]))
conns = max(params["conns"], threads)
out = run_in("bcli", "cli", ["wrk", "-t", str(threads), "-c", str(conns),
"-d", f"{args.duration}s", "--latency", "-s", script,
f"http://{host}:{port}/"], args.duration + 30)
res = {"raw": out}
m = re.search(r"(\d+) requests in", out)
res["requests"] = int(m.group(1)) if m else 0
m = re.search(r"Requests/sec:\s+([\d.]+)", out)
res["rate"] = float(m.group(1)) if m else 0.0
m = re.search(r"Transfer/sec:\s+([\d.]+)(\w+)", out)
res["gbit"] = float(m.group(1)) * UNITS[m.group(2)] * 8 / 1e9 if m else 0.0
for pct in ("50", "99"):
m = re.search(rf"^\s+{pct}%\s+(\S+)", out, re.M)
res[f"p{pct}_ms"] = to_ms(m.group(1)) if m else float("nan")
errors = 0
m = re.search(r"Socket errors: connect (\d+), read (\d+), write (\d+), timeout (\d+)", out)
if m:
errors += sum(int(x) for x in m.groups())
m = re.search(r"Non-2xx or 3xx responses: (\d+)", out)
if m:
errors += int(m.group(1))
res["errors"] = errors
res["retrans"] = 0
res["bytes"] = res["gbit"] * 1e9 / 8 * args.duration
return res
def run_stream(proxy, params, args, work):
direct = proxy.kind == "none"
host = SRV_ADDR if direct else PRX_IN
port = ORIGIN_TCP if direct else TCP_PORT
argv = ["iperf3", "-c", host, "-p", str(port), "-t", str(args.duration),
"-P", str(params["streams"]), "-J"]
if params.get("reverse"):
argv.append("-R")
out = run_in("bcli", "cli", argv, args.duration + 30)
res = {"raw": out, "rate": float("nan"), "p50_ms": float("nan"),
"p99_ms": float("nan"), "requests": 0, "errors": 0, "retrans": 0}
try:
j = json.loads(out)
s = j["end"]["sum_received"]
res["gbit"] = s["bits_per_second"] / 1e9
res["bytes"] = s["bytes"]
res["retrans"] = j["end"].get("sum_sent", {}).get("retransmits", 0)
if "error" in j:
res["errors"] = 1
except (ValueError, KeyError):
res["gbit"], res["bytes"], res["errors"] = 0.0, 0, 1
return res
def measure(proxy, scenario, args, work):
kind, params = SCENARIOS[scenario]
p = proxy.start(args, work)
try:
time.sleep(0.5)
before = {r: cg_usage(r) for r in ("cli", "prx", "srv")}
t0 = time.time()
res = (run_http if kind == "http" else run_stream)(proxy, params, args, work)
wall = time.time() - t0
after = {r: cg_usage(r) for r in ("cli", "prx", "srv")}
res["peak_mb"] = cg_peak("prx")
finally:
stop(p)
for r in ("cli", "prx", "srv"):
used = after[r] - before[r]
res[f"{r}_cpu_s"] = used
# share of the CPUs given to that role, to see which side was the limit
res[f"{r}_util"] = used / (wall * len(cpulist(args.cpus[r])))
gb = res["bytes"] / 1e9
res["cpu_s_per_gb"] = res["prx_cpu_s"] / gb if gb else float("nan")
res["cpu_us_per_req"] = (res["prx_cpu_s"] * 1e6 / res["requests"]
if res["requests"] else float("nan"))
return res
# --- main --------------------------------------------------------------------
FIELDS = ["proxy", "scenario", "rep", "gbit", "rate", "p50_ms", "p99_ms", "errors", "retrans",
"prx_cpu_s", "cpu_s_per_gb", "cpu_us_per_req", "peak_mb",
"cli_util", "prx_util", "srv_util"]
def summary(rows):
keys = []
for r in rows:
if (r["proxy"], r["scenario"]) not in keys:
keys.append((r["proxy"], r["scenario"]))
cols = ["gbit", "rate", "p99_ms", "cpu_s_per_gb", "cpu_us_per_req", "peak_mb",
"prx_util", "cli_util", "srv_util", "errors"]
print("\nmedians (" + ", ".join(cols) + ")")
print("%-24s %-14s " % ("proxy", "scenario") + " ".join("%10s" % c[:10] for c in cols))
for proxy, scenario in sorted(keys, key=lambda k: (k[1], k[0])):
sel = [r for r in rows if r["proxy"] == proxy and r["scenario"] == scenario]
vals = []
for c in cols:
xs = [r[c] for r in sel if r[c] == r[c]]
vals.append("%10.3g" % statistics.median(xs) if xs else "%10s" % "-")
print("%-24s %-14s " % (proxy, scenario) + " ".join(vals))
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--proxy", action="append", required=True,
help="3proxy:<binary>[:<service options>], nginx[:<binary>], haproxy[:<binary>] "
"or none (the kernel routing, an upper bound); repeat")
ap.add_argument("--scenario", action="append", choices=sorted(SCENARIOS),
help="default: all")
ap.add_argument("--reps", type=int, default=3)
ap.add_argument("--duration", type=int, default=10, help="seconds per run")
ap.add_argument("--cpus", default="cli=0-3,prx=4-5,srv=6-7",
help="CPUs per role (default %(default)s)")
ap.add_argument("--mtu", type=int, default=1500)
ap.add_argument("--netem", help='netem on the client link, e.g. "delay 1ms rate 10gbit"')
ap.add_argument("--out", default="bench/results", help="directory for the CSV and logs")
args = ap.parse_args()
if os.geteuid():
sys.exit("needs root")
args.cpus = dict(x.split("=") for x in args.cpus.split(","))
proxies = [Proxy(s) for s in args.proxy]
scenarios = args.scenario or list(SCENARIOS)
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
os.makedirs(args.out, exist_ok=True)
work = tempfile.mkdtemp(prefix="3proxy-bench-")
os.chmod(work, 0o755)
csvpath = os.path.join(args.out, f"{stamp}.csv")
rawpath = os.path.join(args.out, f"{stamp}.raw.txt")
with open(os.path.join(args.out, f"{stamp}.env.txt"), "w") as f:
f.write(" ".join(sys.argv) + "\n")
f.write(sh("uname -a; lscpu | head -20; nproc; "
"cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null; "
"iperf3 --version | head -1; wrk -v 2>&1 | head -1; nginx -v 2>&1; "
"haproxy -v 2>/dev/null | head -1", check=False))
rows = []
origin = []
try:
net_up(args)
cg_up(args)
origin = origin_up(args, work)
with open(csvpath, "w", newline="") as f, open(rawpath, "w") as raw:
w = csv.DictWriter(f, FIELDS, extrasaction="ignore")
w.writeheader()
# Every repetition runs each proxy once, rotating the order, so a
# drift over time is shared by all of them.
for rep in range(args.reps):
for scenario in scenarios:
order = proxies[rep % len(proxies):] + proxies[:rep % len(proxies)]
for proxy in order:
res = measure(proxy, scenario, args, work)
res.update(proxy=proxy.label, scenario=scenario, rep=rep)
rows.append(res)
w.writerow(res)
f.flush()
raw.write(f"=== {proxy.label} {scenario} {rep}\n{res['raw']}\n")
print("%-24s %-14s rep %d: %7.2f Gbit/s %10.0f req/s proxy cpu %5.2fs "
"util cli %3.0f%% prx %3.0f%% srv %3.0f%% errors %d" % (
proxy.label, scenario, rep, res["gbit"],
res["rate"] if res["rate"] == res["rate"] else 0,
res["prx_cpu_s"], res["cli_util"] * 100,
res["prx_util"] * 100, res["srv_util"] * 100,
res["errors"]), flush=True)
finally:
for p in origin:
stop(p)
cg_down()
net_down()
summary(rows)
print(f"\n{csvpath}\nlogs and configurations: {work}")
if __name__ == "__main__":
main()

52
bench/build.sh Normal file
View File

@ -0,0 +1,52 @@
#!/bin/sh
# Builds 3proxy versions for comparison, each from a clean tree and with the
# Makefile it shipped with, into bench/bin/3proxy-<name>.
#
# bench/build.sh 0.9.9.1 1.0.1 devel git refs (tags, branches, commits)
# bench/build.sh . the working tree, uncommitted
# changes included, as "worktree"
# NAME=devel-splice EXTRA_CFLAGS=-DWITHSPLICE bench/build.sh devel
# CC="gcc -fcommon" bench/build.sh 0.8.13 old Makefiles set CFLAGS
# outright, so a flag a newer
# compiler needs goes with CC
#
# Run from anywhere inside the repository.
set -e
top=$(git rev-parse --show-toplevel)
out="$top/bench/bin"
work="${TMPDIR:-/tmp}/3proxy-bench-src"
mkdir -p "$out" "$work"
for ref in "$@"; do
if [ "$ref" = "." ]; then
name=${NAME:-worktree}
src="$work/$name"
rm -rf "$src"
mkdir -p "$src"
# tracked and untracked files as they are now, nothing ignored
(cd "$top" && git ls-files -co --exclude-standard -z |
xargs -0 sh -c 'for f; do [ -f "$f" ] && printf "%s\0" "$f"; done' sh |
tar cf - --null -T - ) | (cd "$src" && tar xf -)
else
name=${NAME:-$ref}
src="$work/$name"
rm -rf "$src"
mkdir -p "$src"
(cd "$top" && git archive "$ref") | (cd "$src" && tar xf -)
fi
echo "== $name"
(
cd "$src"
mkdir -p bin
make -f Makefile.Linux CC="${CC:-gcc}" EXTRA_CFLAGS="$EXTRA_CFLAGS" >build.log 2>&1 ||
{ tail -20 build.log; exit 1; }
)
# versions before 0.9 leave the binary in src/
for b in "$src/bin/3proxy" "$src/src/3proxy"; do
[ -f "$b" ] && break
done
cp "$b" "$out/3proxy-$name"
echo " $out/3proxy-$name"
done

View File

@ -34,7 +34,8 @@ Each connected client requires:
<ul> <ul>
<li>1 thread (process) <li>1 thread (process)
<li>2 sockets (file descriptors). For FTP, 4 sockets are required. <li>2 sockets (file descriptors). For FTP, 4 sockets are required.
<br>Up to 128K of kernel buffer memory. This is the theoretical maximum; actual numbers depend on connection quality and traffic amount. <br>With splice() (Linux, -s), 2 more pipes (file descriptors) = 4 file descriptors, 6 for FTP.
<br>Up to 128K (up to 256K with splice()) of kernel buffer memory. This is the theoretical maximum; actual numbers depend on connection quality and traffic amount.
<br>1 additional socket (file descriptor) during name resolution for non-cached names <br>1 additional socket (file descriptor) during name resolution for non-cached names
<br>1 additional socket during authentication or logging for RADIUS authentication or logging. <br>1 additional socket during authentication or logging for RADIUS authentication or logging.
<li>1 ephemeral port (3 ephemeral ports for FTP connections). <li>1 ephemeral port (3 ephemeral ports for FTP connections).
@ -279,6 +280,8 @@ tuning anything else.
If 3proxy is used in a VPS environment, there can be additional limitations. If 3proxy is used in a VPS environment, there can be additional limitations.
For example, kernel resources, system CPU usage, and IOCTLs can be limited differently, and this can become a bottleneck. For example, kernel resources, system CPU usage, and IOCTLs can be limited differently, and this can become a bottleneck.
splice() (see below) moves work into the kernel, so where the hosting limits kernel resources
(seen as nearly 100% CPU usage without any real activity of the application) do not enable it.
<h4>Extending the Ephemeral Port Range</h4> <h4>Extending the Ephemeral Port Range</h4>
@ -641,17 +644,37 @@ monitor "/some/path/3proxy/3proxy.ver"
e.g., with "touch /some/path/3proxy/3proxy.ver". e.g., with "touch /some/path/3proxy/3proxy.ver".
</ol> </ol>
<h4>Use TCP_NODELAY to Speed Up Connections with Small Amounts of Data</h4> <h4>TCP_NODELAY for Connections with Small Amounts of Data</h4>
If most requests require an exchange with a small amount of data in both directions TCP_NODELAY is set by default on client (oc) and server (os)
without the need for bandwidth, e.g., messengers or small web requests, connections: without it, Nagle's algorithm holds back a short write until the
you can eliminate Nagle's algorithm delay with the TCP_NODELAY flag. Usage example: previous one is acknowledged, and a peer delays its acknowledgement, so a reply
written in more than one part (e.g. HTTP headers, then a short body) waits tens
of milliseconds. On slow connections with high delays, where the bandwidth is the
bottleneck and fewer packets matter more than latency, it can be turned off:
<pre> <pre>
proxy -osTCP_NODELAY -ocTCP_NODELAY proxy -osTCP_NODELAY=0 -ocTCP_NODELAY=0
</pre> </pre>
sets TCP_NODELAY for client (oc) and server (os) connections.
<p>Do not use TCP_NODELAY on slow connections with high delays when <h4>Use Splice to Speed Up Large Data Transfers</h4>
connection bandwidth is a bottleneck.
On Linux, splice() passes data between the client and the server connections
through a kernel pipe, without copying it into the 3proxy process. Linux builds
include it, but a service uses it only when given the -s option:
<pre>
proxy -s
socks -s
tcppm -s 8080 192.168.1.1 80
</pre>
-s0 turns it off explicitly. In builds without it (CMake option 3PROXY_USE_SPLICE=OFF,
or -DWITHSPLICE removed from the Makefile) -s is accepted and ignored.
<p>splice() pays off when most of the traffic is large transfers: in the
<a href="https://github.com/3proxy/3proxy/tree/master/bench">benchmarks</a> it moved
1.4 to 1.7 times more data than the read/write path at about half the CPU per gigabyte.
It does not help small requests and short connections, which go slower with it, and
every spliced connection needs two more pipes (file descriptors) and more kernel
buffer memory. Data filters are not applied to spliced data.
<p>TCP_NODELAY and splice() do not conflict and can be combined on high-speed connections.
<h4>Add Grace Delay to Reduce System Calls</h4> <h4>Add Grace Delay to Reduce System Calls</h4>
@ -661,7 +684,7 @@ the minimal number of packets in the same direction to apply the algorithm,
and the last value is the delay added after polling and prior to reading data. and the last value is the delay added after polling and prior to reading data.
The example above adds a 10-millisecond delay before reading data if the average The example above adds a 10-millisecond delay before reading data if the average
polling size is below 8000 bytes and 3 read operations have been made in the same polling size is below 8000 bytes and 3 read operations have been made in the same
direction. <pre>logdump 1 1</pre> is useful direction. It's especially useful with splice. <pre>logdump 1 1</pre> is useful
to see how grace delays work; choose a delay value to avoid filling the read to see how grace delays work; choose a delay value to avoid filling the read
buffer (typically 64K) but keep the request sizes close to the chosen average buffer (typically 64K) but keep the request sizes close to the chosen average
on large file uploads/downloads. on large file uploads/downloads.

View File

@ -184,8 +184,10 @@ connect to given remote HOST:port instead of listening local connection on -p or
.br .br
.B -oc\fIOPTIONS\fB, -os\fIOPTIONS\fB, -ol\fIOPTIONS\fB, -or\fIOPTIONS\fB, -oR\fIOPTIONS\fR .B -oc\fIOPTIONS\fB, -os\fIOPTIONS\fB, -ol\fIOPTIONS\fB, -or\fIOPTIONS\fB, -oR\fIOPTIONS\fR
options for proxy-to-client (\fB-oc\fR), proxy-to-server (\fB-os\fR), proxy listening (\fB-ol\fR), connect back client (\fB-or\fR), connect back listening (\fB-oR\fR) sockets. options for proxy-to-client (\fB-oc\fR), proxy-to-server (\fB-os\fR), proxy listening (\fB-ol\fR), connect back client (\fB-or\fR), connect back listening (\fB-oR\fR) sockets.
\fIOPTIONS\fR is a comma separated list of \fINAME\fR or \fINAME\fR=\fIVALUE\fR; a name alone sets the option to 1 (TCP_MAXSEG to the value of \fBmaxseg\fR), and \fINAME\fR=0 turns it off. Options add to the defaults and to the options given earlier for the same socket. An unknown name or a value which is not a number is an error.
TCP_NODELAY is set by default on proxy-to-client and proxy-to-server sockets, so a reply written in several parts is not delayed by Nagle's algorithm; use \fB-ocTCP_NODELAY=0\fR or \fB-osTCP_NODELAY=0\fR to turn it off, e.g. to send fewer packets on slow links.
Options like TCP_CORK, TCP_NODELAY, TCP_DEFER_ACCEPT, TCP_QUICKACK, TCP_TIMESTAMPS, TCP_FASTOPEN, SO_REUSEADDR, SO_REUSEPORT, SO_EXCLUSIVEADDRUSE, SO_PORT_SCALABILITY, SO_REUSE_UNICASTPORT, SO_KEEPALIVE, SO_DONTROUTE may be supported depending on OS. Options like TCP_CORK, TCP_NODELAY, TCP_DEFER_ACCEPT, TCP_QUICKACK, TCP_TIMESTAMPS, TCP_FASTOPEN, SO_REUSEADDR, SO_REUSEPORT, SO_EXCLUSIVEADDRUSE, SO_PORT_SCALABILITY, SO_REUSE_UNICASTPORT, SO_KEEPALIVE, SO_DONTROUTE may be supported depending on OS.
SO_REUSEADDR and SO_REUSEPORT are set on the listening socket by default on Unix. On Windows SO_REUSEADDR is not set: it is not required to rebind a listening port and it only lets another local process bind the same address and port. Use SO_EXCLUSIVEADDRUSE (Windows) on the listening socket (\fB-ol\fR) to prevent that. SO_REUSEADDR and SO_REUSEPORT are set by default on the listening sockets (\fB-ol\fR, \fB-oR\fR) on Unix; \fB-olSO_REUSEPORT=0\fR turns one off. On Windows SO_REUSEADDR is not set: it is not required to rebind a listening port and it only lets another local process bind the same address and port. Use SO_EXCLUSIVEADDRUSE (Windows) on the listening socket (\fB-ol\fR) to prevent that.
.br .br
.B -H .B -H
(for all services) Expect HAProxy PROXY protocol v1 header on incoming connection. (for all services) Expect HAProxy PROXY protocol v1 header on incoming connection.
@ -200,11 +202,13 @@ delay GRACE_DELAY milliseconds before polling if average polling size is below G
(for admin) secure, allow only secure operations, currently only traffic counters (for admin) secure, allow only secure operations, currently only traffic counters
view without ability to reset. view without ability to reset.
.br .br
(for TCP services, Linux) enable splice(). splice() is not built by default and (for TCP services, Linux) use splice() to pass data between the client and the
is disabled even when built, because current Linux does not implement server without copying it through 3proxy. It is off unless -s is given (-s0 turns
SPLICE_F_MOVE, so no real zero-copy takes place and the read/write path is faster it off explicitly). splice() suits large transfers: they go faster and use less
for most traffic. Rebuild with -DWITHSPLICE to make -s available, -s0 disables it CPU. Small requests and short connections go slower with it, every connection
explicitly. needs two more pipes (file descriptors) and more kernel memory, and data filters
are not applied to spliced data. Available in builds with WITHSPLICE, the default
on Linux.
.br .br
.B -C .B -C
(for TCP services) a connection closed by any of the sides terminates the (for TCP services) a connection closed by any of the sides terminates the

View File

@ -74,13 +74,8 @@ void __stdcall CommandHandler( DWORD dwCommand )
SetStatus( SERVICE_STOP_PENDING, 0, 1 ); SetStatus( SERVICE_STOP_PENDING, 0, 1 );
conf.timetoexit = 1; conf.timetoexit = 1;
conf.paused++; conf.paused++;
Sleep(2000); /* ServiceMain() reports the service stopped once cyclestep() has
SetStatus( SERVICE_STOPPED, 0, 0 ); seen this and the services are done */
#ifdef WITH_ODBC
_3proxy_mutex_lock(&log_mutex);
close_sql();
_3proxy_mutex_unlock(&log_mutex);
#endif
break; break;
case SERVICE_CONTROL_PAUSE: case SERVICE_CONTROL_PAUSE:
SetStatus( SERVICE_PAUSE_PENDING, 0, 1 ); SetStatus( SERVICE_PAUSE_PENDING, 0, 1 );
@ -106,6 +101,13 @@ void __stdcall ServiceMain(int argc, unsigned char* argv[] )
SetStatus( SERVICE_START_PENDING, 0, 1 ); SetStatus( SERVICE_START_PENDING, 0, 1 );
SetStatus( SERVICE_RUNNING, 0, 0 ); SetStatus( SERVICE_RUNNING, 0, 0 );
cyclestep(); cyclestep();
#ifdef WITH_ODBC
_3proxy_mutex_lock(&log_mutex);
close_sql();
_3proxy_mutex_unlock(&log_mutex);
#endif
/* the last thing: the process may be ended once this is reported */
SetStatus( SERVICE_STOPPED, 0, 0 );
} }
#endif #endif
@ -240,6 +242,39 @@ void dumpcounters(struct trafcount *tlin, int counterd){
} }
} }
/* Lets the services finish before the process exits, for EXITWAIT ms at
most: each one waits for its connections, and a connection may hang on
its peer. Posts left over from reloads are consumed on the way, the
count decides. */
#define EXITWAIT 3000
static unsigned long nowms(void){
#ifdef _WIN32
return (unsigned long)GetTickCount();
#else
struct timeval tv;
gettimeofday(&tv, NULL);
return (unsigned long)tv.tv_sec * 1000 + tv.tv_usec / 1000;
#endif
}
static void waitservices(void){
unsigned long start = nowms(), passed;
for(;;){
int n;
_3proxy_mutex_lock(&runningservices_mutex);
n = runningservices;
_3proxy_mutex_unlock(&runningservices_mutex);
if(!n) return;
/* a clock set back makes this huge, which ends the wait as well */
passed = nowms() - start;
if(passed >= EXITWAIT) return;
if(_3proxy_sem_timedlock(serviceexited, (unsigned)(EXITWAIT - passed))) return;
}
}
void cyclestep(void){ void cyclestep(void){
struct tm *tm; struct tm *tm;
time_t minutecounter; time_t minutecounter;
@ -336,9 +371,7 @@ void cyclestep(void){
if(conf.timetoexit){ if(conf.timetoexit){
conf.paused++; conf.paused++;
doschedule(); doschedule();
usleep(SLEEPTIME*999); waitservices();
usleep(SLEEPTIME*999);
usleep(SLEEPTIME*999);
return; return;
} }
@ -517,6 +550,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
} }
_3proxy_mutex_init(&config_mutex); _3proxy_mutex_init(&config_mutex);
_3proxy_mutex_init(&runningservices_mutex);
_3proxy_mutex_init(&bandlim_mutex); _3proxy_mutex_init(&bandlim_mutex);
_3proxy_mutex_init(&connlim_mutex); _3proxy_mutex_init(&connlim_mutex);
_3proxy_mutex_init(&tc_mutex); _3proxy_mutex_init(&tc_mutex);
@ -524,7 +558,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
#ifndef NORADIUS #ifndef NORADIUS
_3proxy_mutex_init(&rad_mutex); _3proxy_mutex_init(&rad_mutex);
#endif #endif
if(_3proxy_sem_init(conf.threadinit, 1, 1)){ if(_3proxy_sem_init(conf.threadinit, 1, 1) || _3proxy_sem_init_timed(serviceexited, 0, 0x7fffffff)){
fprintf(stderr, "semaphore init failed\n"); fprintf(stderr, "semaphore init failed\n");
return 1; return 1;
} }

View File

@ -387,7 +387,6 @@ int
return shutdown(s, how); return shutdown(s, how);
} }
int WINAPI def_closesocket(void* state, SOCKET s){ int WINAPI def_closesocket(void* state, SOCKET s){
usleep(SLEEPTIME);
return closesocket(s); return closesocket(s);
} }
#else #else
@ -715,7 +714,9 @@ int doconnect(struct clientparam * param){
if(param->srv->so._getpeername(param->sostate, param->remsock, (struct sockaddr *)&param->sinsr, &size)==-1) {return (14);} if(param->srv->so._getpeername(param->sostate, param->remsock, (struct sockaddr *)&param->sinsr, &size)==-1) {return (14);}
} }
else { else {
#ifndef _WIN32
struct linger lg = {1,conf.timeouts[LINGER_TO]}; struct linger lg = {1,conf.timeouts[LINGER_TO]};
#endif
int tries = RANGETRIES; int tries = RANGETRIES;
if(SAISNULL(&param->sinsr)){ if(SAISNULL(&param->sinsr)){
@ -745,9 +746,12 @@ int doconnect(struct clientparam * param){
param->sinsl = param->srv->extsa; param->sinsl = param->srv->extsa;
} }
*SAPORT(&param->sinsl) = 0; *SAPORT(&param->sinsl) = 0;
setopts(param->remsock, param->srv->srvsockopts); setopts(param->remsock, &param->srv->srvsockopts);
#ifndef _WIN32
/* not on Windows, see the accepting side in proxymain.c */
param->srv->so._setsockopt(param->sostate, param->remsock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg)); param->srv->so._setsockopt(param->sostate, param->remsock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg));
#endif
if(extbindtodevice(param, param->remsock)) return 12; if(extbindtodevice(param, param->remsock)) return 12;
#ifdef WITH_UN #ifdef WITH_UN
if(*SAFAMILY(&param->sinsl) != AF_UNIX) if(*SAFAMILY(&param->sinsl) != AF_UNIX)

View File

@ -38,6 +38,9 @@ _3proxy_mutex_t bandlim_mutex;
_3proxy_mutex_t connlim_mutex; _3proxy_mutex_t connlim_mutex;
_3proxy_mutex_t tc_mutex; _3proxy_mutex_t tc_mutex;
_3proxy_mutex_t config_mutex; _3proxy_mutex_t config_mutex;
int runningservices = 0;
_3proxy_mutex_t runningservices_mutex;
_3proxy_sem_t serviceexited;
int haveerror = 0; int haveerror = 0;
int linenum = 0; int linenum = 0;
@ -76,7 +79,18 @@ DWORD WINAPI startsrv(LPVOID data) {
void * startsrv(void * data) { void * startsrv(void * data) {
#endif #endif
struct child *d = (struct child *)data; struct child *d = (struct child *)data;
/* counted here rather than where the service is listed, so every way
out of it is counted too; start_proxy_thread() waits for the service
to start, so it is counted by the time that returns */
_3proxy_mutex_lock(&runningservices_mutex);
runningservices++;
_3proxy_mutex_unlock(&runningservices_mutex);
mainfunc(d->argc, (char **)d->argv); mainfunc(d->argc, (char **)d->argv);
_3proxy_mutex_lock(&runningservices_mutex);
runningservices--;
_3proxy_mutex_unlock(&runningservices_mutex);
_3proxy_sem_unlock(serviceexited);
return 0; return 0;
} }

View File

@ -23,14 +23,18 @@ void * ftpprchild(struct clientparam* param) {
SOCKET sc = INVALID_SOCKET, ss = INVALID_SOCKET, clidatasock = INVALID_SOCKET; SOCKET sc = INVALID_SOCKET, ss = INVALID_SOCKET, clidatasock = INVALID_SOCKET;
SASIZETYPE sasize; SASIZETYPE sasize;
char * req = NULL; char * req = NULL;
#ifndef _WIN32
struct linger lg; struct linger lg;
#endif
struct pollfd fds; struct pollfd fds;
if(!(buf = malloc(BUFSIZE))) RETURN(876); if(!(buf = malloc(BUFSIZE))) RETURN(876);
param->ctrlsock = param->clisock; param->ctrlsock = param->clisock;
param->operation = CONNECT; param->operation = CONNECT;
#ifndef _WIN32
lg.l_onoff = 1; lg.l_onoff = 1;
lg.l_linger = conf.timeouts[STRING_L];; lg.l_linger = conf.timeouts[STRING_L];
#endif
if(socksend(param, param->ctrlsock, (unsigned char *)"220 Ready\r\n", 11, conf.timeouts[STRING_S])!=11) {RETURN (801);} if(socksend(param, param->ctrlsock, (unsigned char *)"220 Ready\r\n", 11, conf.timeouts[STRING_S])!=11) {RETURN (801);}
for(;;){ for(;;){
i = sockgetlinebuf(param, CLIENT, buf, BUFSIZE - 10, '\n', conf.timeouts[CONNECTION_S]); i = sockgetlinebuf(param, CLIENT, buf, BUFSIZE - 10, '\n', conf.timeouts[CONNECTION_S]);
@ -262,8 +266,11 @@ void * ftpprchild(struct clientparam* param) {
} }
sc = param->remsock; sc = param->remsock;
param->remsock = ss; param->remsock = ss;
#ifndef _WIN32
/* not on Windows, see the accepting side in proxymain.c */
param->srv->so._setsockopt(param->sostate, param->remsock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg)); param->srv->so._setsockopt(param->sostate, param->remsock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg));
param->srv->so._setsockopt(param->sostate, clidatasock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg)); param->srv->so._setsockopt(param->sostate, clidatasock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg));
#endif
param->clisock = clidatasock; param->clisock = clidatasock;
res = mapsocket(param, conf.timeouts[CONNECTION_S]); res = mapsocket(param, conf.timeouts[CONNECTION_S]);
if(param->remsock != INVALID_SOCKET) { if(param->remsock != INVALID_SOCKET) {

View File

@ -1154,6 +1154,37 @@ static void httpsrv_drain(struct clientparam *param, uint64_t len)
} }
} }
/* Ends a connection this server is closing while the client may still be
sending: a body which was not read, or a request sent behind one answered
with a close. Closing with that unread resets the connection, and the
client loses the answer. The sending side is shut down instead and what
arrives is thrown away until the client closes too, for a bounded time
and amount, which is what lets the answer be read. */
static void httpsrv_linger(struct clientparam *param)
{
char buf[HTTPSRV_BLOCK];
struct pollfd fd;
uint64_t left = HTTPSRV_MAXBODY;
time_t deadline = time(NULL) + conf.timeouts[LINGER_TO];
if(param->clisock == INVALID_SOCKET) return;
if(param->srv->so._shutdown(param->sostate, param->clisock, SHUT_WR)) return;
while(left){
int timeo = (int)(deadline - time(NULL));
int got;
if(timeo <= 0) break;
memset(&fd, 0, sizeof(fd));
fd.fd = param->clisock;
fd.events = POLLIN;
if(param->srv->so._poll(param->sostate, &fd, 1, timeo * 1000) <= 0) break;
got = (int)param->srv->so._recv(param->sostate, param->clisock, buf,
(left > sizeof(buf))? (int)sizeof(buf) : (int)left, 0);
if(got <= 0) break;
left -= (uint64_t)got;
}
}
/* Keeps the request as the client wrote it. Only what a handoff needs: the /* Keeps the request as the client wrote it. Only what a handoff needs: the
request line and the headers, exactly as they came, since the proxy code request line and the headers, exactly as they came, since the proxy code
reads them again from the beginning. */ reads them again from the beginning. */
@ -1451,6 +1482,12 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
} }
} }
if(i && i != 10){ if(i && i != 10){
/* The body goes before the answer, as for any answer this server
gives: left in the stream it would be read as the next request. */
if(!r->drained){
if(r->contentlen) httpsrv_drain(param, r->contentlen);
r->drained = 1;
}
/* 4 no credentials, 5 unknown user, 6 wrong password: all of them /* 4 no credentials, 5 unknown user, 6 wrong password: all of them
should let the client offer credentials again. */ should let the client offer credentials again. */
if(i >= 4 && i <= 6) op_authrequired(r); if(i >= 4 && i <= 6) op_authrequired(r);
@ -1573,7 +1610,12 @@ void * httpsrvchild(struct clientparam *param)
dolog(param, (unsigned char *)logbuf); dolog(param, (unsigned char *)logbuf);
} }
if(handoff || !r.keepalive) break; if(handoff) break;
if(!r.keepalive){
/* the client may still be sending; let it read the answer */
httpsrv_linger(param);
break;
}
first = 0; first = 0;
} }
/* A child named by an access rule takes the connection over, which the /* A child named by an access rule takes the connection over, which the

View File

@ -467,6 +467,10 @@ struct dictionary;
struct node; struct node;
struct property; struct property;
extern _3proxy_mutex_t config_mutex; extern _3proxy_mutex_t config_mutex;
/* services running, and a post each time one of them has finished */
extern int runningservices;
extern _3proxy_mutex_t runningservices_mutex;
extern _3proxy_sem_t serviceexited;
extern _3proxy_mutex_t bandlim_mutex; extern _3proxy_mutex_t bandlim_mutex;
extern _3proxy_mutex_t connlim_mutex; extern _3proxy_mutex_t connlim_mutex;
extern _3proxy_mutex_t tc_mutex; extern _3proxy_mutex_t tc_mutex;
@ -497,7 +501,8 @@ extern struct socketoptions {
int opt; int opt;
char * optname; char * optname;
} sockopts[]; } sockopts[];
void setopts(SOCKET s, int opts); int getopts(const char *s, struct sockoptset *opts);
void setopts(SOCKET s, const struct sockoptset *opts);
char * printopts(char *sep); char * printopts(char *sep);
#ifdef _WINCE #ifdef _WINCE

View File

@ -224,27 +224,62 @@ char * printopts(char *sep){
} }
int getopts(const char *s){ /* A comma separated list of options, each NAME or NAME=VALUE; a name alone
int i=0, ret=0; stands for 1, or for TCP_MAXSEG for the value of maxseg. Options are added
for(; sockopts[i].optname; i++)if(strstr(s,sockopts[i].optname)) ret |= (1<<i); to what is already set, so a default is changed only by naming it, and
return ret; turned off with NAME=0. An unknown name or a value which is not a number
makes the whole list fail. */
int getopts(const char *s, struct sockoptset *opts){
while(*s){
const char *end = strchr(s, ',');
const char *eq;
size_t len;
int i, value = 1;
if(!end) end = s + strlen(s);
eq = memchr(s, '=', (size_t)(end - s));
len = (size_t)((eq? eq : end) - s);
if(eq){
char *num;
if(!len) return -1;
value = (int)strtol(eq + 1, &num, 0);
if(num == eq + 1 || num != end) return -1;
}
else if(len) {
#ifdef TCP_MAXSEG
if(len == 10 && !strncmp(s, "TCP_MAXSEG", 10)) value = 0;
#endif
}
if(len){
for(i = 0; sockopts[i].optname; i++)
if(strlen(sockopts[i].optname) == len && !strncmp(s, sockopts[i].optname, len)) break;
if(!sockopts[i].optname || i >= MAXSOCKOPTS) return -1;
opts->set |= (1u << i);
opts->value[i] = value;
}
s = *end? end + 1 : end;
}
return 0;
} }
void setopts(SOCKET s, int opts){ void setopts(SOCKET s, const struct sockoptset *opts){
int i, opt, set; int i, value;
for(i = 0; opts >= (opt = (1<<i)); i++){
set = 1; for(i = 0; sockopts[i].optname && i < MAXSOCKOPTS; i++){
if(!(opts->set & (1u << i))) continue;
value = opts->value[i];
#ifdef TCP_MAXSEG #ifdef TCP_MAXSEG
if(sockopts[i].opt == TCP_MAXSEG){ if(sockopts[i].opt == TCP_MAXSEG && *sockopts[i].optname == 'T'){
if(!conf.maxseg) continue; if(!value) value = conf.maxseg;
set = conf.maxseg; if(!value) continue;
} }
#endif #endif
if(opts & opt) setsockopt(s, *sockopts[i].optname == 'T'? IPPROTO_TCP: setsockopt(s, *sockopts[i].optname == 'T'? IPPROTO_TCP:
#ifdef SOL_IP #ifdef SOL_IP
*sockopts[i].optname == 'I'? SOL_IP: *sockopts[i].optname == 'I'? SOL_IP:
#endif #endif
SOL_SOCKET, sockopts[i].opt, (char *)&set, sizeof(set)); SOL_SOCKET, sockopts[i].opt, (char *)&value, sizeof(value));
} }
} }
@ -306,7 +341,9 @@ int MODULEMAINFUNC (int argc, char** argv){
unsigned char *cbc_string = NULL, *cbl_string = NULL; unsigned char *cbc_string = NULL, *cbl_string = NULL;
PROXYSOCKADDRTYPE cbsa; PROXYSOCKADDRTYPE cbsa;
FILE *fp = NULL; FILE *fp = NULL;
#ifndef _WIN32
struct linger lg; struct linger lg;
#endif
int nlog = 5000; int nlog = 5000;
int haveslot = 0, nfull = 0; int haveslot = 0, nfull = 0;
#ifdef __linux__ #ifdef __linux__
@ -354,7 +391,9 @@ int MODULEMAINFUNC (int argc, char** argv){
" -64 Prefer IPv6 for outgoing connections, use both IPv4 and IPv6\n" " -64 Prefer IPv6 for outgoing connections, use both IPv4 and IPv6\n"
" -ocOPTIONS, -osOPTIONS, -olOPTIONS, -orOPTIONS -oROPTIONS - options for\n" " -ocOPTIONS, -osOPTIONS, -olOPTIONS, -orOPTIONS -oROPTIONS - options for\n"
" to-client (oc), to-server (os), listening (ol) socket, connect back client\n" " to-client (oc), to-server (os), listening (ol) socket, connect back client\n"
" (or) socket, connect back server (oR) listening socket\n" " (or) socket, connect back server (oR) listening socket, a comma separated\n"
" list of NAME or NAME=VALUE (NAME=0 turns an option off); TCP_NODELAY is\n"
" set on to-client and to-server sockets by default\n"
" where possible options are: "; " where possible options are: ";
#endif #endif
@ -399,6 +438,34 @@ int MODULEMAINFUNC (int argc, char** argv){
srvinit(&srv, &defparam); srvinit(&srv, &defparam);
srv.pf = childdef.pf; srv.pf = childdef.pf;
isudp = childdef.isudp; isudp = childdef.isudp;
/* Default socket options, which -oc, -os, -ol and -oR change by naming
them, NAME=0 to turn one off.
Nagle's algorithm holds back a short write until the previous one is
acknowledged, and a peer delays its acknowledgement, which stalls every
reply written in more than one piece.
SO_REUSEADDR is not set on Windows: it is not needed to rebind a
listening port there, and it only allows another local process to bind
the same address and port, with undefined behaviour as to which socket
receives the connections. Use -olSO_EXCLUSIVEADDRUSE to prevent that
instead. A Windows build which does not share the listening socket is the
exception: its UDP services bind a second socket to the same address to
answer from, and Windows only allows that when both sockets ask for it.
*/
(void)getopts("TCP_NODELAY", &srv.clisockopts);
(void)getopts("TCP_NODELAY", &srv.srvsockopts);
#ifndef _WIN32
(void)getopts("SO_REUSEADDR", &srv.lissockopts);
(void)getopts("SO_REUSEADDR", &srv.cbssockopts);
#elif !defined SHARE_UDP_SOCKET
if(isudp) (void)getopts("SO_REUSEADDR", &srv.lissockopts);
#endif
#ifdef SO_REUSEPORT
(void)getopts("SO_REUSEPORT", &srv.lissockopts);
(void)getopts("SO_REUSEPORT", &srv.cbssockopts);
#endif
#ifndef NOUDPMAIN #ifndef NOUDPMAIN
if(isudp) { if(isudp) {
if(!udp_table.ihashtable)inithashtable(&udp_table, 64, 256, 65536); if(!udp_table.ihashtable)inithashtable(&udp_table, 64, 256, 65536);
@ -625,7 +692,8 @@ int MODULEMAINFUNC (int argc, char** argv){
break; break;
case 's': case 's':
#ifdef WITHSPLICE #ifdef WITHSPLICE
if(isudp || srv.service == S_ADMIN) /* admin is an httpsrv, where -s is the limited mode */
if(isudp || srv.service == S_ADMIN || srv.service == S_HTTPSRV)
#endif #endif
srv.s_option = 1 + atoi(argv[i]+2); srv.s_option = 1 + atoi(argv[i]+2);
#ifdef WITHSPLICE #ifdef WITHSPLICE
@ -636,19 +704,19 @@ int MODULEMAINFUNC (int argc, char** argv){
case 'o': case 'o':
switch(argv[i][2]){ switch(argv[i][2]){
case 's': case 's':
srv.srvsockopts = getopts(argv[i]+3); error = getopts(argv[i]+3, &srv.srvsockopts);
break; break;
case 'c': case 'c':
srv.clisockopts = getopts(argv[i]+3); error = getopts(argv[i]+3, &srv.clisockopts);
break; break;
case 'l': case 'l':
srv.lissockopts = getopts(argv[i]+3); error = getopts(argv[i]+3, &srv.lissockopts);
break; break;
case 'r': case 'r':
srv.cbcsockopts = getopts(argv[i]+3); error = getopts(argv[i]+3, &srv.cbcsockopts);
break; break;
case 'R': case 'R':
srv.cbcsockopts = getopts(argv[i]+3); error = getopts(argv[i]+3, &srv.cbssockopts);
break; break;
default: default:
error = 1; error = 1;
@ -827,42 +895,13 @@ int MODULEMAINFUNC (int argc, char** argv){
perror("socket()"); perror("socket()");
return -2; return -2;
} }
setopts(sock, srv.lissockopts); setopts(sock, &srv.lissockopts);
#ifdef _WIN32 #ifdef _WIN32
ioctlsocket(sock, FIONBIO, &ul); ioctlsocket(sock, FIONBIO, &ul);
#else #else
fcntl(sock,F_SETFL,O_NONBLOCK | fcntl(sock,F_GETFL)); fcntl(sock,F_SETFL,O_NONBLOCK | fcntl(sock,F_GETFL));
#endif #endif
srv.srvsock = sock; srv.srvsock = sock;
#ifdef WITH_UN
if(*SAFAMILY(&srv.intsa) != AF_UNIX)
#endif
{
/* SO_REUSEADDR is not set on Windows: it is not needed to rebind a listening
port there, and it only allows another local process to bind the same
address and port, with undefined behaviour as to which socket receives the
connections. Use -olSO_EXCLUSIVEADDRUSE to prevent that instead.
A Windows build which does not share the listening socket is the exception:
its UDP services bind a second socket to the same address to answer from,
and Windows only allows that when both sockets ask for it.
*/
#ifndef _WIN32
opt = 1;
if(srv.so._setsockopt(srv.so.state, sock, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(int)))perror("setsockopt()");
#else
#ifndef SHARE_UDP_SOCKET
if(isudp){
opt = 1;
if(srv.so._setsockopt(srv.so.state, sock, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(int)))perror("setsockopt()");
}
#endif
#endif
#ifdef SO_REUSEPORT
opt = 1;
srv.so._setsockopt(srv.so.state, sock, SOL_SOCKET, SO_REUSEPORT, (char *)&opt, sizeof(int));
#endif
}
#if defined SO_BINDTODEVICE || defined IP_BOUND_IF #if defined SO_BINDTODEVICE || defined IP_BOUND_IF
if(bindtodevice(&srv.so, srv.so.state, sock, srv.ibindtodevice, *SAFAMILY(&srv.intsa))) { if(bindtodevice(&srv.so, srv.so.state, sock, srv.ibindtodevice, *SAFAMILY(&srv.intsa))) {
dolog(&defparam, (unsigned char *)"failed to bind device"); dolog(&defparam, (unsigned char *)"failed to bind device");
@ -955,16 +994,7 @@ int MODULEMAINFUNC (int argc, char** argv){
freesrvstrings(&srv, cbc_string, cbl_string); freesrvstrings(&srv, cbc_string, cbl_string);
return -6; return -6;
} }
#ifndef _WIN32 setopts(srv.cbsock, &srv.cbssockopts);
opt = 1;
srv.so._setsockopt(srv.so.state, srv.cbsock, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(int));
#endif
#ifdef SO_REUSEPORT
opt = 1;
srv.so._setsockopt(srv.so.state, srv.cbsock, SOL_SOCKET, SO_REUSEPORT, (char *)&opt, sizeof(int));
#endif
setopts(srv.cbsock, srv.cbssockopts);
if(srv.so._bind(srv.so.state, srv.cbsock, (struct sockaddr*)&cbsa, SASIZE(&cbsa))==-1) { if(srv.so._bind(srv.so.state, srv.cbsock, (struct sockaddr*)&cbsa, SASIZE(&cbsa))==-1) {
dolog(&defparam, (unsigned char *)"Failed to bind connect back socket"); dolog(&defparam, (unsigned char *)"Failed to bind connect back socket");
@ -1023,7 +1053,7 @@ int MODULEMAINFUNC (int argc, char** argv){
if(iscbc){ if(iscbc){
new_sock=so._socket(so.state, SASOCK(&defparam.sincr), SOCK_STREAM, IPPROTO_TCP); new_sock=so._socket(so.state, SASOCK(&defparam.sincr), SOCK_STREAM, IPPROTO_TCP);
if(new_sock != INVALID_SOCKET){ if(new_sock != INVALID_SOCKET){
setopts(new_sock, srv.cbcsockopts); setopts(new_sock, &srv.cbcsockopts);
parsehost(srv.family, cbc_string, (struct sockaddr *)&defparam.sincr); parsehost(srv.family, cbc_string, (struct sockaddr *)&defparam.sincr);
#if defined SO_BINDTODEVICE || defined IP_BOUND_IF #if defined SO_BINDTODEVICE || defined IP_BOUND_IF
@ -1100,7 +1130,7 @@ int MODULEMAINFUNC (int argc, char** argv){
} }
continue; continue;
} }
setopts(new_sock, srv.clisockopts); setopts(new_sock, &srv.clisockopts);
} }
size = sizeof(defparam.sincl); size = sizeof(defparam.sincl);
if(srv.so._getsockname(srv.so.state, new_sock, (struct sockaddr *)&defparam.sincl, &size)){ if(srv.so._getsockname(srv.so.state, new_sock, (struct sockaddr *)&defparam.sincl, &size)){
@ -1117,10 +1147,14 @@ int MODULEMAINFUNC (int argc, char** argv){
#else #else
fcntl(new_sock,F_SETFL,O_NONBLOCK | fcntl(new_sock,F_GETFL)); fcntl(new_sock,F_SETFL,O_NONBLOCK | fcntl(new_sock,F_GETFL));
#endif #endif
#ifndef _WIN32
/* Not on Windows: there closesocket() on a non-blocking socket with a linger
timeout fails with WSAEWOULDBLOCK while data is queued, leaving the socket
open. Without it the close completes in the background, data first. */
lg.l_onoff = 1; lg.l_onoff = 1;
lg.l_linger = conf.timeouts[STRING_L]; lg.l_linger = conf.timeouts[STRING_L];
srv.so._setsockopt(srv.so.state, new_sock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg)); srv.so._setsockopt(srv.so.state, new_sock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg));
#endif
srv.so._setsockopt(srv.so.state, new_sock, SOL_SOCKET, SO_OOBINLINE, (char *)&opt, sizeof(int)); srv.so._setsockopt(srv.so.state, new_sock, SOL_SOCKET, SO_OOBINLINE, (char *)&opt, sizeof(int));
} }
#ifndef NOUDPMAIN #ifndef NOUDPMAIN
@ -1413,21 +1447,34 @@ void freeparam(struct clientparam * param) {
} }
if(param->clibuf) free(param->clibuf); if(param->clibuf) free(param->clibuf);
if(param->srvbuf) free(param->srvbuf); if(param->srvbuf) free(param->srvbuf);
/* Not shut down on Windows: there shutting down receiving resets the
connection as soon as the peer sends anything more, and close()
alone sends whatever is queued before its FIN. Elsewhere it stays:
on the BSDs it discards what is left unread, so the close that
follows is not turned into a reset. */
if(param->srv) { if(param->srv) {
if(param->ctrlsocksrv != INVALID_SOCKET && param->ctrlsocksrv != param->remsock) { if(param->ctrlsocksrv != INVALID_SOCKET && param->ctrlsocksrv != param->remsock) {
#ifndef _WIN32
param->srv->so._shutdown(param->sostate, param->ctrlsocksrv, SHUT_RDWR); param->srv->so._shutdown(param->sostate, param->ctrlsocksrv, SHUT_RDWR);
#endif
param->srv->so._closesocket(param->sostate, param->ctrlsocksrv); param->srv->so._closesocket(param->sostate, param->ctrlsocksrv);
} }
if(param->ctrlsock != INVALID_SOCKET && param->ctrlsock != param->clisock) { if(param->ctrlsock != INVALID_SOCKET && param->ctrlsock != param->clisock) {
#ifndef _WIN32
param->srv->so._shutdown(param->sostate, param->ctrlsock, SHUT_RDWR); param->srv->so._shutdown(param->sostate, param->ctrlsock, SHUT_RDWR);
#endif
param->srv->so._closesocket(param->sostate, param->ctrlsock); param->srv->so._closesocket(param->sostate, param->ctrlsock);
} }
if(param->remsock != INVALID_SOCKET) { if(param->remsock != INVALID_SOCKET) {
#ifndef _WIN32
param->srv->so._shutdown(param->sostate, param->remsock, SHUT_RDWR); param->srv->so._shutdown(param->sostate, param->remsock, SHUT_RDWR);
#endif
param->srv->so._closesocket(param->sostate, param->remsock); param->srv->so._closesocket(param->sostate, param->remsock);
} }
if(param->clisock != INVALID_SOCKET) { if(param->clisock != INVALID_SOCKET) {
#ifndef _WIN32
param->srv->so._shutdown(param->sostate, param->clisock, SHUT_RDWR); param->srv->so._shutdown(param->sostate, param->clisock, SHUT_RDWR);
#endif
param->srv->so._closesocket(param->sostate, param->clisock); param->srv->so._closesocket(param->sostate, param->clisock);
} }
} }

View File

@ -819,9 +819,6 @@ log("timeout");
else if(inclientpipe || inserverpipe) res = 94; else if(inclientpipe || inserverpipe) res = 94;
#endif #endif
if((param->nwrites > 0 && !SERVERTERMWRITE) || (param->nreads > 0 && !CLIENTTERMWRITE))
usleep(SLEEPTIME * 10);
CLEANRET: CLEANRET:
#ifdef WITHSPLICE #ifdef WITHSPLICE

View File

@ -184,7 +184,7 @@ void * sockschild(struct clientparam* param) {
param->operation = command == 2?BIND:UDPASSOC; param->operation = command == 2?BIND:UDPASSOC;
if(command == 2){ if(command == 2){
if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(&param->req), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN (11);} if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(&param->req), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN (11);}
setopts(param->remsock, param->srv->lissockopts); setopts(param->remsock, &param->srv->lissockopts);
if(extbindtodevice(param, param->remsock)) {RETURN (12);} if(extbindtodevice(param, param->remsock)) {RETURN (12);}
} }
break; break;

View File

@ -529,6 +529,14 @@ struct filterp {
#define MAX_FILTERS 16 #define MAX_FILTERS 16
/* Socket options for one kind of socket, -oc and the like: which of
sockopts[] are given, by their index there, and the value of each. */
#define MAXSOCKOPTS 32
struct sockoptset {
unsigned set;
int value[MAXSOCKOPTS];
};
struct sockfuncs { struct sockfuncs {
void * state; void * state;
@ -613,7 +621,7 @@ struct srvparam {
int stacksize; int stacksize;
int noforce; int noforce;
int anonymous; int anonymous;
int clisockopts, srvsockopts, lissockopts, cbcsockopts, cbssockopts; struct sockoptset clisockopts, srvsockopts, lissockopts, cbcsockopts, cbssockopts;
int gracetraf, gracenum, gracedelay; int gracetraf, gracenum, gracedelay;
int requirecert; int requirecert;
int haproxy; int haproxy;

View File

@ -1,10 +1,12 @@
"""The admin interface, now a set of handlers on the HTTP server.""" """The admin interface, now a set of handlers on the HTTP server."""
import time
def run(t): def run(t):
adm = t.free_port() adm = t.free_port()
lim = t.free_port() lim = t.free_port()
t.start("admin", f""" server = t.start("admin", f"""
log log
auth iponly auth iponly
allow * allow *
@ -20,16 +22,34 @@ def run(t):
url = f"http://127.0.0.1:{adm}" url = f"http://127.0.0.1:{adm}"
def reload(path):
"""Ask for a reload and wait for it to be done. It happens on the next
tick of the main loop, at most a second later; both old services stop
and the new ones start, and a request in between is refused (Windows
cannot bind a port still listening) or reset."""
before = server.output()
accepting = before.count("Accepting connections")
exiting = before.count("Exiting thread")
reply = t.http(url + path)
deadline = time.time() + 10
while time.time() < deadline:
now = server.output()
if (now.count("Accepting connections") >= accepting + 2
and now.count("Exiting thread") >= exiting + 2):
break
time.sleep(0.05)
return reply
# --- the predefined pages ----------------------------------------- # --- the predefined pages -----------------------------------------
t.eq(200, t.http(url + "/").status, "the main page") t.eq(200, t.http(url + "/").status, "the main page")
t.eq(200, t.http(url + "/C").status, "the counters page") t.eq(200, t.http(url + "/C").status, "the counters page")
t.eq(200, t.http(url + "/R").status, "the reload page") t.eq(200, reload("/R").status, "the reload page")
t.eq(200, t.http(url + "/S").status, "the services page") t.eq(200, t.http(url + "/S").status, "the services page")
counters = t.http(url + "/C") counters = t.http(url + "/C")
t.contains(counters, "countin", "the counters page names the counter type") t.contains(counters, "countin", "the counters page names the counter type")
t.contains(counters, "<tr>", "the counters page renders a table") t.contains(counters, "<tr>", "the counters page renders a table")
t.contains(t.http(url + "/R"), "Reload", "the reload page confirms the request") t.contains(reload("/R"), "Reload", "the reload page confirms the request")
t.contains(t.http(url + "/S"), "<", "the services page returns markup") t.contains(t.http(url + "/S"), "<", "the services page returns markup")
# --- the menu no longer offers the removed config editor ----------- # --- the menu no longer offers the removed config editor -----------