mirror of
https://github.com/3proxy/3proxy.git
synced 2026-10-30 14:45:49 +08:00
Compare commits
7 Commits
98e8dfb004
...
94feaf2728
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94feaf2728 | ||
|
|
c827f77137 | ||
|
|
0e8d757464 | ||
|
|
f128fd2aa7 | ||
|
|
baea44477e | ||
|
|
9ca0b84774 | ||
|
|
36382bda96 |
@ -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_PAM "Enable PAM/PamAuth" ON)
|
||||
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_WSAPOLL "Use WSAPoll instead of select() (Windows only)" ON)
|
||||
option(3PROXY_USE_NETFILTER "Enable Linux netfilter support (Linux only)" ON)
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
# add -DWITH_ODBC to CFLAGS and -lodbc to LIBS to compile with ODBC
|
||||
# library support. Add -DSAFESQL for poorely written ODBC library / drivers.
|
||||
#
|
||||
# Linux splice() support is not built by default, it is slower than the
|
||||
# read/write path for most traffic. Add -DWITHSPLICE to CFLAGS to build it,
|
||||
# it must additionally be enabled per service with the -s option.
|
||||
# Linux splice() support is built, but used only by services given the -s
|
||||
# option: it is faster for large transfers and slower for small requests.
|
||||
# Remove -DWITHSPLICE from CFLAGS to build without it.
|
||||
|
||||
BUILDDIR = ../bin/
|
||||
PREFIX ?= 3proxy_
|
||||
@ -14,7 +14,7 @@ CRYPT_PREFIX ?= $(PREFIX)
|
||||
CC ?= gcc
|
||||
|
||||
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
|
||||
LN ?= ${CC}
|
||||
DCFLAGS ?= -fPIC
|
||||
|
||||
2
bench/.gitignore
vendored
Normal file
2
bench/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
bin/
|
||||
results/
|
||||
97
bench/README.md
Normal file
97
bench/README.md
Normal 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
575
bench/bench.py
Normal 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
52
bench/build.sh
Normal 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
|
||||
@ -34,7 +34,8 @@ Each connected client requires:
|
||||
<ul>
|
||||
<li>1 thread (process)
|
||||
<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 during authentication or logging for RADIUS authentication or logging.
|
||||
<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.
|
||||
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>
|
||||
|
||||
@ -641,17 +644,37 @@ monitor "/some/path/3proxy/3proxy.ver"
|
||||
e.g., with "touch /some/path/3proxy/3proxy.ver".
|
||||
</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
|
||||
without the need for bandwidth, e.g., messengers or small web requests,
|
||||
you can eliminate Nagle's algorithm delay with the TCP_NODELAY flag. Usage example:
|
||||
TCP_NODELAY is set by default on client (oc) and server (os)
|
||||
connections: without it, Nagle's algorithm holds back a short write until the
|
||||
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>
|
||||
proxy -osTCP_NODELAY -ocTCP_NODELAY
|
||||
proxy -osTCP_NODELAY=0 -ocTCP_NODELAY=0
|
||||
</pre>
|
||||
sets TCP_NODELAY for client (oc) and server (os) connections.
|
||||
<p>Do not use TCP_NODELAY on slow connections with high delays when
|
||||
connection bandwidth is a bottleneck.
|
||||
|
||||
<h4>Use Splice to Speed Up Large Data Transfers</h4>
|
||||
|
||||
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>
|
||||
|
||||
@ -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.
|
||||
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
|
||||
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
|
||||
buffer (typically 64K) but keep the request sizes close to the chosen average
|
||||
on large file uploads/downloads.
|
||||
|
||||
@ -184,8 +184,10 @@ connect to given remote HOST:port instead of listening local connection on -p or
|
||||
.br
|
||||
.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.
|
||||
\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.
|
||||
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
|
||||
.B -H
|
||||
(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
|
||||
view without ability to reset.
|
||||
.br
|
||||
(for TCP services, Linux) enable splice(). splice() is not built by default and
|
||||
is disabled even when built, because current Linux does not implement
|
||||
SPLICE_F_MOVE, so no real zero-copy takes place and the read/write path is faster
|
||||
for most traffic. Rebuild with -DWITHSPLICE to make -s available, -s0 disables it
|
||||
explicitly.
|
||||
(for TCP services, Linux) use splice() to pass data between the client and the
|
||||
server without copying it through 3proxy. It is off unless -s is given (-s0 turns
|
||||
it off explicitly). splice() suits large transfers: they go faster and use less
|
||||
CPU. Small requests and short connections go slower with it, every connection
|
||||
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
|
||||
.B -C
|
||||
(for TCP services) a connection closed by any of the sides terminates the
|
||||
|
||||
56
src/3proxy.c
56
src/3proxy.c
@ -74,13 +74,8 @@ void __stdcall CommandHandler( DWORD dwCommand )
|
||||
SetStatus( SERVICE_STOP_PENDING, 0, 1 );
|
||||
conf.timetoexit = 1;
|
||||
conf.paused++;
|
||||
Sleep(2000);
|
||||
SetStatus( SERVICE_STOPPED, 0, 0 );
|
||||
#ifdef WITH_ODBC
|
||||
_3proxy_mutex_lock(&log_mutex);
|
||||
close_sql();
|
||||
_3proxy_mutex_unlock(&log_mutex);
|
||||
#endif
|
||||
/* ServiceMain() reports the service stopped once cyclestep() has
|
||||
seen this and the services are done */
|
||||
break;
|
||||
case SERVICE_CONTROL_PAUSE:
|
||||
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_RUNNING, 0, 0 );
|
||||
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
|
||||
|
||||
@ -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){
|
||||
struct tm *tm;
|
||||
time_t minutecounter;
|
||||
@ -336,9 +371,7 @@ void cyclestep(void){
|
||||
if(conf.timetoexit){
|
||||
conf.paused++;
|
||||
doschedule();
|
||||
usleep(SLEEPTIME*999);
|
||||
usleep(SLEEPTIME*999);
|
||||
usleep(SLEEPTIME*999);
|
||||
waitservices();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -517,6 +550,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
|
||||
}
|
||||
|
||||
_3proxy_mutex_init(&config_mutex);
|
||||
_3proxy_mutex_init(&runningservices_mutex);
|
||||
_3proxy_mutex_init(&bandlim_mutex);
|
||||
_3proxy_mutex_init(&connlim_mutex);
|
||||
_3proxy_mutex_init(&tc_mutex);
|
||||
@ -524,7 +558,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
|
||||
#ifndef NORADIUS
|
||||
_3proxy_mutex_init(&rad_mutex);
|
||||
#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");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -387,7 +387,6 @@ int
|
||||
return shutdown(s, how);
|
||||
}
|
||||
int WINAPI def_closesocket(void* state, SOCKET s){
|
||||
usleep(SLEEPTIME);
|
||||
return closesocket(s);
|
||||
}
|
||||
#else
|
||||
@ -715,7 +714,9 @@ int doconnect(struct clientparam * param){
|
||||
if(param->srv->so._getpeername(param->sostate, param->remsock, (struct sockaddr *)¶m->sinsr, &size)==-1) {return (14);}
|
||||
}
|
||||
else {
|
||||
#ifndef _WIN32
|
||||
struct linger lg = {1,conf.timeouts[LINGER_TO]};
|
||||
#endif
|
||||
int tries = RANGETRIES;
|
||||
|
||||
if(SAISNULL(¶m->sinsr)){
|
||||
@ -745,9 +746,12 @@ int doconnect(struct clientparam * param){
|
||||
param->sinsl = param->srv->extsa;
|
||||
}
|
||||
*SAPORT(¶m->sinsl) = 0;
|
||||
setopts(param->remsock, param->srv->srvsockopts);
|
||||
setopts(param->remsock, ¶m->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));
|
||||
#endif
|
||||
if(extbindtodevice(param, param->remsock)) return 12;
|
||||
#ifdef WITH_UN
|
||||
if(*SAFAMILY(¶m->sinsl) != AF_UNIX)
|
||||
|
||||
14
src/conf.c
14
src/conf.c
@ -38,6 +38,9 @@ _3proxy_mutex_t bandlim_mutex;
|
||||
_3proxy_mutex_t connlim_mutex;
|
||||
_3proxy_mutex_t tc_mutex;
|
||||
_3proxy_mutex_t config_mutex;
|
||||
int runningservices = 0;
|
||||
_3proxy_mutex_t runningservices_mutex;
|
||||
_3proxy_sem_t serviceexited;
|
||||
|
||||
int haveerror = 0;
|
||||
int linenum = 0;
|
||||
@ -76,7 +79,18 @@ DWORD WINAPI startsrv(LPVOID data) {
|
||||
void * startsrv(void * data) {
|
||||
#endif
|
||||
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);
|
||||
_3proxy_mutex_lock(&runningservices_mutex);
|
||||
runningservices--;
|
||||
_3proxy_mutex_unlock(&runningservices_mutex);
|
||||
_3proxy_sem_unlock(serviceexited);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@ -23,14 +23,18 @@ void * ftpprchild(struct clientparam* param) {
|
||||
SOCKET sc = INVALID_SOCKET, ss = INVALID_SOCKET, clidatasock = INVALID_SOCKET;
|
||||
SASIZETYPE sasize;
|
||||
char * req = NULL;
|
||||
#ifndef _WIN32
|
||||
struct linger lg;
|
||||
#endif
|
||||
struct pollfd fds;
|
||||
|
||||
if(!(buf = malloc(BUFSIZE))) RETURN(876);
|
||||
param->ctrlsock = param->clisock;
|
||||
param->operation = CONNECT;
|
||||
#ifndef _WIN32
|
||||
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);}
|
||||
for(;;){
|
||||
i = sockgetlinebuf(param, CLIENT, buf, BUFSIZE - 10, '\n', conf.timeouts[CONNECTION_S]);
|
||||
@ -262,8 +266,11 @@ void * ftpprchild(struct clientparam* param) {
|
||||
}
|
||||
sc = param->remsock;
|
||||
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, clidatasock, SOL_SOCKET, SO_LINGER, (char *)&lg, sizeof(lg));
|
||||
#endif
|
||||
param->clisock = clidatasock;
|
||||
res = mapsocket(param, conf.timeouts[CONNECTION_S]);
|
||||
if(param->remsock != INVALID_SOCKET) {
|
||||
|
||||
@ -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
|
||||
request line and the headers, exactly as they came, since the proxy code
|
||||
reads them again from the beginning. */
|
||||
@ -1451,6 +1482,12 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
|
||||
}
|
||||
}
|
||||
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
|
||||
should let the client offer credentials again. */
|
||||
if(i >= 4 && i <= 6) op_authrequired(r);
|
||||
@ -1573,7 +1610,12 @@ void * httpsrvchild(struct clientparam *param)
|
||||
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;
|
||||
}
|
||||
/* A child named by an access rule takes the connection over, which the
|
||||
|
||||
@ -467,6 +467,10 @@ struct dictionary;
|
||||
struct node;
|
||||
struct property;
|
||||
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 connlim_mutex;
|
||||
extern _3proxy_mutex_t tc_mutex;
|
||||
@ -497,7 +501,8 @@ extern struct socketoptions {
|
||||
int opt;
|
||||
char * optname;
|
||||
} 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);
|
||||
|
||||
#ifdef _WINCE
|
||||
|
||||
173
src/proxymain.c
173
src/proxymain.c
@ -224,27 +224,62 @@ char * printopts(char *sep){
|
||||
}
|
||||
|
||||
|
||||
int getopts(const char *s){
|
||||
int i=0, ret=0;
|
||||
for(; sockopts[i].optname; i++)if(strstr(s,sockopts[i].optname)) ret |= (1<<i);
|
||||
return ret;
|
||||
/* A comma separated list of options, each NAME or NAME=VALUE; a name alone
|
||||
stands for 1, or for TCP_MAXSEG for the value of maxseg. Options are added
|
||||
to what is already set, so a default is changed only by naming it, and
|
||||
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){
|
||||
int i, opt, set;
|
||||
for(i = 0; opts >= (opt = (1<<i)); i++){
|
||||
set = 1;
|
||||
void setopts(SOCKET s, const struct sockoptset *opts){
|
||||
int i, value;
|
||||
|
||||
for(i = 0; sockopts[i].optname && i < MAXSOCKOPTS; i++){
|
||||
if(!(opts->set & (1u << i))) continue;
|
||||
value = opts->value[i];
|
||||
#ifdef TCP_MAXSEG
|
||||
if(sockopts[i].opt == TCP_MAXSEG){
|
||||
if(!conf.maxseg) continue;
|
||||
set = conf.maxseg;
|
||||
if(sockopts[i].opt == TCP_MAXSEG && *sockopts[i].optname == 'T'){
|
||||
if(!value) value = conf.maxseg;
|
||||
if(!value) continue;
|
||||
}
|
||||
#endif
|
||||
if(opts & opt) setsockopt(s, *sockopts[i].optname == 'T'? IPPROTO_TCP:
|
||||
setsockopt(s, *sockopts[i].optname == 'T'? IPPROTO_TCP:
|
||||
#ifdef SOL_IP
|
||||
*sockopts[i].optname == 'I'? SOL_IP:
|
||||
#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;
|
||||
PROXYSOCKADDRTYPE cbsa;
|
||||
FILE *fp = NULL;
|
||||
#ifndef _WIN32
|
||||
struct linger lg;
|
||||
#endif
|
||||
int nlog = 5000;
|
||||
int haveslot = 0, nfull = 0;
|
||||
#ifdef __linux__
|
||||
@ -354,7 +391,9 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
" -64 Prefer IPv6 for outgoing connections, use both IPv4 and IPv6\n"
|
||||
" -ocOPTIONS, -osOPTIONS, -olOPTIONS, -orOPTIONS -oROPTIONS - options for\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: ";
|
||||
#endif
|
||||
|
||||
@ -399,6 +438,34 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
srvinit(&srv, &defparam);
|
||||
srv.pf = childdef.pf;
|
||||
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
|
||||
if(isudp) {
|
||||
if(!udp_table.ihashtable)inithashtable(&udp_table, 64, 256, 65536);
|
||||
@ -625,7 +692,8 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
break;
|
||||
case 's':
|
||||
#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
|
||||
srv.s_option = 1 + atoi(argv[i]+2);
|
||||
#ifdef WITHSPLICE
|
||||
@ -636,19 +704,19 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
case 'o':
|
||||
switch(argv[i][2]){
|
||||
case 's':
|
||||
srv.srvsockopts = getopts(argv[i]+3);
|
||||
error = getopts(argv[i]+3, &srv.srvsockopts);
|
||||
break;
|
||||
case 'c':
|
||||
srv.clisockopts = getopts(argv[i]+3);
|
||||
error = getopts(argv[i]+3, &srv.clisockopts);
|
||||
break;
|
||||
case 'l':
|
||||
srv.lissockopts = getopts(argv[i]+3);
|
||||
error = getopts(argv[i]+3, &srv.lissockopts);
|
||||
break;
|
||||
case 'r':
|
||||
srv.cbcsockopts = getopts(argv[i]+3);
|
||||
error = getopts(argv[i]+3, &srv.cbcsockopts);
|
||||
break;
|
||||
case 'R':
|
||||
srv.cbcsockopts = getopts(argv[i]+3);
|
||||
error = getopts(argv[i]+3, &srv.cbssockopts);
|
||||
break;
|
||||
default:
|
||||
error = 1;
|
||||
@ -827,42 +895,13 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
perror("socket()");
|
||||
return -2;
|
||||
}
|
||||
setopts(sock, srv.lissockopts);
|
||||
setopts(sock, &srv.lissockopts);
|
||||
#ifdef _WIN32
|
||||
ioctlsocket(sock, FIONBIO, &ul);
|
||||
#else
|
||||
fcntl(sock,F_SETFL,O_NONBLOCK | fcntl(sock,F_GETFL));
|
||||
#endif
|
||||
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(bindtodevice(&srv.so, srv.so.state, sock, srv.ibindtodevice, *SAFAMILY(&srv.intsa))) {
|
||||
dolog(&defparam, (unsigned char *)"failed to bind device");
|
||||
@ -955,16 +994,7 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
freesrvstrings(&srv, cbc_string, cbl_string);
|
||||
return -6;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
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);
|
||||
setopts(srv.cbsock, &srv.cbssockopts);
|
||||
|
||||
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");
|
||||
@ -1023,7 +1053,7 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
if(iscbc){
|
||||
new_sock=so._socket(so.state, SASOCK(&defparam.sincr), SOCK_STREAM, IPPROTO_TCP);
|
||||
if(new_sock != INVALID_SOCKET){
|
||||
setopts(new_sock, srv.cbcsockopts);
|
||||
setopts(new_sock, &srv.cbcsockopts);
|
||||
|
||||
parsehost(srv.family, cbc_string, (struct sockaddr *)&defparam.sincr);
|
||||
#if defined SO_BINDTODEVICE || defined IP_BOUND_IF
|
||||
@ -1100,7 +1130,7 @@ int MODULEMAINFUNC (int argc, char** argv){
|
||||
}
|
||||
continue;
|
||||
}
|
||||
setopts(new_sock, srv.clisockopts);
|
||||
setopts(new_sock, &srv.clisockopts);
|
||||
}
|
||||
size = sizeof(defparam.sincl);
|
||||
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
|
||||
fcntl(new_sock,F_SETFL,O_NONBLOCK | fcntl(new_sock,F_GETFL));
|
||||
#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_linger = conf.timeouts[STRING_L];
|
||||
|
||||
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));
|
||||
}
|
||||
#ifndef NOUDPMAIN
|
||||
@ -1413,21 +1447,34 @@ void freeparam(struct clientparam * param) {
|
||||
}
|
||||
if(param->clibuf) free(param->clibuf);
|
||||
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->ctrlsocksrv != INVALID_SOCKET && param->ctrlsocksrv != param->remsock) {
|
||||
#ifndef _WIN32
|
||||
param->srv->so._shutdown(param->sostate, param->ctrlsocksrv, SHUT_RDWR);
|
||||
#endif
|
||||
param->srv->so._closesocket(param->sostate, param->ctrlsocksrv);
|
||||
}
|
||||
if(param->ctrlsock != INVALID_SOCKET && param->ctrlsock != param->clisock) {
|
||||
#ifndef _WIN32
|
||||
param->srv->so._shutdown(param->sostate, param->ctrlsock, SHUT_RDWR);
|
||||
#endif
|
||||
param->srv->so._closesocket(param->sostate, param->ctrlsock);
|
||||
}
|
||||
if(param->remsock != INVALID_SOCKET) {
|
||||
#ifndef _WIN32
|
||||
param->srv->so._shutdown(param->sostate, param->remsock, SHUT_RDWR);
|
||||
#endif
|
||||
param->srv->so._closesocket(param->sostate, param->remsock);
|
||||
}
|
||||
if(param->clisock != INVALID_SOCKET) {
|
||||
#ifndef _WIN32
|
||||
param->srv->so._shutdown(param->sostate, param->clisock, SHUT_RDWR);
|
||||
#endif
|
||||
param->srv->so._closesocket(param->sostate, param->clisock);
|
||||
}
|
||||
}
|
||||
|
||||
@ -819,9 +819,6 @@ log("timeout");
|
||||
else if(inclientpipe || inserverpipe) res = 94;
|
||||
#endif
|
||||
|
||||
if((param->nwrites > 0 && !SERVERTERMWRITE) || (param->nreads > 0 && !CLIENTTERMWRITE))
|
||||
usleep(SLEEPTIME * 10);
|
||||
|
||||
CLEANRET:
|
||||
|
||||
#ifdef WITHSPLICE
|
||||
|
||||
@ -184,7 +184,7 @@ void * sockschild(struct clientparam* param) {
|
||||
param->operation = command == 2?BIND:UDPASSOC;
|
||||
if(command == 2){
|
||||
if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(¶m->req), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN (11);}
|
||||
setopts(param->remsock, param->srv->lissockopts);
|
||||
setopts(param->remsock, ¶m->srv->lissockopts);
|
||||
if(extbindtodevice(param, param->remsock)) {RETURN (12);}
|
||||
}
|
||||
break;
|
||||
|
||||
@ -529,6 +529,14 @@ struct filterp {
|
||||
|
||||
#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 {
|
||||
void * state;
|
||||
@ -613,7 +621,7 @@ struct srvparam {
|
||||
int stacksize;
|
||||
int noforce;
|
||||
int anonymous;
|
||||
int clisockopts, srvsockopts, lissockopts, cbcsockopts, cbssockopts;
|
||||
struct sockoptset clisockopts, srvsockopts, lissockopts, cbcsockopts, cbssockopts;
|
||||
int gracetraf, gracenum, gracedelay;
|
||||
int requirecert;
|
||||
int haproxy;
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
"""The admin interface, now a set of handlers on the HTTP server."""
|
||||
|
||||
import time
|
||||
|
||||
|
||||
def run(t):
|
||||
adm = t.free_port()
|
||||
lim = t.free_port()
|
||||
t.start("admin", f"""
|
||||
server = t.start("admin", f"""
|
||||
log
|
||||
auth iponly
|
||||
allow *
|
||||
@ -20,16 +22,34 @@ def run(t):
|
||||
|
||||
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 -----------------------------------------
|
||||
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 + "/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")
|
||||
|
||||
counters = t.http(url + "/C")
|
||||
t.contains(counters, "countin", "the counters page names the counter type")
|
||||
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")
|
||||
|
||||
# --- the menu no longer offers the removed config editor -----------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user