Compare commits

...

4 Commits

Author SHA1 Message Date
Vladimir Dubrovin
359ce5f8b4 -B: the TLS ClientHello split at the SNI, which -s is taken for splice
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled
tlspr splits the ClientHello it passes in the middle of the server name
(SNI) when the service has s_option, set by -s. In builds with splice()
(Linux, the default there) -s is splice for every service but UDP ones,
admin and httpsrv, so the split was never made, and the How-To's SNI
break did nothing on Linux.

-B sets it whatever the build: for TCP services but admin and httpsrv,
where s_option has a meaning of its own. -s still does it where it is
not splice. The How-To, 3proxy.cfg(5) and tlspr(8) give -B.

Fixes #1283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:52:39 +03:00
Vladimir Dubrovin
65213b172d Outgoing connections bound with IP_BIND_ADDRESS_NO_PORT
The socket of a connection to the destination is bound to the external
address before connect(), with port 0 unless a range is given. bind()
then takes an ephemeral port for the socket alone: not one in use
towards another destination, nor one of a connection still closing in
TIME_WAIT, which connect() would reuse. With many connections the range
runs out and bind() fails, result 12, or searches long for a port: 7 ms
a call was seen. The more connections are open at once, the sooner:
with event loops, serving more of them at once, the client of every
tenth connection or so of a saturated relay got its end at once.

Where the system has IP_BIND_ADDRESS_NO_PORT (Linux 4.2), the port is
left to connect() for a socket that is going to connect: chosen for
the pair with the destination, together with IP_LOCAL_PORT_RANGE when
a range is given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:34:14 +03:00
Vladimir Dubrovin
237a4499fa ftppr: the data socket towards the client from the socket functions
It was made with socket(): in the main network namespace rather than
the -ni one the client is reached in, and past the socket functions of
the service, which the rest of its life went through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 7718ffa9f7)
2026-10-24 20:55:40 +03:00
Vladimir Dubrovin
2d06b4672a Connect back: a connection one client takes leaves the others waiting
The clients of a -R service all wait for the connection back on the
same listening socket, which was blocking: one connection woke them
all, one took it, and the others were left in accept() until the next,
past their time out and through a reload. The socket is non-blocking,
and an accept() which finds nothing, the connection taken by another
client or gone, is not one of the client's tries. nonblock() makes a
socket non-blocking, acceptagain() tells such an accept().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 126c7d00ec)
2026-10-24 20:55:40 +03:00
10 changed files with 66 additions and 9 deletions

View File

@ -648,16 +648,16 @@ see <a href="#DNSPR">How to set up a DNS proxy (dnspr)</a>.
<br>tlspr can be used as a parent with the "tls" type to implement SNI splitting for DPI bypass (similar to NoDPI/GoodByeDPI).
The client sends the first part of the TLS ClientHello, tlspr splits it at the SNI extension and sends it in two TCP packets,
which can bypass some DPI systems that look for blocked hostnames in TLS handshakes.
<br>To enable SNI break, use <code>parent ... tls 0.0.0.0 0</code> and the <code>-s</code> option on the listening service with TCP_NODELAY:
<br>To enable SNI break, use <code>parent ... tls 0.0.0.0 0</code> and the <code>-B</code> option on the listening service:
</p><pre>
auth iponly
allow *
parent 1000 tls 0.0.0.0 0
allow *
proxy -s -i127.0.0.1 -ocTCP_NODELAY -osTCP_NODELAY -p1443
proxy -B -i127.0.0.1 -p1443
</pre>
<p>
TCP_NODELAY is required to prevent the kernel from merging the split packets.
TCP_NODELAY, set by default, is required to prevent the kernel from merging the split packets: do not turn it off with -ocTCP_NODELAY=0 or -osTCP_NODELAY=0.
</p>
<p>
<b>Configuration examples:</b>

View File

@ -656,16 +656,16 @@ iptables -t nat -A PREROUTING -p tcp -m multiport --dports 443,993,465 -j REDIRE
<br>tlspr может использоваться как родительский прокси типа "tls" для реализации SNI-фрагментации (аналог NoDPI/GoodByeDPI).
Клиент отправляет первую часть TLS ClientHello, tlspr разбивает его на расширении SNI и отправляет двумя TCP-пакетами,
что позволяет обойти некоторые DPI-системы, ищущие заблокированные имена хостов в TLS-рукопожатиях.
<br>Для включения SNI break используйте <code>parent ... tls 0.0.0.0 0</code> и опцию <code>-s</code> на слушающем сервисе с TCP_NODELAY:
<br>Для включения SNI break используйте <code>parent ... tls 0.0.0.0 0</code> и опцию <code>-B</code> на слушающем сервисе:
</p><pre>
auth iponly
allow *
parent 1000 tls 0.0.0.0 0
allow *
proxy -s -i127.0.0.1 -ocTCP_NODELAY -osTCP_NODELAY -p1443
proxy -B -i127.0.0.1 -p1443
</pre>
<p>
TCP_NODELAY необходим, чтобы ядро не объединяло разделенные пакеты.
TCP_NODELAY, установленный по умолчанию, необходим, чтобы ядро не объединяло разделенные пакеты: не отключайте его опциями -ocTCP_NODELAY=0 или -osTCP_NODELAY=0.
</p>
<p>
<b>Примеры конфигурации:</b>

View File

@ -198,6 +198,13 @@ any protocol-specific data.
.B -g(\fIGRACE_TRAFF\fB,\fIGRACE_NUM\fB,\fIGRACE_DELAY\fR)
delay GRACE_DELAY milliseconds before polling if average polling size is below GRACE_TRAFF bytes and GRACE_NUM read operations in a single direction are detected within 1 second. Useful to minimize polling
.br
.B -B
(for TCP services) split the TLS ClientHello across two TCP segments in the
middle of the server name (SNI) when it is passed through tlspr, as with
\fBparent ... tls 0.0.0.0 0\fR: some DPI systems looking for blocked host names
do not reassemble it. TCP_NODELAY, set by default, must not be turned off, or the
kernel may join the segments again. In builds without splice() \fB-s\fR does the same.
.br
.B -s
(for admin) secure, allow only secure operations, currently only traffic counters
view without ability to reset.

View File

@ -76,8 +76,8 @@ destination_port. Port to establish outgoing connections. Required unless the Tr
.B -c
TLS_CHECK_LEVEL. 0 (default) - allow non-TLS traffic to pass, 1 - require TLS, only check client HELLO packet, 2 - require TLS, check both client and server HELLO, 3 - require TLS, check that the server sends a certificate (not compatible with TLS 1.3), 4 - require mutual TLS, check that the server sends a certificate request and the client sends a certificate (not compatible with TLS 1.3)
.TP
.B -s
Split the TLS Client HELLO packet across multiple TCP segments to make SNI-based DPI detection harder. An optional numeric value can follow (e.g. \fB-s1\fR) to control the splitting behaviour.
.B -B
Split the TLS Client HELLO packet across two TCP segments, in the middle of the SNI, to make SNI-based DPI detection harder. In builds without splice() \fB-s\fR does the same; with splice() (Linux) \fB-s\fR is splice, see \fB3proxy.cfg\fR(5).
.TP
.B -Ximap | -Xpop3 | -Xsmtp
STARTTLS mode for the given protocol. The proxy speaks the plaintext protocol

View File

@ -649,6 +649,26 @@ int parseconnusername(char *username, struct clientparam *param, int extpasswd,
}
/* What waits for a socket waits in sockpoll(): a call on it never does. */
void nonblock(SOCKET sock){
#ifdef _WIN32
unsigned long ul = 1;
ioctlsocket(sock, FIONBIO, &ul);
#else
fcntl(sock, F_SETFL, O_NONBLOCK | fcntl(sock, F_GETFL));
#endif
}
/* accept() with nothing to accept: another took it, or it was gone */
int acceptagain(void){
#ifdef _WIN32
int err = WSAGetLastError();
return err == WSAEWOULDBLOCK || err == WSAECONNRESET || err == WSAEINTR;
#else
return errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED || errno == EINTR;
#endif
}
int connectwithpoll(struct clientparam *param, SOCKET sock, struct sockaddr *sa, SASIZETYPE size, int to){
struct pollfd fds[1];
#ifdef _WIN32
@ -775,6 +795,22 @@ int doconnect(struct clientparam * param){
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 IP_BIND_ADDRESS_NO_PORT
/* A port the kernel chooses is chosen by connect(), for the pair with
the destination: one in use towards another destination, or closing
from an earlier connection, does as well. bind() would take one of
its own, and with many connections find none left, or search long
for it. */
if((param->operation >= 256 || (param->operation & CONNECT) || param->redirected)
# ifdef WITH_UN
&& *SAFAMILY(&param->sinsl) != AF_UNIX
# endif
){
int one = 1;
param->srv->so._setsockopt(param->sostate, param->remsock, IPPROTO_IP, IP_BIND_ADDRESS_NO_PORT, (char *)&one, sizeof(one));
}
#endif
#ifdef WITH_UN
if(*SAFAMILY(&param->sinsl) != AF_UNIX)
#endif

View File

@ -132,7 +132,7 @@ void * ftpprchild(struct clientparam* param) {
param->srv->so._closesocket(param->sostate, clidatasock);
clidatasock = INVALID_SOCKET;
}
if ((clidatasock=socket(SASOCK(&param->sincl), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN(821);}
if ((clidatasock=nssocket(param->srv, &param->srv->so, param->sostate, NS_IN, SASOCK(&param->sincl), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN(821);}
if(intbindtodevice(param, clidatasock)){RETURN(822);}
*SAPORT(&param->sincl) = 0;
if(bindwithrange(param, clidatasock, &param->sincl, param->intport)){RETURN(822);}

View File

@ -54,6 +54,8 @@ static void serveparam(struct clientparam *param){
}
param->remsock = param->srv->so._accept(param->sostate, param->srv->cbsock, (struct sockaddr*)&param->sinsr, &size);
if(param->remsock == INVALID_SOCKET) {
/* another client of the service took it: not a try */
if(acceptagain()) { i++; continue; }
dolog(param, (unsigned char *)"Connect back accept() failed");
continue;
}

View File

@ -221,6 +221,7 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){
" association: 1 - log, 2 - authorize, 3 - both (default), 0 (same as -U) -\n"
" neither. The first destination is always authorized and logged\n"
#endif
" -B split the TLS ClientHello passed through tlspr at the SNI (DPI bypass)\n"
"-g(GRACE_TRAFF,GRACE_NUM,GRACE_DELAY) - delay GRACE_DELAY milliseconds before polling if average polling size below GRACE_TRAFF bytes and GRACE_NUM read operations in single directions are detected within 1 second to minimize polling\n"
" -fFORMAT logging format (see documentation)\n"
" -l log to stdout\n"
@ -486,6 +487,13 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){
case 'U':
srv->udpauth = *(argv[i]+2)? atoi(argv[i]+2) : 0;
break;
case 'B':
/* the TLS ClientHello split at the SNI (tlspr), which -s is
where -s is not splice; s_option means something else
for these */
if(!s->isudp && srv->service != S_ADMIN && srv->service != S_HTTPSRV)
srv->s_option = 1 + atoi(argv[i]+2);
break;
case 's':
#ifdef WITHSPLICE
/* admin is an httpsrv, where -s is the limited mode */

View File

@ -23,6 +23,8 @@ static int cblisten(struct srvstart *s){
return -6;
}
setopts(srv->cbsock, &srv->cbssockopts, srv->maxseg);
/* the clients of the service all wait for it: one accepts what comes */
nonblock(srv->cbsock);
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");

View File

@ -332,6 +332,8 @@ int nsbroken(void);
/* a function everywhere, never a macro: its callers have preprocessor
lines among its arguments */
SOCKET nssocket(struct srvparam *srv, struct sockfuncs *sf, void *state, int side, int domain, int type, int protocol);
void nonblock(SOCKET sock);
int acceptagain(void);
int socksend(struct clientparam *param, SOCKET sock, unsigned char * buf, int bufsize, int to);
int socksendto(struct clientparam *param, SOCKET sock, struct sockaddr * sin, unsigned char * buf, int bufsize, int to);
int sockrecvfrom(struct clientparam *param, SOCKET sock, struct sockaddr * sin, unsigned char * buf, int bufsize, int to);