diff --git a/doc/html/man5/3proxy.cfg.5.html b/doc/html/man5/3proxy.cfg.5.html
index c6955f6..7cd5d7a 100644
--- a/doc/html/man5/3proxy.cfg.5.html
+++ b/doc/html/man5/3proxy.cfg.5.html
@@ -218,12 +218,33 @@ second. Useful to minimize polling
-s
(for admin) secure, allow only secure operations, currently
only traffic counters view without ability to reset.
-(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.
+-s alone splices both directions where it pays: a body
+of known length if at least 16K of it is left to read, a
+stream once it carries large reads, back to copying when they
+turn small. -s1 always splices what the server sends
+to the client (downloads), -s2 what the client sends
+(uploads), -s3 both; adding 4 (-s5, -s6,
+-s7, the last the same as -s, and the default)
+splices those directions only where it pays. -s0 turns
+it off. Where it pays can be tuned with
+:MIN,STREAK,SMALL
+after the mode, e.g. -s:16384,2,4096 (the default) or
+-s6:65536,3,8192: a direction is spliced after
+STREAK reads in a row of MIN bytes or more
+(SMALL from a TLS socket, which hands out a record at a
+time) and copied again after more than STREAK splices
+in a row of less than SMALL; a body of known length is
+spliced if at least MIN of it is left to read. splice()
+suits large transfers: they go faster and use less CPU. A
+spliced direction needs a pipe (two file descriptors), made
+when first needed and kept for the connection, and more kernel
+memory. A service with data filters does not splice, so that
+the filters see all of the data, nor does one ending or
+beginning TLS, unless the kernel does TLS for it both ways
+(ssl_ktls). Available in builds with WITHSPLICE, the
+default on Linux.
-C
(for TCP services) a connection closed by any of the sides
terminates the session, buffered data is delivered before
diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5
index b0bddbd..a3f0285 100644
--- a/man/3proxy.cfg.5
+++ b/man/3proxy.cfg.5
@@ -209,11 +209,18 @@ once it carries large reads, back to copying when they turn small. \fB-s1\fR alw
splices what the server sends to the client (downloads), \fB-s2\fR what the client
sends (uploads), \fB-s3\fR both; adding 4 (\fB-s5\fR, \fB-s6\fR, \fB-s7\fR, the last the same as
\fB-s\fR, and the default) splices those directions only where it pays. \fB-s0\fR turns it
-off. splice() suits large transfers: they go faster and use less CPU; small
-requests go slower with it. A spliced direction needs a pipe (two file
-descriptors), made when first needed and kept for the connection, and more
-kernel memory. A service with data filters does not splice, so that the filters
-see all of the data. Available in builds with WITHSPLICE, the default on Linux.
+off. Where it pays can be tuned with \fB:\fIMIN\fB,\fISTREAK\fB,\fISMALL\fR after the
+mode, e.g. \fB-s:16384,2,4096\fR (the default) or \fB-s6:65536,3,8192\fR: a direction is
+spliced after \fISTREAK\fR reads in a row of \fIMIN\fR bytes or more (\fISMALL\fR from a
+TLS socket, which hands out a record at a time) and copied again after more
+than \fISTREAK\fR splices in a row of less than \fISMALL\fR; a body of known length is
+spliced if at least \fIMIN\fR of it is left to read. splice() suits large
+transfers: they go faster and use less CPU. A spliced direction needs a pipe
+(two file descriptors), made when first needed and kept for the connection,
+and more kernel memory. A service with data filters does not splice, so that
+the filters see all of the data, nor does one ending or beginning TLS, unless
+the kernel does TLS for it both ways (\fBssl_ktls\fR). 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
diff --git a/src/proxy.h b/src/proxy.h
index bcfd602..710604a 100644
--- a/src/proxy.h
+++ b/src/proxy.h
@@ -235,6 +235,11 @@ extern struct extparam confboot;
#define SPLICE_DOWNLOAD 1
#define SPLICE_UPLOAD 2
#define SPLICE_AUTO 4
+/* where SPLICE_AUTO splices, by default (-s[N]:MIN,STREAK,SMALL): see
+ sockmap.c */
+#define SPLICE_MIN 16384
+#define SPLICE_STREAK 2
+#define SPLICE_SMALL 4096
int sockmap(struct clientparam * param, int timeo, int usesplice);
int udpsockmap(struct clientparam * param, int timeo);
int udpbind(struct clientparam * param);
diff --git a/src/proxymain.c b/src/proxymain.c
index 0251dcf..1db0220 100644
--- a/src/proxymain.c
+++ b/src/proxymain.c
@@ -436,6 +436,10 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){
" -s[N] Use splice(): -s1 server to client, -s2 client to server, -s3 both\n"
" directions; -s (-s7, the default) both, -s5 or -s6 one, only where it\n"
" pays (large transfers); -s0 never. Not with data filters\n"
+ " :MIN,STREAK,SMALL tune where it pays (-s:16384,2,4096): splice after\n"
+ " STREAK reads in a row of MIN bytes or more (SMALL from a TLS socket),\n"
+ " copy again after more than STREAK splices of less than SMALL; a body of\n"
+ " known length is spliced if MIN or more of it is left\n"
" -C connection closed by any of the sides terminates the session, by default\n"
" the session is kept until both sides close it (TCP half-close)\n"
" -U(0-3) (for socks) what to do when the destination changes within an UDP\n"
@@ -716,10 +720,22 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){
#endif
srv->s_option = 1 + atoi(argv[i]+2);
#ifdef WITHSPLICE
- else
- srv->usesplice = (*(argv[i]+2)? atoi(argv[i]+2) :
+ else {
+ /* -s[N][:MIN,STREAK,SMALL] */
+ char *heur = strchr(argv[i]+2, ':');
+
+ srv->usesplice = ((argv[i][2] && argv[i][2] != ':')? atoi(argv[i]+2) :
SPLICE_DOWNLOAD|SPLICE_UPLOAD|SPLICE_AUTO) &
(SPLICE_DOWNLOAD|SPLICE_UPLOAD|SPLICE_AUTO);
+ if(heur){
+ int min = 0, streak = 0, small = 0;
+
+ sscanf(heur+1, "%d,%d,%d", &min, &streak, &small);
+ if(min > 0) srv->splicemin = min;
+ if(streak > 0) srv->splicestreak = streak;
+ if(small > 0) srv->splicesmall = small;
+ }
+ }
#endif
break;
case 'o':
@@ -1498,6 +1514,9 @@ void srvinit(struct srvparam * srv, struct clientparam *param){
#ifdef WITHSPLICE
/* where it pays: large transfers, -s0 to never */
srv->usesplice = SPLICE_DOWNLOAD|SPLICE_UPLOAD|SPLICE_AUTO;
+ srv->splicemin = SPLICE_MIN;
+ srv->splicestreak = SPLICE_STREAK;
+ srv->splicesmall = SPLICE_SMALL;
#endif
srv->halfclose = 1;
srv->udpauth = 3;
diff --git a/src/sockmap.c b/src/sockmap.c
index 390ecf3..c5e297c 100644
--- a/src/sockmap.c
+++ b/src/sockmap.c
@@ -44,18 +44,16 @@ ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t le
/* With SPLICE_AUTO, when a direction allowed to splice actually does
(without it, an allowed direction always does).
- A body of known length is spliced if at least SPLICE_MIN of it is left
+ A body of known length is spliced if at least splicemin of it is left
to read once what is already buffered is counted: a smaller one, or one
read whole with its headers, is cheaper to copy.
A stream of unknown length starts copied and is spliced after
- SPLICE_STREAK reads in a row of at least SPLICE_MIN each, and goes back to
- copying after SPLICE_STREAK+1 splices in a row of less than SPLICE_SMALL. */
-#define SPLICE_MIN 16384
-#define SPLICE_SMALL 4096
-#define SPLICE_STREAK 2
-/* a large read from a TLS socket: TLS hands out a record at a time, at most
- 16K and often less */
-#define SPLICE_TLSMIN SPLICE_SMALL
+ splicestreak reads in a row of at least splicemin each, and goes back to
+ copying after splicestreak+1 splices in a row of less than splicesmall.
+ From a TLS socket a read of splicesmall counts as large: TLS hands out a
+ record at a time, at most 16K and often less. The service sets the three
+ (-s[N]:MIN,STREAK,SMALL), SPLICE_MIN, SPLICE_STREAK and SPLICE_SMALL by
+ default. */
#endif
@@ -149,6 +147,8 @@ int sockmap(struct clientparam * param, int timeo, int usesplice){
into the buffer first (drain), once the pipe is empty, so as to keep the
order; tail: the last read left some, the next only collects it */
int draincli = 0, drainsrv = 0, tailcli = 0, tailsrv = 0;
+ int splicemin = param->srv->splicemin, splicestreak = param->srv->splicestreak,
+ splicesmall = param->srv->splicesmall;
if((!param->nolongdatfilter && (param->ndatfilterscli > 0 || param->ndatfilterssrv)) || param->nosplice) usesplice = 0;
allowcli = (usesplice & SPLICE_UPLOAD) != 0;
@@ -190,8 +190,8 @@ int sockmap(struct clientparam * param, int timeo, int usesplice){
if(inclientbuf >= fromclient) FROMCLIENT = 0;
#ifdef WITHSPLICE
/* a body of known length large enough is spliced from the start */
- if(autosplice && allowsrv && param->waitserver64 && fromserver > inserverbuf && fromserver - inserverbuf >= SPLICE_MIN) splicesrv = 1;
- if(autosplice && allowcli && param->waitclient64 && fromclient > inclientbuf && fromclient - inclientbuf >= SPLICE_MIN) splicecli = 1;
+ if(autosplice && allowsrv && param->waitserver64 && fromserver > inserverbuf && fromserver - inserverbuf >= (uint64_t)splicemin) splicesrv = 1;
+ if(autosplice && allowcli && param->waitclient64 && fromclient > inclientbuf && fromclient - inclientbuf >= (uint64_t)splicemin) splicecli = 1;
#endif
if(param->srvinbuf == param->srvoffset) param->srvinbuf =param->srvoffset = 0;
if(param->cliinbuf == param->clioffset) param->cliinbuf =param->clioffset = 0;
@@ -504,8 +504,8 @@ log("done read from client to pipe");
graceclitraf += res;
gracesrvnum = gracesrvtraf = 0;
if(autosplice && !param->waitclient64){
- if(res >= SPLICE_SMALL) streakcli = 0;
- else if(++streakcli > SPLICE_STREAK) wantbufcli = 1;
+ if(res >= splicesmall) streakcli = 0;
+ else if(++streakcli > splicestreak) wantbufcli = 1;
}
inclientpipe += res;
if(inclientpipe >= MAXSPLICE) TOCLIENTPIPE = 0;
@@ -567,8 +567,8 @@ log("done read from server to pipe\n");
gracesrvtraf += res;
graceclinum = graceclitraf = 0;
if(autosplice && !param->waitserver64){
- if(res >= SPLICE_SMALL) streaksrv = 0;
- else if(++streaksrv > SPLICE_STREAK) wantbufsrv = 1;
+ if(res >= splicesmall) streaksrv = 0;
+ else if(++streaksrv > splicestreak) wantbufsrv = 1;
}
param->nreads++;
param->statssrv64 += res;
@@ -620,8 +620,8 @@ log("done read from client to buf");
else if(autosplice && allowcli && !param->waitclient64){
int tls = splicetls? splicetls(param, param->clisock) : 0;
- if(res >= (tls? SPLICE_TLSMIN : SPLICE_MIN)){
- if(++streakcli >= SPLICE_STREAK){
+ if(res >= (tls? splicesmall : splicemin)){
+ if(++streakcli >= splicestreak){
/* a stream: splice from now on, after what is
buffered has been sent */
splicecli = TOCLIENTPIPE = FROMCLIENTPIPE = 1;
@@ -675,8 +675,8 @@ log("done read from server to buf");
else if(autosplice && allowsrv && !param->waitserver64){
int tls = splicetls? splicetls(param, param->remsock) : 0;
- if(res >= (tls? SPLICE_TLSMIN : SPLICE_MIN)){
- if(++streaksrv >= SPLICE_STREAK){
+ if(res >= (tls? splicesmall : splicemin)){
+ if(++streaksrv >= splicestreak){
splicesrv = TOSERVERPIPE = FROMSERVERPIPE = 1;
streaksrv = 0;
}
diff --git a/src/structures.h b/src/structures.h
index 6d1f2d6..17e82b0 100644
--- a/src/structures.h
+++ b/src/structures.h
@@ -653,6 +653,8 @@ struct srvparam {
unsigned char fakeip6[16];
#ifdef WITHSPLICE
int usesplice;
+ /* the automatic mode's heuristics, -s[N]:MIN,STREAK,SMALL */
+ int splicemin, splicestreak, splicesmall;
#endif
int halfclose;
int udpauth;
diff --git a/tests/cases/splice.py b/tests/cases/splice.py
new file mode 100644
index 0000000..2f90e3b
--- /dev/null
+++ b/tests/cases/splice.py
@@ -0,0 +1,121 @@
+"""splice(): every mode relays what it is given unchanged, small and large,
+both ways, the automatic mode's heuristics set or not (-s[N]:MIN,STREAK,SMALL).
+
+Where splice() is not built (anything but Linux) -s is accepted and ignored,
+so the same checks then cover copying.
+"""
+
+import os
+import socket
+import threading
+
+MODES = ["-s0", "-s", "-s3", "-s:65536,3,8192", "-s2:32768,2,4096"]
+
+
+def echo_origin(listener):
+ """Sends back whatever each connection sends, until it shuts down its
+ side."""
+ def serve(c):
+ try:
+ while True:
+ b = c.recv(65536)
+ if not b:
+ break
+ c.sendall(b)
+ c.shutdown(socket.SHUT_WR)
+ except OSError:
+ pass
+ c.close()
+
+ while True:
+ try:
+ c, _ = listener.accept()
+ except OSError:
+ return
+ threading.Thread(target=serve, args=(c,), daemon=True).start()
+
+
+def echo_through(port, data):
+ """data sent through the mapped port while what comes back is read; the
+ sending side is shut down once all is sent."""
+ s = socket.create_connection(("127.0.0.1", port), timeout=20)
+ got = []
+
+ def reader():
+ while True:
+ try:
+ b = s.recv(65536)
+ except OSError:
+ return
+ if not b:
+ return
+ got.append(b)
+
+ th = threading.Thread(target=reader, daemon=True)
+ th.start()
+ s.sendall(data)
+ s.shutdown(socket.SHUT_WR)
+ th.join(30)
+ s.close()
+ return b"".join(got)
+
+
+def run(t):
+ origin = t.free_port()
+ listener = socket.socket()
+ listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ listener.bind(("127.0.0.1", 0))
+ listener.listen(64)
+ echo = listener.getsockname()[1]
+ threading.Thread(target=echo_origin, args=(listener,), daemon=True).start()
+
+ proxies, mapped = [], []
+ conf = f"""
+ log
+ auth iponly
+ allow *
+ http data * /data
+ httpsrv -p{origin}
+
+ flush
+ auth iponly
+ allow *
+"""
+ for mode in MODES:
+ p, m = t.free_port(), t.free_port()
+ proxies.append(p)
+ mapped.append(m)
+ conf += f" proxy {mode} -p{p}\n tcppm {mode} {m} 127.0.0.1 {echo}\n"
+ t.start("splice", conf, ports=[origin] + proxies + mapped)
+
+ direct = {size: t.http(f"http://127.0.0.1:{origin}/data?size={size}").body
+ for size in (1000, 100000, 5000000)}
+ chunked = t.http(f"http://127.0.0.1:{origin}/data?size=1000000&chunked=1").body
+ up = os.urandom(3000000)
+
+ try:
+ for mode, p, m in zip(MODES, proxies, mapped):
+ proxy = f"127.0.0.1:{p}"
+ for size, body in direct.items():
+ got = t.http(f"http://127.0.0.1:{origin}/data?size={size}", proxy=proxy)
+ if got.body == body:
+ t.ok(f"{mode}: a {size} byte answer arrives unchanged")
+ else:
+ t.fail(f"{mode}: a {size} byte answer arrives unchanged",
+ f"{size} bytes", f"{got.length} different bytes")
+ got = t.http(f"http://127.0.0.1:{origin}/data?size=1000000&chunked=1", proxy=proxy)
+ if got.body == chunked:
+ t.ok(f"{mode}: a chunked answer arrives unchanged")
+ else:
+ t.fail(f"{mode}: a chunked answer arrives unchanged",
+ f"{len(chunked)} bytes", f"{got.length} different bytes")
+ back = echo_through(m, up)
+ if back == up:
+ t.ok(f"{mode}: 3 MB sent through tcppm come back unchanged")
+ else:
+ t.fail(f"{mode}: 3 MB sent through tcppm come back unchanged",
+ f"{len(up)} bytes", f"{len(back)} bytes, different")
+ small = echo_through(m, b"hello")
+ t.eq(b"hello", small, f"{mode}: a few bytes through tcppm come back")
+ finally:
+ listener.close()