splice() over kernel TLS

A service with ssl_ktls keeps its splice() mode. A connection whose TLS
sockets all got kernel TLS both ways may splice, as a plain one does when
no data filters apply; one where the kernel did not take TLS (nosplice)
copies as before.

TLS may hold data for a socket that a splice() from it would not see:
OpenSSL reads more from a kernel TLS socket than it is asked for. sockmap
asks the TLS code (splicetls) and reads that through TLS into the buffer
first, once the pipe has drained, so the order is kept, then splices on.
A read from a TLS socket counts as large at 4K, TLS handing out a record
at a time, and the short read collecting what TLS left from the last one
does not end a run of large ones. A splice() refused because a record is
not data (an alert, a key update, a ticket) falls back to reading it
through TLS instead of ending the connection.

The kernel TLS test compares the bodies with the origin's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vladimir Dubrovin 2026-10-14 18:50:02 +03:00
parent 44e372649a
commit cfe52878d0
5 changed files with 124 additions and 22 deletions

View File

@ -488,7 +488,13 @@ extern struct datatype datatypes[64];
extern struct commands commandhandlers[];
#ifdef WITHSPLICE
#define mapsocket(a,b) ((a->srv->usesplice && !a->ndatfilterssrv && !a->ndatfilterscli)?sockmap(a,b,a->srv->usesplice):sockmap(a,b,0))
/* set by the TLS code where it splices over kernel TLS: 0 for a socket TLS
does not handle, SPLICE_TLS for one it does, SPLICE_TLSPENDING for one
whose data it has read and not handed out, which splice() would not see */
#define SPLICE_TLS 1
#define SPLICE_TLSPENDING 2
extern int (*splicetls)(struct clientparam *param, SOCKET s);
#define mapsocket(a,b) ((a->srv->usesplice && !a->nosplice && !a->ndatfilterssrv && !a->ndatfilterscli)?sockmap(a,b,a->srv->usesplice):sockmap(a,b,0))
#else
#define mapsocket(a,b) sockmap(a,b, 0)
#endif

View File

@ -38,6 +38,9 @@ ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t le
#define MAXSPLICE 65536
/* splice() refuses a kernel TLS record that is not data: EINVAL, or EIO
and EBADMSG on some kernels */
#define SPLICEREFUSED(e) ((e) == EINVAL || (e) == EIO || (e) == EBADMSG)
/* With SPLICE_AUTO, when a direction allowed to splice actually does
(without it, an allowed direction always does).
@ -50,6 +53,9 @@ ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t le
#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
#endif
@ -96,6 +102,10 @@ static int hupwrite(SOCKET s, short revents, int timeo){
/* usesplice is a mask of the directions passed with splice() rather than
through a buffer: SPLICE_DOWNLOAD, server to client, and SPLICE_UPLOAD,
client to server. */
#ifdef WITHSPLICE
int (*splicetls)(struct clientparam *param, SOCKET s) = NULL;
#endif
int sockmap(struct clientparam * param, int timeo, int usesplice){
int halfclose = param->srv->halfclose;
uint64_t fromclient=0x7fffffffffffffff, fromserver =0x7fffffffffffffff;
@ -135,8 +145,12 @@ int sockmap(struct clientparam * param, int timeo, int usesplice){
towards switching; wantbuf: go back to copying once the pipe is empty */
int allowcli = 0, allowsrv = 0, splicecli = 0, splicesrv = 0, autosplice;
int streakcli = 0, streaksrv = 0, wantbufcli = 0, wantbufsrv = 0;
/* TLS holds data for the side that a splice() would not see: it is read
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;
if((!param->nolongdatfilter && (param->ndatfilterscli > 0 || param->ndatfilterssrv))) usesplice = 0;
if((!param->nolongdatfilter && (param->ndatfilterscli > 0 || param->ndatfilterssrv)) || param->nosplice) usesplice = 0;
allowcli = (usesplice & SPLICE_UPLOAD) != 0;
allowsrv = (usesplice & SPLICE_DOWNLOAD) != 0;
autosplice = (usesplice & SPLICE_AUTO) != 0;
@ -151,6 +165,8 @@ int sockmap(struct clientparam * param, int timeo, int usesplice){
#define splicesrv 0
#define wantbufcli 0
#define wantbufsrv 0
#define draincli 0
#define drainsrv 0
#endif
inserverbuf = param->srvinbuf - param->srvoffset;
@ -439,6 +455,16 @@ log("read from client to pipe");
if(param->cliinbuf < CLIROOM) TOCLIENTBUF = 1;
continue;
}
/* what TLS has read and not handed out yet is read through it
first, behind what the pipe holds */
if(splicetls && splicetls(param, param->clisock) == SPLICE_TLSPENDING){
TOCLIENTPIPE = 0;
if(!inclientpipe){
draincli = 1;
if(param->cliinbuf < CLIROOM) TOCLIENTBUF = 1;
}
continue;
}
errno = 0;
res = splice(param->clisock, NULL, pipecli[1], NULL, (int)MIN((uint64_t)MAXSPLICE - inclientpipe, (uint64_t)fromclient-inclientpipe), SPLICE_F_NONBLOCK|SPLICE_F_MOVE);
#ifdef WITHLOG
@ -456,6 +482,14 @@ log(logbuf);
CLIENTTERMREAD = 1;
continue;
}
if(SPLICEREFUSED(errno)) {
/* a record kernel TLS does not splice (an alert, a key
update, a ticket): read it through TLS */
if(autosplice) wantbufcli = 1;
else allowcli = splicecli = FROMCLIENTPIPE = 0;
FROMCLIENT = 1;
continue;
}
if(errno && errno != EINTR && errno != EAGAIN) {
CLIENTTERMREAD = 1;
CLIENTTERMWRITE = 1;
@ -488,6 +522,14 @@ log("read from server to pipe\n");
if(param->srvinbuf < SRVROOM) TOSERVERBUF = 1;
continue;
}
if(splicetls && splicetls(param, param->remsock) == SPLICE_TLSPENDING){
TOSERVERPIPE = 0;
if(!inserverpipe){
drainsrv = 1;
if(param->srvinbuf < SRVROOM) TOSERVERBUF = 1;
}
continue;
}
errno = 0;
res = splice(param->remsock, NULL, pipesrv[1], NULL, MIN(MAXSPLICE - inserverpipe, fromserver - inserverpipe), SPLICE_F_NONBLOCK|SPLICE_F_MOVE);
#ifdef WITHLOG
@ -505,6 +547,12 @@ log(logbuf);
SERVERTERMREAD = 1;
continue;
}
if(SPLICEREFUSED(errno)) {
if(autosplice) wantbufsrv = 1;
else allowsrv = splicesrv = FROMSERVERPIPE = 0;
FROMSERVER = 1;
continue;
}
if(errno && errno != EINTR && errno != EAGAIN) {
SERVERTERMREAD = 1;
SERVERTERMWRITE = 1;
@ -538,7 +586,7 @@ log("done read from server to pipe\n");
}
#endif
{
if(!splicecli && fromclient > inclientbuf && FROMCLIENT && TOCLIENTBUF && !SESSIONEND){
if((!splicecli || draincli) && fromclient > inclientbuf && FROMCLIENT && TOCLIENTBUF && !SESSIONEND){
#ifdef WITHLOG
log("read from client to buf");
#endif
@ -564,14 +612,26 @@ log("done read from client to buf");
graceclitraf += res;
gracesrvnum = gracesrvtraf = 0;
#ifdef WITHSPLICE
if(autosplice && allowcli && !param->waitclient64){
if(res < SPLICE_MIN) streakcli = 0;
else if(++streakcli >= SPLICE_STREAK){
/* a stream: splice from now on, after what is
buffered has been sent */
splicecli = TOCLIENTPIPE = FROMCLIENTPIPE = 1;
streakcli = 0;
if(draincli){
/* what TLS held is in the buffer: splice again */
draincli = 0;
TOCLIENTPIPE = 1;
}
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){
/* a stream: splice from now on, after what is
buffered has been sent */
splicecli = TOCLIENTPIPE = FROMCLIENTPIPE = 1;
streakcli = 0;
}
}
/* a short read only collecting what TLS had left from
the last one does not end a run of large ones */
else if(!tailcli) streakcli = 0;
tailcli = (tls == SPLICE_TLSPENDING);
}
#endif
inclientbuf += res;
@ -582,7 +642,7 @@ log("done read from client to buf");
}
}
if(!splicesrv && fromserver > inserverbuf && FROMSERVER && TOSERVERBUF && !SESSIONEND){
if((!splicesrv || drainsrv) && fromserver > inserverbuf && FROMSERVER && TOSERVERBUF && !SESSIONEND){
#ifdef WITHLOG
log("read from server to buf");
#endif
@ -608,12 +668,21 @@ log("done read from server to buf");
gracesrvtraf += res;
graceclinum = graceclitraf = 0;
#ifdef WITHSPLICE
if(autosplice && allowsrv && !param->waitserver64){
if(res < SPLICE_MIN) streaksrv = 0;
else if(++streaksrv >= SPLICE_STREAK){
splicesrv = TOSERVERPIPE = FROMSERVERPIPE = 1;
streaksrv = 0;
if(drainsrv){
drainsrv = 0;
TOSERVERPIPE = 1;
}
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){
splicesrv = TOSERVERPIPE = FROMSERVERPIPE = 1;
streaksrv = 0;
}
}
else if(!tailsrv) streaksrv = 0;
tailsrv = (tls == SPLICE_TLSPENDING);
}
#endif
param->nreads++;

View File

@ -417,9 +417,22 @@ static void ssl_after_handshake(struct clientparam* param, SSL_CONN conn){
}
#endif
if(!krecv) SSL_set_read_ahead(ssl, SSL_READAHEAD);
(void)ksend;
/* splice() passes the data of a TLS socket only where the kernel does TLS
for it both ways */
if(!ksend || !krecv) param->nosplice = 1;
}
#ifdef WITHSPLICE
/* Whether TLS handles the socket, and whether it has read data for it that
it has not handed out, which a splice() from the socket would not see. */
static int ssl_splicetls(struct clientparam *param, SOCKET s){
struct SSLsock *sslq = searchSSL(param->sostate, s);
if(!sslq) return 0;
return ssl_pending(sslq->conn)? SPLICE_TLSPENDING : SPLICE_TLS;
}
#endif
SSL_CONN dosrvcon(struct clientparam* param, SSL_CERT* cert){
SSL_CONN ServerConn;
char *errSSL=NULL;
@ -779,7 +792,13 @@ static void* ssl_filter_open(void * idata, struct srvparam * srv){
#endif
}
#ifdef WITHSPLICE
srv->usesplice = 0;
/* the data of a TLS connection passes through 3proxy, unless the kernel
does TLS for it: then each connection which got it both ways may
splice() */
#ifdef SSL_OP_ENABLE_KTLS
if(!sc || !sc->ktls)
#endif
srv->usesplice = 0;
#endif
return sc;
}
@ -1291,6 +1310,9 @@ void ssl_install(void){
pl->symbols.next = ssl_symbols;
}
#ifdef WITHSPLICE
splicetls = ssl_splicetls;
#endif
tcppmfunc = (PROXYFUNC)pl->findbyname("tcppm");
if(!tcppmfunc) return;
proxyfunc = (PROXYFUNC)pl->findbyname("proxy");

View File

@ -740,6 +740,7 @@ struct clientparam {
weight,
nolog,
nolongdatfilter,
nosplice, /* a TLS socket of the connection is not in the kernel */
nooverwritefilter,
transparent,
chunked,

View File

@ -60,11 +60,15 @@ def run(t):
t.eq(200, r.status, "a request through the TLS chain with ssl_ktls arrives")
t.contains(r, "path=/echo", "the origin sees it")
# many records, both ways through both TLS legs
# many records, both ways through both TLS legs; where the kernel took
# TLS a large body is spliced, so what arrives is compared with the origin
for size in (100000, 3000000):
t.eq(size, t.http(f"http://127.0.0.1:{origin}/data?size={size}",
proxy=through).length,
f"a {size} byte body survives the kernel TLS chain")
url = f"http://127.0.0.1:{origin}/data?size={size}"
got = t.http(url, proxy=through)
t.eq(size, got.length, f"a {size} byte body survives the kernel TLS chain")
t.ok(f"the {size} byte body arrives unchanged") if got.body == t.http(url).body \
else t.fail(f"the {size} byte body arrives unchanged", "the origin's bytes",
"different bytes")
t.eq(200000, t.http(f"http://127.0.0.1:{origin}/data?size=200000&chunked=1",
proxy=through).length,
"a chunked body survives the kernel TLS chain")