Compare commits

..

15 Commits

Author SHA1 Message Date
Vladimir Dubrovin
14e87dbd78 pcre: a match data of the connection's own, not of the rule's
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
Every connection matching a pcre rule matched on the one match data
block of the rule, which pcre2_match() writes into: two connections
matching at the same time wrote the same block, a race that corrupts
memory and brings the process down after a while under load. A
connection that passes the rule's ACL now makes a match data block of
its own when it starts and frees it when it ends; the match context is
kept, as it is only read while matching.
2026-11-02 12:22:20 +03:00
Vladimir Dubrovin
629a978c00 help: the features of the build listed, and the tests skip what they need
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
3proxy --help (and the usage message of a bad invocation) now lists
what the build was made with: TLS and its backend, RADIUS, PCRE, IPv6,
UNIX sockets, the built-in HTTP server, transparency, ODBC and the
optional proxy services. The test runner reads the list before the
first case and the cases state the features they need with
t.skip_unless(), so a build without a feature skips its cases instead
of failing them; a binary too old to report anything skips the cases
with needs and runs the rest.
2026-10-30 14:24:46 +03:00
Vladimir Dubrovin
a75b488aaf radius: the reply hashed with the secret, and taken only from the server asked
The reply digest copied the shared secret right after the received
bytes, so a reply of 4100 bytes with a secret of 17 or more characters
wrote up to 47 bytes of it past the reply buffer on the stack, before
the reply was authenticated. The digest is now taken over the reply
and the secret in place, with nothing written past the reply, and it
fails closed when the hash cannot be made. A reply is also taken only
from the address and port of the server the request went to, and only
when its identifier and length field match the request, all before any
digest work; the reply's sender was never checked before.
2026-10-30 13:47:21 +03:00
Vladimir Dubrovin
e9714fa4df resolve: the PTR answer of dnsname walked inside its record data
The label walk of a DNS PTR answer ran to the end of the whole message,
not to the end of the record's data, and took a compression pointer for
a label length, so it could write past the end of the answer buffer on
the stack. An answer of 3841 bytes or more reached past the buffer
itself. A record that ended exactly at the end of the message was also
read one byte past it. The walk now stops at the end of the record's
data, a name that does not end there, or one that is compressed or
empty is not accepted, and the answer loop reads nothing past the
message.
2026-10-30 12:42:39 +03:00
Vladimir Dubrovin
9be415b731 StringsPlugin: a line's end cut off without reading past it
load_string() put a NUL where a line read from the file had its CR or
LF, and stepped over it as well as over the character after: past the
line's NUL terminator, into what was left in the buffer from longer
lines before, or past its end, writing NULs wherever it found CR or LF.
The line is cut at its first CR or LF instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e396da2696)
2026-10-28 12:02:37 +03:00
Vladimir Dubrovin
371e78691d Windows: an error set where socket errors are read
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
errno is WSAGetLastError() on Windows, but seterrno3() set the C
library's errno there: the EAGAIN of a TLS read or write wanting the
socket again was not seen as one, and the error left was whatever the
socket call before had set. seterrno3() sets the socket error now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:11:25 +03:00
Vladimir Dubrovin
a5129f85c7 ftppr: the server's answer to USER reaches the client (#1284)
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
A server which logs in on USER alone (230, anonymous FTP often does) or
refuses it had its answer read but not handed back: ftplogin() gave the
length of what it read only for the answer to PASS. The client, told
331 by ftppr and waiting for the answer to its PASS, waited until the
time out. The answer to USER is the client's now when there is no PASS.

The test case logs in through ftppr to a server answering 230, a
multiline 230, 331 then 230, 530, and 331 then 530.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 17:39:25 +03:00
github-actions[bot]
33dce70177 Update HTML documentation from man pages
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
Update HTML documentation / docs (push) Has been cancelled
Update wiki / wiki (push) Has been cancelled
2026-10-25 15:55:05 +00:00
Vladimir Dubrovin
d770c51761 -B: the TLS ClientHello split at the SNI, which -s is taken for splice
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:53:03 +03:00
Vladimir Dubrovin
aaa36f1dd1 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
b56e5aff2b ftppr: the data socket towards the client from the socket functions
Some checks are pending
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Waiting to run
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Waiting to run
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Waiting to run
It was made with socket(), past the socket functions of the service,
which the rest of its life went through.

(cherry picked from commit 7718ffa)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:56:15 +03:00
Vladimir Dubrovin
0c80c9f526 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().
(cherry picked from commit 126c7d0)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:56:15 +03:00
Vladimir Dubrovin
62fd5e3a8d TLS: a read needing to write, or a write to read, waits for it
A read TLS could do only once it wrote, or a write once it read, was an
error which ended the connection, as when TLS 1.3 answers a key update
while the socket cannot take more. Each TLS socket remembers what a read
and a write it could not do wait for, and the poll of the TLS layer
waits for it instead of what was asked: whatever that gets is the
caller's chance to try again, revents what it asked for, and its events
are given back as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:15:05 +03:00
Vladimir Dubrovin
50bc4b4e27 FilePlugin: socksend() given no clientparam, not a TLS state for one
socksend() takes the client's parameters, and was passed the socket
functions' state instead: NULL, the global functions used then, as
intended, but on a TLS connection the state of its TLS layer, which
socksend() read as a clientparam.
(cherry picked from commit 718798c)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:07:13 +03:00
Vladimir Dubrovin
b0b796823d README: branches, devel is 1.1 and experimental 2.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 16:17:28 +03:00
45 changed files with 509 additions and 69 deletions

View File

@ -8,7 +8,8 @@
- **Master** (current/latest) branch - 3proxy 1.0
- **0.9** (LTS) branch - 3proxy 0.9.9.x
- **Devel** branch - 3proxy 2.0-devel (don't use it)
- **Devel** branch - 3proxy 1.1 (development, don't use it in production)
- **Experimental** branch - 3proxy 2.0 (experimental, don't use it)
### Binaries (deb / rpm / Windows zip)

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

@ -245,6 +245,14 @@ 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 <b><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 <b>parent ... tls
0.0.0.0 0</b>: 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() <b>-s</b> does the same.
<b><br>
-s</b> <br>
(for admin) secure, allow only secure operations, currently
only traffic counters view without ability to reset. <br>

View File

@ -230,15 +230,16 @@ a certificate (not compatible with TLS 1.3)</p></td></tr>
<td width="4%">
<p><b>-s</b></p></td>
<p><b>-B</b></p></td>
<td width="5%"></td>
<td width="82%">
<p>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. <b>-s1</b>) to control the
splitting behaviour.</p></td></tr>
<p>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() <b>-s</b> does
the same; with splice() (Linux) <b>-s</b> is splice, see
<b>3proxy.cfg</b>(5).</p> </td></tr>
</table>
<p style="margin-left:9%;"><b>-Ximap | -Xpop3 |

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

@ -382,6 +382,21 @@ void cyclestep(void){
#define RETURN(x) {res = x; goto CLEARRETURN;}
static void printusage(struct extparam *cconf, int argc, char * argv[]){
fprintf(stderr, "Usage: %s [conffile]\n", argv[0]);
#ifdef _WIN32
fprintf(stderr, "\n\t%s --install [conffile]\n\tto install as service\n"
"\n\t%s --remove\n\tto remove service\n", argv[0], argv[0]);
#else
fprintf(stderr, "\n if conffile is missing, configuration is expected from stdin\n");
#endif
fprintf(stderr, "\n\t%s --help\n\tthis message\n", argv[0]);
fprintf(stderr, "available socket options:\n\t%s\n", printopts("\n\t"));
fprintf(stderr, "built with:\n\t%s\n", printfeatures("\n\t"));
fprintf(stderr, "\n%s %s\n%s\n", cconf->stringtable[2], cconf->stringtable[3], copyright);
}
#ifndef _WINCE
int main(int argc, char * argv[]) {
@ -424,6 +439,10 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
#endif
conf.stringtable = strings;
if(argc == 2 && (!strcmp((char *)argv[1], "--help") || !strcmp((char *)argv[1], "-h"))) {
printusage(&conf, argc, argv);
return 0;
}
#ifdef _WIN32
#ifndef _WINCE
if((argc == 2 || argc == 3)&& !strcmp((char *)argv[1], "--install")) {
@ -536,18 +555,9 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
}
if(argc > 2 || !(fp)) {
fprintf(stderr, "Usage: %s [conffile]\n", argv[0]);
#ifdef _WIN32
fprintf(stderr, "\n\t%s --install [conffile]\n\tto install as service\n"
"\n\t%s --remove\n\tto remove service\n", argv[0], argv[0]);
#else
fprintf(stderr, "\n if conffile is missing, configuration is expected from stdin\n");
#endif
fprintf(stderr, "available socket options:\n\t%s\n", printopts("\n\t"));
fprintf(stderr, "\n%s %s\n%s\n", conf.stringtable[2], conf.stringtable[3], copyright);
printusage(&conf, argc, argv);
return 1;
}
}
#ifndef _WIN32
/* before readconfig() starts the services: a client gone while it is

View File

@ -198,12 +198,20 @@ static int calc_replydigest(char *packet, char *original, const char *secret, in
int secretlen;
uint8_t calc_digest[AUTH_VECTOR_LEN];
uint8_t calc_vector[AUTH_VECTOR_LEN];
unsigned int dlen = AUTH_VECTOR_LEN;
mdh_ctx *ctx;
memcpy(calc_vector, packet + 4, AUTH_VECTOR_LEN);
memcpy(packet + 4, original, AUTH_VECTOR_LEN);
secretlen = strlen(secret);
memcpy(packet + len, secret, secretlen);
md5_calc(calc_digest, (u_char *)packet, len + secretlen);
/* hash the reply, then the secret: nothing is written past the reply */
if(!(ctx = mdh_init(MDH_MD5, 16))) return 2;
if(!mdh_update(ctx, packet, len) || !mdh_update(ctx, secret, secretlen) ||
!mdh_final(ctx, calc_digest, &dlen)) {
mdh_free(ctx);
return 2;
}
mdh_free(ctx);
/*
* Return 0 if OK, 2 if not OK.
@ -298,6 +306,7 @@ int radsend(struct clientparam * param, int auth, int stop){
int total_length;
int len;
PROXYSOCKADDRTYPE saremote;
PROXYSOCKADDRTYPE safrom;
struct pollfd fds[1];
char vector[AUTH_VECTOR_LEN];
radius_packet_t packet, rpacket;
@ -568,15 +577,25 @@ int radsend(struct clientparam * param, int auth, int stop){
continue;
}
salen = sizeof(saremote);
salen = sizeof(safrom);
data_len = so._recvfrom(so.state, remsock, (char *)&rpacket, sizeof(packet)-16,
0, (struct sockaddr *)&saremote, &salen);
0, (struct sockaddr *)&safrom, &salen);
if (data_len < 20) {
continue;
}
/* the reply must come from the server we asked */
if(*SAFAMILY(&safrom) != *SAFAMILY(&saremote) ||
memcmp(SAADDR(&safrom), SAADDR(&saremote), SAADDRLEN(&saremote)) ||
*SAPORT(&safrom) != *SAPORT(&saremote)) {
continue;
}
/* and answer this request, and be of its own length */
if(data_len != ntohs(rpacket.length) || rpacket.id != packet.id) {
continue;
}
if( auth && rpacket.code != PW_AUTHENTICATION_ACK &&
rpacket.code != PW_AUTHENTICATION_REJECT ){

View File

@ -634,6 +634,26 @@ int parseconnusername(char *username, struct clientparam *param, int extpasswd,
}
/* What waits for a socket waits in poll(): 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
@ -760,6 +780,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

@ -38,7 +38,7 @@ int ftplogin(struct clientparam *param, char *nbuf, int *innbuf) {
if(i < 3) return 706;
buf[i] = 0;
if(atoi(buf)/100 != 2) {
*innbuf = i;
if(innbuf)*innbuf = i;
return 702;
}
sprintf(buf, "USER %.128s\r\n", param->extusername?param->extusername:(unsigned char *)"anonymous");
@ -77,6 +77,9 @@ int ftplogin(struct clientparam *param, char *nbuf, int *innbuf) {
return 701;
}
}
/* USER answered at once, logged in (230) or refused: that answer is
the client's, waiting for one to its PASS */
else if(innbuf)*innbuf = i;
if(res != 2) {
return 700;
}

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=param->srv->so._socket(param->sostate, 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

@ -220,6 +220,14 @@ struct pcre_filter_data {
struct ace *acl;
};
/* What a connection matches with: pcre2_match() writes into its match
data, so a thread matching on the rule's own one would race every
other thread doing the same. */
struct pcre_client_data {
struct pcre_filter_data *pcrefd;
pcre2_match_data * match_data;
};
static void pcre_data_free(struct pcre_filter_data *pcrefd){
_3proxy_mutex_lock(&pcre_mutex);
pcrefd->users--;
@ -253,16 +261,30 @@ static void* pcre_filter_open(void * idata, struct srvparam * param){
static FILTER_ACTION pcre_filter_client(void *fo, struct clientparam * param, void** fc){
int res;
struct ace tmpace;
struct pcre_filter_data *rule = (struct pcre_filter_data *)fo;
struct pcre_client_data *pcd;
*fc = fo;
if(!fo) return PASS;
#define pcrefd ((struct pcre_filter_data *)fo)
if(!pcrefd->acl) return CONTINUE;
if(!rule->acl) return CONTINUE;
memset (&tmpace, 0, sizeof(struct ace));
tmpace.src = pcrefd->acl->src;
tmpace.src = rule->acl->src;
res = pl->ACLMatches(&tmpace, param);
#undef pcrefd
return (res)? CONTINUE:PASS;
if(!res) return PASS;
/* the connection goes on with a match data of its own */
if(!(pcd = pl->mallocfunc(sizeof(struct pcre_client_data)))){
*fc = NULL;
return rule->action;
}
pcd->pcrefd = rule;
pcd->match_data = NULL;
if(rule->re && !(pcd->match_data = pcre2_match_data_create_from_pattern(rule->re, NULL))){
pl->freefunc(pcd);
*fc = NULL;
return rule->action;
}
*fc = pcd;
return CONTINUE;
}
/* What a rewritten buffer keeps free for its caller to append to. */
@ -277,7 +299,8 @@ static FILTER_ACTION pcre_filter_buffer(void *fc, struct clientparam *param, uns
char * replace;
char * tmpbuf, *target, *newbuf;
int nreplaces=0;
#define pcrefd ((struct pcre_filter_data *)fc)
struct pcre_client_data *pcd = (struct pcre_client_data *)fc;
#define pcrefd (pcd->pcrefd)
for(acl = pcrefd->acl; acl; acl=acl->next){
if(pl->ACLMatches(acl, param)){
@ -290,9 +313,9 @@ static FILTER_ACTION pcre_filter_buffer(void *fc, struct clientparam *param, uns
for(; offset < *length_p; nreplaces++){
int repsz;
count = pcre2_match(pcrefd->re, (PCRE2_SPTR)*buf_p, *length_p, offset, 0, pcrefd->match_data, pcrefd->match_context);
count = pcre2_match(pcrefd->re, (PCRE2_SPTR)*buf_p, *length_p, offset, 0, pcd->match_data, pcrefd->match_context);
if(count <= 0) break;
ovector = pcre2_get_ovector_pointer(pcrefd->match_data);
ovector = pcre2_get_ovector_pointer(pcd->match_data);
if(ovector[0] > (PCRE2_SIZE)*length_p || ovector[1] > (PCRE2_SIZE)*length_p || ovector[1] < ovector[0]) break;
if(!(replace = pcrefd->replace) || param->nooverwritefilter) return pcrefd->action;
@ -376,7 +399,13 @@ static FILTER_ACTION pcre_filter_buffer(void *fc, struct clientparam *param, uns
#undef pcrefd
}
static void pcre_filter_clear(void *fo){
static void pcre_filter_clear(void *fc){
struct pcre_client_data *pcd = (struct pcre_client_data *)fc;
if(pcd){
if(pcd->match_data) pcre2_match_data_free(pcd->match_data);
pl->freefunc(pcd);
}
}
static void pcre_filter_close(void *fo){

View File

@ -286,7 +286,7 @@ void processcallbacks(struct fp_stream *fps, int what, char *msg, int size){
case GOT_SMTP_REQ:
case GOT_SMTP_DATA:
fps->state = FLUSH_DATA;
pl->socksend(fps->fpd.cp->sostate,fps->fpd.cp->clisock, (unsigned char *)fp_stringtable[1], (int)strlen((char *)fp_stringtable[1]), pl->conf->timeouts[STRING_S]);
pl->socksend(NULL, fps->fpd.cp->clisock, (unsigned char *)fp_stringtable[1], (int)strlen((char *)fp_stringtable[1]), pl->conf->timeouts[STRING_S]);
fps->state = state;
break;
case GOT_HTTP_REQUEST:
@ -298,7 +298,7 @@ void processcallbacks(struct fp_stream *fps, int what, char *msg, int size){
case GOT_HTTP_SRVDATA:
if(!fps->serversent){
fps->state = FLUSH_DATA;
pl->socksend(fps->fpd.cp->sostate, fps->fpd.cp->clisock, (unsigned char *)fp_stringtable[0], (int)strlen((char *)fp_stringtable[0]), pl->conf->timeouts[STRING_S]);
pl->socksend(NULL, fps->fpd.cp->clisock, (unsigned char *)fp_stringtable[0], (int)strlen((char *)fp_stringtable[0]), pl->conf->timeouts[STRING_S]);
fps->state = state;
}
break;
@ -306,7 +306,7 @@ void processcallbacks(struct fp_stream *fps, int what, char *msg, int size){
case GOT_FTP_REQ:
case GOT_FTP_SRVDATA:
fps->state = FLUSH_DATA;
pl->socksend(fps->fpd.cp->sostate, fps->fpd.cp->ctrlsock, (unsigned char *)fp_stringtable[1], (int)strlen((char *)fp_stringtable[1]), pl->conf->timeouts[STRING_S]);
pl->socksend(NULL, fps->fpd.cp->ctrlsock, (unsigned char *)fp_stringtable[1], (int)strlen((char *)fp_stringtable[1]), pl->conf->timeouts[STRING_S]);
fps->state = state;
break;
default:
@ -358,7 +358,7 @@ static int copyfdtosock(struct fp_stream * fps, DIRECTION which, long len){
if(fps->serversent >= fps->srvhdrwritten){
sprintf(fps->buf, "%lx\r\n", len);
sendchunk = (int)strlen(fps->buf);
if(pl->socksend(fps->fpd.cp->sostate, fps->fpd.cp->clisock, (unsigned char *)fps->buf, sendchunk, pl->conf->timeouts[STRING_S]) != sendchunk){
if(pl->socksend(NULL, fps->fpd.cp->clisock, (unsigned char *)fps->buf, sendchunk, pl->conf->timeouts[STRING_S]) != sendchunk){
return -4;
}
}
@ -397,13 +397,13 @@ static int copyfdtosock(struct fp_stream * fps, DIRECTION which, long len){
#endif
return -3;
}
if(pl->socksend(fps->fpd.cp->sostate, sock, (unsigned char *)fps->buf, res, pl->conf->timeouts[STRING_S]) != res) {
if(pl->socksend(NULL, sock, (unsigned char *)fps->buf, res, pl->conf->timeouts[STRING_S]) != res) {
return -4;
}
len -= res;
}
if(sendchunk){
if(pl->socksend(fps->fpd.cp->sostate, sock, (unsigned char *)"\r\n", 2, pl->conf->timeouts[STRING_S]) != 2)
if(pl->socksend(NULL, sock, (unsigned char *)"\r\n", 2, pl->conf->timeouts[STRING_S]) != 2)
return -4;
}
fps->state = state;

View File

@ -61,14 +61,9 @@ char **load_string(FILE *f,int max_count_str, int *countloadstr,
if (strstr(tmpbuf1,"[end]")==NULL)
{
/* find and replace \n \r*/
rstr = tmpbuf1;
while (*rstr!='\0')
{
if (*rstr=='\r' || *rstr=='\n' )
{ *rstr='\0'; rstr++; }
rstr++;
}
/* the line's end cut off: what the file says with \n is what is
sent */
tmpbuf1[strcspn(tmpbuf1, "\r\n")] = '\0';
while ((rstr=strstr(tmpbuf1,"\\n")))
{

View File

@ -191,7 +191,8 @@ size_t threadstacksize(int extra);
#ifndef strncasecmp
#define strncasecmp strnicmp
#endif
#define seterrno3(x) _set_errno(x)
/* errno is WSAGetLastError() here */
#define seterrno3(x) WSASetLastError(x)
#else
#define seterrno3(x) (errno = x)
#endif
@ -405,6 +406,8 @@ unsigned char * dologname (unsigned char *buf, unsigned char *name, const unsign
int readconfig(FILE * fp);
void initcommands(void);
int connectwithpoll(struct clientparam *param, SOCKET sock, struct sockaddr *sa, SASIZETYPE size, int to);
void nonblock(SOCKET sock);
int acceptagain(void);
int bindwithrange(struct clientparam *param, SOCKET sock, PROXYSOCKADDRTYPE *sa, uint32_t range);
int bindtodevice(struct sockfuncs *sf, void *state, SOCKET sock, const char *device, int family);
int extbindtodevice(struct clientparam *param, SOCKET sock);
@ -509,6 +512,7 @@ extern struct socketoptions {
int getopts(const char *s, struct sockoptset *opts);
void setopts(SOCKET s, const struct sockoptset *opts);
char * printopts(char *sep);
char * printfeatures(char *sep);
#ifdef _WINCE
char * CEToUnicode (const char *str);

View File

@ -63,6 +63,8 @@ void * threadfunc (void *p) {
}
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;
}
@ -224,6 +226,69 @@ char * printopts(char *sep){
}
char featsbuf[1024];
/* The features of this build, by the names the manual and the test suite
know them, so a user can see what a binary was made with. */
char * printfeatures(char *sep){
int pos=0;
#define ADDFEATURE(x) pos += sprintf(featsbuf+pos,"%s%s",pos?sep:"",x)
#ifdef WITH_SSL
ADDFEATURE("ssl");
#ifdef WITH_WOLFSSL
ADDFEATURE("wolfssl");
#else
ADDFEATURE("openssl");
#endif
#endif
#ifndef NORADIUS
ADDFEATURE("radius");
#endif
#ifdef WITH_PCRE
ADDFEATURE("pcre");
#endif
#ifndef NOIPV6
ADDFEATURE("ipv6");
#endif
#ifdef WITH_UN
ADDFEATURE("unix");
#endif
#ifdef WITH_HTTPSRV
ADDFEATURE("httpsrv");
#endif
#ifdef WITH_TRANSPARENT
ADDFEATURE("transparent");
#endif
#ifdef WITH_NETFILTER
ADDFEATURE("netfilter");
#endif
#ifdef WITH_PF
ADDFEATURE("pf");
#endif
#ifdef WITH_ODBC
ADDFEATURE("odbc");
#endif
#ifdef WITH_FTP
ADDFEATURE("ftppr");
#endif
#ifdef WITH_POP3P
ADDFEATURE("pop3p");
#endif
#ifdef WITH_SMTPP
ADDFEATURE("smtpp");
#endif
#ifdef WITH_IMAPP
ADDFEATURE("imapp");
#endif
#ifndef NOPLUGINS
ADDFEATURE("plugins");
#endif
#undef ADDFEATURE
return featsbuf;
}
/* 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
@ -364,6 +429,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 stderr\n"
@ -631,6 +697,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 */
@ -864,6 +937,8 @@ static int cblisten(struct srvstart *s){
return -6;
}
setopts(srv->cbsock, &srv->cbssockopts);
/* 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

@ -167,6 +167,7 @@ uint32_t udpresolve(int af, unsigned char * name, unsigned char * value, uint32_
k += 4;
if(na > 255) na = 255; /* somebody is very evil */
for (j = 0; j < na; j++) { /* now there should be answers */
if(k >= len) break;
while(buf[k] < 192 && buf[k] !=0 && (k+buf[k]+14) < len) k+= (buf[k] + 1);
if(!buf[k]) k--;
if((k+(af == AF_INET6?28:16)) > len) {
@ -195,12 +196,14 @@ uint32_t udpresolve(int af, unsigned char * name, unsigned char * value, uint32_
k+= (12 + flen);
continue; /* we need A PTR */
}
for (s2 = buf + k + 12; s2 < (buf + k + 12 + len) && *s2; ){
s1 = s2 + ((unsigned)*s2) + 1;
s1 = buf + k + 12 + flen; /* the end of this record's data, inside the answer */
for (s2 = buf + k + 12; s2 < s1 && *s2 && *s2 < 64; ){
unsigned char *next = s2 + ((unsigned)*s2) + 1;
*s2 = '.';
s2 = s1;
s2 = next;
}
*s2 = 0;
if(s2 >= s1 || *s2 || s2 == buf + k + 12)
break; /* the name runs past its data, is compressed or empty */
if(param->username)free(param->username);
param->username = (unsigned char *)strdup ((char *)buf + k + 13);

View File

@ -74,6 +74,9 @@ static int client_mode = 0;
struct SSLsock {
SOCKET s;
SSL_CONN conn;
/* what a read and a write which could not be done wait for, POLLIN
or POLLOUT, 0 once one is: TLS may have to write to read */
short rwant, wwant;
};
struct SSLstate {
@ -102,10 +105,12 @@ static void addSSL(
if (cli_s != INVALID_SOCKET){
SOSTATE->cli.s = cli_s;
SOSTATE->cli.conn = cli_conn;
SOSTATE->cli.rwant = SOSTATE->cli.wwant = 0;
}
if (srv_s != INVALID_SOCKET){
SOSTATE->srv.s = srv_s;
SOSTATE->srv.conn = srv_conn;
SOSTATE->srv.rwant = SOSTATE->srv.wwant = 0;
}
}
@ -136,12 +141,14 @@ static ssize_t ssl_send(void *state, SOCKET s, const void *msg, size_t len, int
int res, err;
if((res = ssl_write(sslq->conn, (void *)msg, len)) <= 0){
err = SSL_get_error((SSL *)((ssl_conn*)sslq->conn)->ssl, res);
if (err == SSL_ERROR_WANT_WRITE){
if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ){
sslq->wwant = (err == SSL_ERROR_WANT_READ)? POLLIN : POLLOUT;
seterrno3(EAGAIN);
return -1;
}
else seterrno3(err);
}
else sslq->wwant = 0;
return res;
}
@ -158,14 +165,16 @@ static ssize_t ssl_sendto(void *state, SOCKET s, const void *msg, size_t len, in
if ((sslq = searchSSL(state, s))){
int res, err;
if((res = ssl_write(sslq->conn, (void *)msg, len)) <= 0) {
if((res = ssl_write(sslq->conn, (void *)msg, len)) <= 0){
err = SSL_get_error((SSL *)((ssl_conn*)sslq->conn)->ssl, res);
if (err == SSL_ERROR_WANT_WRITE){
if (err == SSL_ERROR_WANT_WRITE || err == SSL_ERROR_WANT_READ){
sslq->wwant = (err == SSL_ERROR_WANT_READ)? POLLIN : POLLOUT;
seterrno3(EAGAIN);
return -1;
}
else seterrno3(err);
}
else sslq->wwant = 0;
return res;
}
@ -183,12 +192,14 @@ static ssize_t ssl_recvfrom(void *state, SOCKET s, void *msg, size_t len, int f
int res, err;
if((res = ssl_read(sslq->conn, (void *)msg, len)) <= 0) {
err = SSL_get_error((SSL *)((ssl_conn*)sslq->conn)->ssl, res);
if (err == SSL_ERROR_WANT_READ) {
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
sslq->rwant = (err == SSL_ERROR_WANT_WRITE)? POLLOUT : POLLIN;
seterrno3(EAGAIN);
return -1;
}
else seterrno3(err);
}
else sslq->rwant = 0;
return res;
}
return sso._recvfrom(sso.state, s, msg, len, flags, from, fromlen);
@ -205,12 +216,14 @@ static ssize_t ssl_recv(void *state, SOCKET s, void *msg, size_t len, int flags)
int res, err;
if((res = ssl_read(sslq->conn, (void *)msg, len)) <= 0) {
err = SSL_get_error((SSL *)((ssl_conn*)sslq->conn)->ssl, res);
if (err == SSL_ERROR_WANT_READ) {
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
sslq->rwant = (err == SSL_ERROR_WANT_WRITE)? POLLOUT : POLLIN;
seterrno3(EAGAIN);
return -1;
}
else seterrno3(err);
}
else sslq->rwant = 0;
return res;
}
@ -235,6 +248,8 @@ static int ssl_poll(void *state, struct pollfd *fds, nfds_t nfds, int timeout){
struct SSLsock *sslq = NULL;
unsigned int i;
int ret = 0;
short own[64], *events = own, ev;
for(i = 0; i < nfds; i++){
if((fds[i].events & POLLIN) && (sslq = searchSSL(state, fds[i].fd)) && ssl_pending(sslq->conn)){
fds[i].revents = POLLIN;
@ -243,8 +258,29 @@ static int ssl_poll(void *state, struct pollfd *fds, nfds_t nfds, int timeout){
else fds[i].revents = 0;
}
if(ret) return ret;
for(i = 0; i < nfds; i++) if((sslq = searchSSL(state, fds[i].fd)) && (sslq->rwant || sslq->wwant)) break;
if(i == nfds) return sso._poll(state, fds, nfds, timeout);
/* A read or a write TLS could not do waits for what TLS needs, a read
for the socket to be writable: whatever that gets is the caller's
chance to try again, revents what it asked for, and its events are
given back as they were. */
if(nfds > sizeof(own)/sizeof(own[0]) && !(events = malloc(nfds * sizeof(*events)))) return -1;
for(i = 0; i < nfds; i++){
events[i] = fds[i].events;
if(!(sslq = searchSSL(state, fds[i].fd))) continue;
ev = fds[i].events & ~(POLLIN|POLLOUT);
if(fds[i].events & POLLIN) ev |= sslq->rwant? sslq->rwant : POLLIN;
if(fds[i].events & POLLOUT) ev |= sslq->wwant? sslq->wwant : POLLOUT;
fds[i].events = ev;
}
ret = sso._poll(state, fds, nfds, timeout);
for(i = 0; i < nfds; i++){
if(ret > 0 && fds[i].events != events[i] && (fds[i].revents & ~(POLLERR|POLLHUP|POLLNVAL)))
fds[i].revents = events[i] | (fds[i].revents & (POLLERR|POLLHUP|POLLNVAL));
fds[i].events = events[i];
}
if(events != own) free(events);
return ret;
}

View File

@ -13,6 +13,14 @@ skips itself when that is missing or the build has no TLS support. With no
`--bin` it looks in `bin/`, then `build/bin/`, then the per-configuration
directories a multi-configuration CMake generator uses.
The binary reports what it was built with in `3proxy --help` (a `built
with:` list), and the runner reads that before the first case: a case names
the features it needs with `if not t.skip_unless("ssl"): return`, and
skips on a build without them instead of failing. A binary too old to
report its features skips every case that needs one, and the cases with no
feature needs still run; some cases also probe at runtime, as a build can
leave a feature compiled in but unusable.
The proxy under test is also the origin server the tests talk to: the `http`
command's `echo` operation reports back how a request arrived - method, path,
query, host, and the source port it came from - and `data` generates a body

View File

@ -4,6 +4,8 @@ import time
def run(t):
if not t.skip_unless("httpsrv"):
return
adm = t.free_port()
lim = t.free_port()
server = t.start("admin", f"""

View File

@ -7,7 +7,9 @@ handshake and the service supplies the port.
def run(t):
certs = t.certs()
if not t.skip_unless("httpsrv"):
return
certs = t.certs() if t.has("ssl") else None
plain = t.free_port()
port = t.free_port()
secure = t.free_port() if certs else None

108
tests/cases/ftppr.py Normal file
View File

@ -0,0 +1,108 @@
"""The FTP proxy logging in to its server: whatever the server answers the
login with reaches the client, which waits for the answer to its PASS."""
import socket
import threading
# what the server answers USER with, and PASS if it asks for one
LOGINS = {
"230 to USER": ([b"230 Anonymous user logged in"], None),
"multiline 230 to USER": ([b"230-Welcome", b"230-to the server", b"230 Login successful."], None),
"331 to USER, 230 to PASS": ([b"331 Please specify the password."], [b"230 Login successful."]),
"530 to USER": ([b"530 Not allowed here"], None),
"331 to USER, 530 to PASS": ([b"331 Please specify the password."], [b"530 Login incorrect."]),
}
def ftp_server(listener, user, password):
"""Greets, answers the login as told, then NOOP and QUIT."""
while True:
try:
c, _ = listener.accept()
except OSError:
return
try:
f = c.makefile("rb")
c.sendall(b"220 Fake FTP\r\n")
if not f.readline().upper().startswith(b"USER "):
continue
c.sendall(b"".join(line + b"\r\n" for line in user))
if password is not None:
if not f.readline().upper().startswith(b"PASS "):
continue
c.sendall(b"".join(line + b"\r\n" for line in password))
for line in f:
if line.upper().startswith(b"NOOP"):
c.sendall(b"200 NOOP ok\r\n")
elif line.upper().startswith(b"QUIT"):
c.sendall(b"221 Bye\r\n")
break
else:
c.sendall(b"502 Not implemented\r\n")
except OSError:
pass
finally:
c.close()
def reply(f):
"""The last line of a reply, multiline or not; empty at the end or on a
time out."""
while True:
try:
line = f.readline()
except OSError:
return b""
if not line:
return b""
if len(line) < 4 or line[3:4] != b"-":
return line.rstrip()
def run(t):
if not t.skip_unless("ftppr"):
return
servers = {}
for name, (user, password) in LOGINS.items():
listener = socket.socket()
listener.bind(("127.0.0.1", 0))
listener.listen(5)
threading.Thread(target=ftp_server, args=(listener, user, password), daemon=True).start()
servers[name] = listener
port = t.free_port()
t.start("ftppr", f"""
log
auth none
ftppr -p{port}
""", ports=[port])
# without WITH_FTP the service closes the connection at once
s = socket.create_connection(("127.0.0.1", port), timeout=5)
built = reply(s.makefile("rb"))
s.close()
if not built:
t.skip("the FTP proxy (not built in this configuration)")
for listener in servers.values():
listener.close()
t.stop_all()
return
for name, listener in servers.items():
s = socket.create_connection(("127.0.0.1", port), timeout=5)
f = s.makefile("rb")
reply(f)
s.sendall(b"USER anonymous@127.0.0.1:%d\r\n" % listener.getsockname()[1])
reply(f)
s.sendall(b"PASS test@example.com\r\n")
got = reply(f)
ok = LOGINS[name][1] if LOGINS[name][1] is not None else LOGINS[name][0]
t.eq(ok[-1][:3], got[:3], f"{name}: the client gets the server's answer")
if got.startswith(b"230"):
s.sendall(b"NOOP\r\n")
t.eq(b"200", reply(f)[:3], f"{name}: and the session goes on")
s.close()
for listener in servers.values():
listener.close()
t.stop_all()

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
openport = t.free_port()
t.start("httpsrv_auth", f"""

View File

@ -8,6 +8,8 @@ import os
def run(t):
if not t.skip_unless("httpsrv"):
return
root = os.path.join(t.tmpdir, "web")
os.makedirs(os.path.join(root, "picts", "set"), exist_ok=True)
with open(os.path.join(root, "a.html"), "w") as fp:

View File

@ -10,6 +10,8 @@ import os
def run(t):
if not t.skip_unless("httpsrv"):
return
root = os.path.join(t.tmpdir, "ka")
os.makedirs(root, exist_ok=True)
with open(os.path.join(root, "a.html"), "w") as fp:

View File

@ -4,6 +4,8 @@ import time
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
t.start("httpsrv_ops", f"""
log

View File

@ -6,6 +6,8 @@ most of them away before they ever reached the server.
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
t.start("httpsrv_parsing", f"""
log

View File

@ -9,6 +9,8 @@ import os
def run(t):
if not t.skip_unless("httpsrv"):
return
root = os.path.join(t.tmpdir, "pp")
os.makedirs(root, exist_ok=True)
with open(os.path.join(root, "a.html"), "w") as fp:

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
srv2 = t.free_port()
t.start("httpsrv_rules", f"""

View File

@ -7,6 +7,8 @@ nscache6: nscache holds the IPv4 side and nothing else.
def run(t):
if not t.skip_unless("httpsrv", "ipv6"):
return
if not t.has_ipv6():
t.skip("IPv6 (this machine has no IPv6 loopback)")
return

View File

@ -8,6 +8,8 @@ import time
def run(t):
if not t.skip_unless("httpsrv"):
return
# The origin runs in a process of its own, so the limit only applies to
# the mapper in front of it.
origin = t.free_port()

View File

@ -30,6 +30,8 @@ def wait_served(server, needle, count, since=0, timeout=5.0):
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
good = t.free_port()
spare = t.free_port()

View File

@ -46,6 +46,8 @@ UNHONOURED = (1, 99)
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
prx = t.free_port()
sks = t.free_port()

View File

@ -37,6 +37,8 @@ def wait_served(server, needle, count, since=0, timeout=5.0):
def run(t):
if not t.skip_unless("httpsrv"):
return
adm = t.free_port()
prx = t.free_port()
dummy = t.free_port()

View File

@ -20,6 +20,8 @@ def _has_pcre(t):
def run(t):
if not t.skip_unless("httpsrv", "pcre"):
return
if not _has_pcre(t):
t.skip("PCRE (this build has no PCRE support)")
return

View File

@ -26,6 +26,8 @@ def halfclose_origin(listener):
def run(t):
if not t.skip_unless("httpsrv"):
return
# --- tcppm ---------------------------------------------------------
origin = t.free_port()
mapped = t.free_port()

View File

@ -6,6 +6,8 @@ from a clean list.
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
other = t.free_port()
prx = t.free_port()

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
sks = t.free_port()
sauth = t.free_port()

View File

@ -11,6 +11,8 @@ def _no_tls(t, server):
def run(t):
if not t.skip_unless("httpsrv", "ssl"):
return
certs = t.certs()
if not certs:
t.skip("TLS (openssl is not available to generate certificates)")

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv", "ssl"):
return
certs = t.certs()
if not certs:
t.skip("tlspr (openssl is not available to generate certificates)")

View File

@ -39,6 +39,8 @@ def _config(t, name, body):
def run(t):
if not t.skip_unless("httpsrv"):
return
# --- the command and its modes ------------------------------------
# These need no redirection, so they run wherever the feature is built.
out = _config(t, "transparent_probe", "log\ntransparent")

View File

@ -36,6 +36,8 @@ def unix_get(path, url, timeout=10):
def run(t):
if not t.skip_unless("httpsrv", "unix"):
return
if not hasattr(socket, "AF_UNIX") or sys.platform.startswith("win"):
t.skip("Unix domain sockets (not on this platform)")
return

View File

@ -122,10 +122,11 @@ class Failure(Exception):
class Tester:
"""The API a case runs against: start servers, make requests, assert."""
def __init__(self, binary, tmpdir, case):
def __init__(self, binary, tmpdir, case, features=None):
self.binary = binary
self.tmpdir = tmpdir
self.case = case
self.features = features if features is not None else frozenset()
self.servers = []
self.checks = []
self.timeout = 10
@ -135,6 +136,25 @@ class Tester:
self.logs = []
self.udp_servers = []
# ---- build features ----------------------------------------------
def has(self, *names):
"""Whether the binary reports every named feature."""
return all(name in self.features for name in names)
def skip_unless(self, *names):
"""Skip what needs a feature the binary was built without.
Returns False after recording the skip, so a case says
`if not t.skip_unless("ssl"): return` and keeps its checks out of
the failed count on a build that cannot pass them.
"""
missing = [name for name in names if name not in self.features]
if not missing:
return True
self.skip("built without: " + ", ".join(missing))
return False
# ---- servers -----------------------------------------------------
def has_ipv6(self):

View File

@ -47,6 +47,37 @@ def load_case(path):
return name, module
def built_features(binary):
"""The features the binary reports in `3proxy --help`, as a set.
A binary too old to have the list, or one that cannot be run at all,
reports nothing: every case that needs a feature then skips, and the
ones that need none still run.
"""
import subprocess
try:
done = subprocess.run([binary, "--help"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=15)
text = done.stdout.decode("utf-8", "replace")
except (OSError, subprocess.SubprocessError):
return frozenset()
features = set()
collecting = False
for line in text.splitlines():
stripped = line.strip()
if line.startswith("built with:"):
collecting = True
rest = stripped[len("built with:"):].strip()
if rest:
features.update(rest.split())
continue
if collecting:
if not line[:1].isspace() or not stripped:
break
features.update(stripped.split())
return frozenset(features)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("pattern", nargs="?", default="",
@ -75,7 +106,9 @@ def main():
return 2
tmpdir = tempfile.mkdtemp(prefix="3proxy-tests.")
features = built_features(binary)
print(f"3proxy tests: {binary}")
print(f"built with: {' '.join(sorted(features)) or '(nothing reported)'}")
print(f"working in: {tmpdir}\n")
passed = failed = skipped = 0
@ -85,7 +118,7 @@ def main():
for path in paths:
name, module = load_case(path)
print(f" {name}")
tester = Tester(binary, tmpdir, name)
tester = Tester(binary, tmpdir, name, features)
error = None
try:
module.run(tester)