Support acls for UDP ASSOC, fix udppm via SOCKSv5
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

This commit is contained in:
Vladimir Dubrovin 2026-08-11 19:02:54 +03:00
parent ddb74a111c
commit bebc3894e2
14 changed files with 699 additions and 124 deletions

View File

@ -428,6 +428,139 @@ use 3proxy's built-in resolver (nserver / nscache / nscache6).
Public resolvers like those from Google have rate limits. For a large number of
requests, install a local caching recursor (ISC bind named, PowerDNS recursor, etc).
<h4>Use the Authentication Cache</h4>
Authentication and authorization are performed for every request. Password
lookup itself is a hash lookup and is cheap, but the ACL is walked linearly,
and with RADIUS or plugin authentication every request costs a network round
trip. 'authcache' keeps the result of a successful authentication for a number
of seconds:
<pre>
authcache &lt;cachetype&gt; &lt;cachetime&gt; &lt;cachesize&gt;
</pre>
'cachetype' lists the fields the entry is keyed by, 'cachetime' is the lifetime
in seconds, and the optional 'cachesize' limits the number of entries. The
cached type must be placed before the real authentication type in the 'auth'
command: a cache miss falls through to the next type in the chain, and a hit
skips it.
<p>There are two cached types:
<ul>
<li>'cache' caches the authentication only; the ACL is still checked on every
request. Use it when ACLs must take effect immediately, or when the
authentication method returns per-user parameters that the authorization step
has to apply.
<li>'cacheacl' also caches the authorization result, so the ACL is not walked
at all on a hit. This is the one that helps with long ACLs, but the key must
cover every field the ACL limits, and it can not be used with redirections.
</ul>
<p><b>'cacheacl' does not work with parent proxies.</b> Redirections are applied
while the ACL is walked, so on a 'cacheacl' hit no redirection happens at all:
the request is sent directly instead of through the 'parent' proxy of the
matching ACL entry, and nothing is reported. The first request for a given key
uses the parent and the rest do not, which makes it look intermittent. Use
'cache' with parent proxies:
<pre>
authcache user,pass 60
auth cache strong
allow user1
parent 1000 socks5 10.0.0.1 1080
proxy
</pre>
The one exception is 'parent ... extip': the external address is stored in the
cache entry and restored on a hit, but only if 'ext' is a part of the key:
<pre>
authcache user,pass,ext 60
auth cacheacl strong
allow user1
parent 1000 extip 1.2.3.4 0
proxy
</pre>
Without 'ext' in the key the external address falls back to the default one on
every cache hit.
<p><b>The key must match the ACL.</b> On a 'cacheacl' hit the cached result is
reused as is, so any field the ACL limits but the key does not contain makes
the cached result apply to a request it was not computed for. Use 'user' and
'pass' for the userlist, 'ip' for the sourcelist, 'dstaddr' and 'dsthost' for
the targetlist, 'dstport' for the targetportlist and 'dstoper' for the
operationlist. For this ACL:
<pre>
allow user1,user2 * * 80,443
deny *
</pre>
the key needs 'user', 'pass' and 'dstport':
<pre>
users $/etc/3proxy/passwd
authcache user,pass,dstport 60 65536
auth cacheacl strong
allow user1,user2 * * 80,443
deny *
proxy
</pre>
<p>With RADIUS the round trip is the expensive part, so cache by whatever
identifies the client:
<pre>
radius secret 1.2.3.4
authcache ip,user,pass 600
auth cache radius
proxy
</pre>
'cache' rather than 'cacheacl' is used here because RADIUS may return per-user
parameters such as the outgoing IP; add 'ext' to the key to cache the external
address returned by RADIUS as well.
<p>Choose 'cachetime' to balance the load against how quickly a password or ACL
change has to take effect. Cache entries are <b>not</b> invalidated by a
configuration reload, so a long lifetime keeps applying the old decision to
existing keys after the new configuration is loaded.
<p>Note that 'ip' alone, or 'ip,user' without 'pass', assigns every connection
from the same address to the same user for the lifetime of the entry without
checking the password. That is a deliberate trade-off for high load; do not use
it where clients share addresses.
<h4>Tune UDP Destination Handling with '-U'</h4>
Within a SOCKSv5 UDP association the destination is taken from every datagram,
so 3proxy authorizes and logs every new destination. One association can reach
any number of destinations: a DNS forwarder produces one record, but a
peer-to-peer or QUIC client can produce hundreds, each costing an authorization
(a network round trip with RADIUS or plugin authentication) and a log record.
The 'socks' service option '-U' controls this:
<pre>
socks -U3 &#35; default: log and authorize every new destination
socks -U2 &#35; authorize, do not log
socks -U1 &#35; log, do not authorize
socks -U0 &#35; neither (same as bare -U)
</pre>
The destination of the first datagram is always authorized and logged, so the
association as a whole is still subject to ACLs, authentication and the choice
of the parent proxy.
<p>Before reaching for the option, note that the cost is already avoided when no
ACL entry reachable by the client limits the destination: in that case the
result can not depend on the destination and no re-authorization is performed
whatever '-U' says. '-U' only matters for configurations that do limit the
destination.
<p>For high load:
<ul>
<li><b>'-U2'</b> is the safe one. Destination ACLs keep working, only the
per-destination log records are dropped. Use it when log volume, not CPU, is
the problem.
<li><b>'-U' ('-U0')</b> removes both costs and is the fastest, but destination
ACLs then apply only to the first destination of each association, and the
parent proxy and external address chosen for it are kept for the whole
association. Use it when UDP destinations are not restricted anyway, or when
the restriction is enforced elsewhere (a firewall, or the parent proxy).
<li>Keep the default '-U3' when destination ACLs are a security boundary, and
make the authorization itself cheap with 'authcache' instead, see "Use the
Authentication Cache" above.
</ul>
<h4>Avoid Large Lists</h4>
Currently, 3proxy is not optimized to use large ACLs, user lists, etc. All lists

View File

@ -1338,6 +1338,82 @@ the second hop is 192.168.20.1, and the 3rd one is either 192.168.30.1 with a pr
of 30% or 192.168.40.1 with a probability of 70%.
</p>
<li><a name="UDPACL">How to apply ACLs to UDP traffic</a>
<p>
A SOCKSv5 UDP ASSOCIATE request only asks 3proxy to open a relay port, it
carries no destination. The destination is taken from every datagram the client
sends afterwards, so a single association may reach any number of destinations.
3proxy authorizes the destination of every datagram, so ACLs limiting the
destination address, host name or port do apply to UDP traffic.
</p>
<p>
Differences from TCP:
</p>
<ul>
<li>A datagram to a denied destination is dropped, the association is not
terminated.
<li>The parent proxy and the external address are selected for the destination
of the datagram and not for the ASSOCIATE request, and are re-established if a
later destination selects another ACL entry. Only <code>socks5</code> and
<code>socks5+</code> parents can be used for UDP.
<li>ACLs are checked for the datagrams sent by the client only. A datagram
received on the association is relayed to the client without an ACL check: with
a parent proxy only the datagrams from the parent are accepted, an association
without a parent accepts a datagram from any source. The source reported to the
client is the actual source of the datagram. The address in the log record is the destination the
client sent the datagrams to, the source of the received datagrams is not
logged.
<li>A client may use a single association to reach many destinations. An ACL
entry which limits the destination silently drops the datagrams to every other
destination, so limiting the destination of UDP traffic can break such clients
in a way which is hard to diagnose. Limit it only when it is really required.
<li>Bandwidth limiters and traffic counters which limit the destination are
selected when the destination is authorized, so they are selected for the
destination of the first datagram and are not re-selected for every following
destination. They can not be applied per destination to incoming UDP traffic at
all.
<li>Every change of the destination host name, address or port is authorized
again and logged. Use <code>nscache</code> for host names and the
authentication cache for anything more expensive than 'iponly'. The
<code>-U</code> option of the <code>socks</code> service turns the logging
and/or the re-authorization off; with <code>-U0</code> or <code>-U1</code>
only the first destination of an association is authorized.
</ul>
<p>
Limit the destinations a client may send datagrams to:
</p><pre>
auth iponly
allow * * 8.8.8.8 53
deny *
socks -p1080
</pre>
<p>
The ASSOCIATE request succeeds, datagrams to 8.8.8.8:53 are relayed, datagrams
to any other destination are dropped.
</p>
<p>
The same with 'strong' (or RADIUS, or plugin) authentication, where every new
destination would otherwise be authenticated again:
</p><pre>
users user1:CL:password
authcache user,pass,dstaddr,dstport 60
auth cacheacl strong
allow user1 * 8.8.8.8 53
deny *
socks -p1080
</pre>
<p>
With 'cacheacl' the ACL result is not re-evaluated on a cache hit, so every
field the ACL limits must be a part of the cache key: 'user' and 'pass' for the
userlist, 'dstaddr' and 'dstport' for the targetlist and the targetportlist,
'ip' for the sourcelist, 'dsthost' for host names in the targetlist, 'dstoper'
for the operationlist. Without the destination in the key the result cached for
one destination is applied to any other destination and the destination is not
checked at all. Use plain 'cache' instead of 'cacheacl' if ACLs may change
during the cache lifetime, or if a parent proxy is used: on a 'cacheacl' hit
the ACL is not walked, so no parent proxy is selected and datagrams are sent
directly.
</p>
<li><A NAME="BANDLIM">How to limit bandwidth</a>
<p>
3proxy supports bandwidth filters. Use the bandlimin/bandlimout and

View File

@ -1371,6 +1371,85 @@ pcre_extend deny * 192.168.0.1/16
192.168.10.1, второе - 192.168.20.1, а третье - либо 192.168.30.1 с
вероятностью 0.3 либо 192.168.40.1 с вероятностью 0.7
</p>
<li><a name="UDPACL"><i>Как применять ACL к UDP трафику</i></a>
<p>
Запрос SOCKSv5 UDP ASSOCIATE лишь просит 3proxy открыть порт для ретрансляции
и не содержит адреса назначения. Адрес назначения берется из каждой
датаграммы, отправленной клиентом, поэтому одна ассоциация может обращаться
к любому числу адресов назначения. 3proxy авторизует адрес назначения каждой
датаграммы, поэтому ACL, ограничивающие адрес, имя хоста или порт
назначения, применяются и к UDP трафику.
</p>
<p>
Отличия от TCP:
</p>
<ul>
<li>Датаграмма к запрещенному адресу назначения отбрасывается, ассоциация
при этом не завершается.
<li>Вышестоящий прокси и внешний адрес выбираются для адреса назначения
датаграммы, а не для запроса ASSOCIATE, и устанавливаются заново, если
последующий адрес назначения соответствует другой записи ACL. Для UDP
могут использоваться только родители типа <code>socks5</code> и
<code>socks5+</code>.
<li>ACL проверяются только для датаграмм, отправленных клиентом. Датаграмма,
полученная в рамках ассоциации, передается клиенту без проверки ACL: при
использовании вышестоящего прокси принимаются только датаграммы от него, а
ассоциация без вышестоящего прокси принимает датаграмму от любого источника.
Клиенту сообщается фактический адрес источника датаграммы. В журнал записывается адрес назначения, на который клиент
отправлял датаграммы, а не адрес источника полученных датаграмм.
<li>Клиент может использовать одну ассоциацию для обращения ко многим адресам
назначения. Запись ACL, ограничивающая адрес назначения, молча отбрасывает
датаграммы ко всем остальным адресам назначения, поэтому ограничение адреса
назначения для UDP может нарушить работу таких клиентов, и причину трудно
установить. Ограничивайте его, только если это действительно необходимо.
<li>Ограничители полосы и счетчики трафика, ограничивающие адрес назначения,
выбираются при авторизации адреса назначения, поэтому они выбираются для
адреса назначения первой датаграммы и не выбираются заново для каждого
последующего адреса назначения. К входящему UDP трафику они не могут быть
применены по адресу назначения вообще.
<li>Каждое изменение имени хоста, адреса или порта назначения
авторизуется заново и записывается в журнал. Используйте <code>nscache</code>
для имен хостов и кэш аутентификации для всего, что дороже 'iponly'. Опция
<code>-U</code> сервиса <code>socks</code> отключает журналирование и/или
повторную авторизацию; при <code>-U0</code> или <code>-U1</code>
авторизуется только первый адрес назначения ассоциации.
</ul>
<p>
Ограничить адреса назначения, куда клиент может отправлять датаграммы:
</p><pre>
auth iponly
allow * * 8.8.8.8 53
deny *
socks -p1080
</pre>
<p>
Запрос ASSOCIATE выполняется успешно, датаграммы к 8.8.8.8:53 передаются,
датаграммы к любому другому адресу назначения отбрасываются.
</p>
<p>
То же самое с аутентификацией 'strong' (или RADIUS, или через плагин), при
которой иначе каждый новый адрес назначения аутентифицировался бы заново:
</p><pre>
users user1:CL:password
authcache user,pass,dstaddr,dstport 60
auth cacheacl strong
allow user1 * 8.8.8.8 53
deny *
socks -p1080
</pre>
<p>
При 'cacheacl' результат проверки ACL не вычисляется заново при попадании
в кэш, поэтому каждое поле, ограничиваемое в ACL, должно входить в ключ
кэша: 'user' и 'pass' для списка пользователей, 'dstaddr' и 'dstport' для списка
адресов и списка портов назначения, 'ip' для списка источников, 'dsthost'
для имен хостов в списке назначения, 'dstoper' для списка операций. Без
адреса назначения в ключе результат, закэшированный для одного адреса
назначения, применяется к любому другому, и адрес назначения не
проверяется вообще. Используйте 'cache' вместо 'cacheacl', если ACL могут
изменяться в течение времени жизни кэша или если используется вышестоящий
прокси: при попадании в кэш 'cacheacl' обход ACL не выполняется, поэтому
вышестоящий прокси не выбирается и датаграммы отправляются напрямую.
</p>
<li><a name="BANDLIM"><i>Как ограничивать скорости приема</i></a>
<p>
3proxy позволяет устанавливать фильтры ширины потребляемого канала. Для этого

View File

@ -25,6 +25,26 @@ with an unreachable DNS server (because gethostbyname will block other threads).
user requests can be logged.
<li>Use -xyz+A character filtering sequences for 'logformat', especially with
ODBC logging, to prevent SQL and log record injections.
<li>With the 'cacheacl' authentication type the ACL result is cached and is not
re-evaluated on a cache hit, so every field the ACL limits must be a part of
the 'authcache' key. Any field the ACL limits but the key does not contain
makes the cached result apply to a request it was not computed for, and that
limitation is not enforced. Use 'user' and 'pass' for the userlist, 'ip' for
the sourcelist, 'dstaddr' and 'dsthost' for the targetlist, 'dstport' for the
targetportlist and 'dstoper' for the operationlist. Use plain 'cache' instead
of 'cacheacl' if ACLs may change during the cache lifetime; cache entries are
not invalidated by a configuration reload.
<li>'cacheacl' must not be used together with parent proxies. The ACL is not
walked on a cache hit, so no redirection is applied and the request is sent
directly instead of through the 'parent' proxy of the matching ACL entry,
without any error. If the parent proxy is the only sanctioned path to the
network, this silently bypasses it. Use 'cache' instead. 'parent ... extip'
is the only redirection which survives, and only if 'ext' is a part of the
'authcache' key.
<li>The destination of every datagram of a SOCKSv5 UDP association is
authorized, but only with the default '-U3' (or '-U2') of the 'socks' service.
With '-U0' or '-U1' only the first destination of an association is authorized,
so ACLs limiting the destination do not restrict the rest of the association.
<li>Immediately report all service crashes to the developers.
<li>Participate in code audit :)
</ul>

View File

@ -209,6 +209,16 @@ sending side and expects the answer, but it keeps the socket of the side which
has already closed the connection in CLOSE_WAIT state. Use -C to close both
sockets as soon as any of the sides closes the connection, -C1 to request
the default behaviour explicitly.
.br
.B -U\fI[MODE]\fR
(for socks) what to do when the destination changes within an UDP association:
\fB1\fR - log it, \fB2\fR - authorize it, \fB3\fR - both, the default,
\fB0\fR (same as bare \fB-U\fR) - neither. The destination of the first datagram
is always authorized and logged. With \fB-U0\fR and \fB-U1\fR the destination is
not authorized again, so ACLs limiting the destination only apply to the first
destination of the association, and the parent proxy and the external address
selected for it are kept for the whole association. Use \fB-U2\fR to keep the
authorization and to drop the per-destination log records only.
.br
(for dnspr) simple, do not use resolver and 3proxy cache, always use external DNS server.
.br
@ -638,6 +648,13 @@ SOCKSv5, FTP, POP3 and HTTP proxy.
authorization result is also cached and not re-evaluated on each request. Faster
than \fBcache\fR, but ACL changes do not take effect for cached users until the
cache entry expires. Use \fBcache\fR if ACLs may change during the cache lifetime.
Because the ACL is not walked on a cache hit, no redirection is applied to a
request served from the cache: it is sent directly instead of through the
\fBparent\fR proxy of the matching ACL entry, with no error reported. Do not use
\fBcacheacl\fR together with parent proxies, use \fBcache\fR instead. The only
redirection type which survives is \fBextip\fR, and only if \fBext\fR is a part
of the \fBauthcache\fR key, because the external address is then restored from
the cache entry.
.br
\fBradius\fR - authentication with RADIUS.
.br
@ -693,6 +710,17 @@ assigned to the same user without actual authentication.
.br
Multiple types can be combined (e.g. \fBip,user,dstaddr,dstport\fR).
.br
With \fBcacheacl\fR the ACL result is not re-evaluated on a cache hit, so every
field the ACL limits must be a part of the cache key, otherwise the result
cached for one request is applied to a different one: \fBuser\fR/\fBpass\fR
for the userlist, \fBip\fR for the sourcelist, \fBdstaddr\fR/\fBdsthost\fR
for the targetlist, \fBdstport\fR for the targetportlist and \fBdstoper\fR
for the operationlist. It is especially important for SOCKSv5 UDP traffic,
where the destination of every datagram is authorized: without \fBdstaddr\fR
and \fBdstport\fR in the key the result cached for one destination is applied
to any other destination and the destination is not checked at all, see
\fBallow\fR.
.br
Use auth type \fBcache\fR (or \fBcacheacl\fR) for cached authentication
.br
@ -777,6 +805,39 @@ non-privileged (1024-65535) ports on the remote side.
.br
Timeperiodlists is a list of time
periods in HH:MM:SS-HH:MM:SS format. For example, 00:00:00-08:00:00,17:00:00-24:00:00 lists non-working hours.
.br
A SOCKSv5 UDP ASSOCIATE request carries no destination, the destination of
every datagram is taken from the datagram itself. 3proxy authorizes the
destination of every datagram, so ACLs limiting the destination address, the
destination host name or the destination port do apply to UDP traffic, unless
the \fB-U\fR option of the \fBsocks\fR service says otherwise.
.br
ACLs are only checked for the datagrams sent by the client. A datagram received
on the association is relayed to the client without an ACL check: with a parent
proxy only the datagrams from the parent are accepted, an association without a
parent accepts a datagram from any source. The source reported to the client is
the actual source of the datagram. The address in the log record is the destination the
client sent the datagrams to, the source of the datagrams received on the
association is not logged.
.br
A client may use a single association to reach many destinations. An ACL entry
which limits the destination silently drops the datagrams to every other
destination, so limiting the destination of UDP traffic can break such clients
in a way which is hard to diagnose. Limit it only when it is really required.
.br
Bandwidth limiters (\fBbandlimin\fR, \fBbandlimout\fR) and traffic counters
(\fBcountin\fR, \fBcountout\fR) which limit the destination are selected when
the destination is authorized, so within an UDP association they are selected
for the destination of the first datagram and are not re-selected for every
following destination. They can not be applied per destination to the incoming
UDP traffic at all, because an incoming datagram is not matched against any
destination.
.br
Only \fBsocks5\fR and \fBsocks5+\fR parents can be used for UDP. Because
every new destination is authorized, use \fBauthcache\fR, especially with
\'strong\', RADIUS or plugin authentication: \fBcacheacl\fR with the
destination in the cache key if no parent proxy is used, \fBcache\fR
otherwise, see \fBcacheacl\fR.
.br
.BR parent

View File

@ -36,6 +36,8 @@ int ACLmatches(struct ace* acentry, struct clientparam * param){
struct hostname * hstentry=NULL;
int i;
int match = 0;
int dstdep = 0;
int preauth = (param->preauth == 1 && acentry->action <= REDIRECT);
username = param->username?param->username:(unsigned char *)"-";
if(acentry->src) {
@ -45,7 +47,10 @@ int ACLmatches(struct ace* acentry, struct clientparam * param){
}
if(!ipentry) return 0;
}
if((acentry->dst && (!SAISNULL(&param->req) || param->operation==BIND)) || (acentry->dstnames && param->hostname)) {
if(preauth && (acentry->dst || acentry->dstnames)) {
dstdep = 1;
}
else if((acentry->dst && (!SAISNULL(&param->req) || param->operation==BIND)) || (acentry->dstnames && param->hostname)) {
for(ipentry = acentry->dst; ipentry; ipentry = ipentry->next)
if(IPInentry((struct sockaddr *)&param->req, ipentry)) {
break;
@ -93,7 +98,10 @@ int ACLmatches(struct ace* acentry, struct clientparam * param){
}
if(!ipentry && !hstentry) return 0;
}
if(acentry->ports && (*SAPORT(&param->req) || param->operation == BIND)) {
if(preauth && acentry->ports) {
dstdep = 1;
}
else if(acentry->ports && (*SAPORT(&param->req) || param->operation == BIND)) {
for (portentry = acentry->ports; portentry; portentry = portentry->next)
if(ntohs(*SAPORT(&param->req)) >= portentry->startport &&
ntohs(*SAPORT(&param->req)) <= portentry->endport) {
@ -125,6 +133,10 @@ int ACLmatches(struct ace* acentry, struct clientparam * param){
}
}
if(acentry->weight && (acentry->weight < param->weight)) return 0;
if(dstdep) {
param->dstindep = 0;
return acentry->action != DENY;
}
return 1;
}
@ -132,6 +144,7 @@ int ACLmatches(struct ace* acentry, struct clientparam * param){
int checkACL(struct clientparam * param){
struct ace* acentry;
if(param->preauth == 1) param->dstindep = 1;
if(!param->srv->acl) {
return 0;
}
@ -150,6 +163,8 @@ int checkACL(struct clientparam * param){
if(param->redirected && acentry->chains && SAISNULL(&acentry->chains->addr) && !*SAPORT(&acentry->chains->addr)) {
continue;
}
param->lastace = acentry;
if(param->preauth) return 2;
if((param->operation == UDPASSOC)? (param->ctrlsocksrv != INVALID_SOCKET) : (param->remsock != INVALID_SOCKET)) {
return 0;
}
@ -166,6 +181,7 @@ int checkACL(struct clientparam * param){
}
return res;
}
param->lastace = acentry;
return acentry->action;
}
}

View File

@ -102,6 +102,7 @@ int cacheauth(struct clientparam * param){
*(SAFAMILY(&param->sinsl)) = ac.sinsl_family;
memcpy(SAADDR(&param->sinsl), ac.sinsl_addr, SAADDRLEN(&param->sinsl));
}
if(param->preauth == 1 && param->srv->acl) param->dstindep = 0;
return 0;
}
@ -110,6 +111,7 @@ int doauth(struct clientparam * param){
struct auth *authfuncs;
int ret = 0;
if(param->preauth == 1 && !param->srv->acl) param->dstindep = 1;
for(authfuncs=param->srv->authfuncs; authfuncs; authfuncs=authfuncs->next){
res = authfuncs->authenticate?(*authfuncs->authenticate)(param):0;
if(!res) {
@ -148,7 +150,7 @@ int doauth(struct clientparam * param){
if(ret > 9) return ret;
}
if(!res){
ret = alwaysauth(param);
ret = (param->preauth == 2)? 0 : alwaysauth(param);
if (param->afterauthfilters){
FILTER_ACTION action;

View File

@ -9,6 +9,15 @@
#include "proxy.h"
#ifdef __linux__
#include <sched.h>
int switch_ns(struct srvparam *srv, int target_fd) {
if(target_fd < 0) return 0;
if(srv->saved_nsfd >= 0 && setns(srv->saved_nsfd, CLONE_NEWNET)) return -1;
return setns(target_fd, CLONE_NEWNET);
}
#endif
char * copyright = COPYRIGHT;

View File

@ -204,6 +204,10 @@ extern int timeouts[12];
int sockmap(struct clientparam * param, int timeo, int usesplice);
int udpsockmap(struct clientparam * param, int timeo);
int udpbind(struct clientparam * param);
#ifdef __linux__
int switch_ns(struct srvparam *srv, int target_fd);
#endif
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);

View File

@ -331,6 +331,9 @@ int MODULEMAINFUNC (int argc, char** argv){
" -s Use splice() - no filtering for data, off by default\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"
" association: 1 - log, 2 - authorize, 3 - both (default), 0 (same as -U) -\n"
" neither. The first destination is always authorized and logged\n"
#endif
"-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"
@ -601,6 +604,9 @@ int MODULEMAINFUNC (int argc, char** argv){
case 'C':
srv.halfclose = *(argv[i]+2)? atoi(argv[i]+2) : 0;
break;
case 'U':
srv.udpauth = *(argv[i]+2)? atoi(argv[i]+2) : 0;
break;
case 's':
#ifdef WITHSPLICE
if(isudp || srv.service == S_ADMIN)
@ -1253,6 +1259,7 @@ void srvinit(struct srvparam * srv, struct clientparam *param){
srv->usesplice = 0;
#endif
srv->halfclose = 1;
srv->udpauth = 3;
memset(param, 0, sizeof(struct clientparam));
param->srv = srv;
param->version = srv->version;
@ -1355,7 +1362,6 @@ void srvfree(struct srvparam * srv){
void freeparam(struct clientparam * param) {
if(param->res == 2) return;
if(param->srv){
if(param->srv->so.freefunc) param->srv->so.freefunc(param->sostate);
_3proxy_mutex_lock(&param->srv->counter_mutex);

View File

@ -7,15 +7,6 @@
*/
#include "proxy.h"
#ifdef __linux__
#include <sched.h>
static int switch_ns(struct srvparam *srv, int target_fd) {
if(target_fd < 0) return 0;
if(srv->saved_nsfd >= 0 && setns(srv->saved_nsfd, CLONE_NEWNET)) return -1;
return setns(target_fd, CLONE_NEWNET);
}
#endif
#define RETURN(xxx) { param->res = xxx; goto CLEANRET; }
@ -219,7 +210,11 @@ void * sockschild(struct clientparam* param) {
RETURN(997);
}
if((res = (*param->srv->authfunc)(param))) {
if(command == 3) param->preauth = 1;
res = (*param->srv->authfunc)(param);
param->preauth = 0;
if(command == 3 && res == 2) res = 0;
if(res) {
RETURN(res);
}
@ -238,18 +233,9 @@ void * sockschild(struct clientparam* param) {
#endif
if(command == 3) {
#ifdef __linux__
if(switch_ns(param->srv, param->srv->o_nsfd)) {RETURN(11);}
#endif
if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(&param->req), SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET) {RETURN (11);}
#ifdef _WIN32
{ unsigned long ul = 1; ioctlsocket(param->remsock, FIONBIO, &ul); }
#else
fcntl(param->remsock, F_SETFL, O_NONBLOCK | fcntl(param->remsock, F_GETFL));
#endif
if((res = udpbind(param))) {RETURN(res);}
}
if(command > 1) {
else if(command == 2) {
if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)&param->sinsl,SASIZE(&param->sinsl))) {
*SAPORT(&param->sinsl) = 0;
if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)&param->sinsl,SASIZE(&param->sinsl)))RETURN (12);
@ -260,6 +246,7 @@ fflush(stderr);
}
sasize = SASIZE(&param->sinsl);
param->srv->so._getsockname(param->sostate, param->remsock, (struct sockaddr *)&param->sinsl, &sasize);
}
if(command == 3) {
param->ctrlsock = param->clisock;
#ifdef __linux__
@ -284,7 +271,6 @@ fprintf(stderr, "%hu binded to communicate with client\n",
fflush(stderr);
#endif
}
}
param->res = 0;

View File

@ -552,6 +552,7 @@ struct srvparam {
int usesplice;
#endif
int halfclose;
int udpauth;
unsigned bufsize;
unsigned authcachetype, authcachetime;
unsigned logdumpsrv, logdumpcli;
@ -644,7 +645,9 @@ struct clientparam {
paused,
version,
connlim,
predatdone;
predatdone,
preauth,
dstindep;
unsigned char *hostname,
*username,
@ -679,6 +682,7 @@ struct clientparam {
PROXYSOCKADDRTYPE udp_relay[3];
int udp_nhops;
struct ace *lastace;
time_t time_start;
};

View File

@ -36,26 +36,19 @@ void * udppmchild(struct clientparam* param) {
int i;
int len = 0;
if(parsehostname((char *)param->srv->target, param, ntohs(param->srv->targetport))) { RETURN(201) }
if(parsehostname((char *)param->srv->target, param, ntohs(param->srv->targetport))) { RETURN(100) }
#ifndef NOIPV6
memcpy(&param->sinsl, *SAFAMILY(&param->req) == AF_INET6 ? (struct sockaddr *)&param->srv->extsa6 : (struct sockaddr *)&param->srv->extsa, SASIZE(&param->req));
param->sinsl = *SAFAMILY(&param->req) == AF_INET6? param->srv->extsa6 : param->srv->extsa;
#else
memcpy(&param->sinsl, (struct sockaddr *)&param->srv->extsa, SASIZE(&param->req));
param->sinsl = param->srv->extsa;
#endif
*SAPORT(&param->sinsl) = 0;
param->remsock = param->srv->so._socket(param->srv->so.state, SASOCK(&param->sinsl), SOCK_DGRAM, IPPROTO_UDP);
if(param->remsock == INVALID_SOCKET) { RETURN(202); }
if(param->srv->so._bind(param->srv->so.state, param->remsock, (struct sockaddr *)&param->sinsl, SASIZE(&param->sinsl))) { RETURN(203); }
#ifdef _WIN32
{ unsigned long ul2 = 1; ioctlsocket(param->remsock, FIONBIO, &ul2); }
#else
fcntl(param->remsock, F_SETFL, O_NONBLOCK | fcntl(param->remsock, F_GETFL));
#endif
memcpy(&param->sinsr, &param->req, sizeof(param->req));
param->sinsr = param->req;
param->operation = UDPASSOC;
authres = (*param->srv->authfunc)(param);
if(authres) { RETURN(authres); }
if((authres = udpbind(param))) { RETURN(authres); }
if(!param->srv->s_option)hashadd(&udp_table, param, &param, MAX_COUNTER_TIME);
if(!param->srvbuf){
if(!(param->srvbuf = malloc(UDPBUFSIZE)))RETURN(11);

View File

@ -8,6 +8,10 @@
#include "proxy.h"
/* space reserved in front of the received datagram to prepend the headers
of the second and the third hop of a SOCKS5 UDP chain */
#define UDPHDROFF 48
int socks5_udp_build_hdr(unsigned char *buf, PROXYSOCKADDRTYPE *addr)
{
buf[0] = buf[1] = buf[2] = 0;
@ -48,69 +52,192 @@ static int socks5_udp_skip_hdr(unsigned char *buf, int len)
* param->srv->s_option non-zero: return after first datagram sent to client
* param->ctrlsock TCP control socket from the client; INVALID_SOCKET if none.
*/
struct udppoll {
struct pollfd fds[4];
int nfds;
int cli, ctrl, ctrlsrv;
};
static void udpfds(struct clientparam *param, struct udppoll *p)
{
memset(p->fds, 0, sizeof(p->fds));
p->nfds = 0;
p->cli = p->ctrl = p->ctrlsrv = -1;
p->fds[p->nfds].fd = param->remsock; /* always index 0 */
p->fds[p->nfds].events = POLLIN;
p->nfds++;
if (!param->waitserver64) {
p->fds[p->nfds].fd = param->clisock;
p->fds[p->nfds].events = POLLIN;
p->cli = p->nfds++;
}
if (param->ctrlsock != INVALID_SOCKET) {
p->fds[p->nfds].fd = param->ctrlsock;
p->fds[p->nfds].events = POLLIN;
p->ctrl = p->nfds++;
}
if (param->ctrlsocksrv != INVALID_SOCKET) {
p->fds[p->nfds].fd = param->ctrlsocksrv;
p->fds[p->nfds].events = POLLIN;
p->ctrlsrv = p->nfds++;
}
}
static void udplog(struct clientparam *param)
{
unsigned char buf[400];
int len;
if(!param->srv->logfunc) return;
len = sprintf((char *)buf, "UDPMAP ");
if(param->hostname) len += sprintf((char *)buf + len, "%.256s", param->hostname);
else len += myinet_ntop(*SAFAMILY(&param->req), SAADDR(&param->req), (char *)buf + len, 64);
sprintf((char *)buf + len, ":%hu", ntohs(*SAPORT(&param->req)));
param->srv->logfunc(param, buf);
}
int udpbind(struct clientparam *param)
{
SOCKET s;
SASIZETYPE sasize;
#ifdef __linux__
if (switch_ns(param->srv, param->srv->o_nsfd)) return 11;
#endif
s = param->srv->so._socket(param->sostate, SASOCK(&param->sinsl), SOCK_DGRAM, IPPROTO_UDP);
#ifdef __linux__
if (switch_ns(param->srv, param->srv->i_nsfd)) {
if (s != INVALID_SOCKET) param->srv->so._closesocket(param->sostate, s);
return 11;
}
#endif
if (s == INVALID_SOCKET) return 11;
#ifdef _WIN32
{ unsigned long ul = 1; ioctlsocket(s, FIONBIO, &ul); }
#else
fcntl(s, F_SETFL, O_NONBLOCK | fcntl(s, F_GETFL));
#endif
param->remsock = s;
if (param->srv->so._bind(param->sostate, param->remsock,
(struct sockaddr *)&param->sinsl, SASIZE(&param->sinsl))) {
*SAPORT(&param->sinsl) = 0;
if (param->srv->so._bind(param->sostate, param->remsock,
(struct sockaddr *)&param->sinsl, SASIZE(&param->sinsl))) {
param->srv->so._closesocket(param->sostate, param->remsock);
param->remsock = INVALID_SOCKET;
return 12;
}
}
sasize = SASIZE(&param->sinsl);
param->srv->so._getsockname(param->sostate, param->remsock,
(struct sockaddr *)&param->sinsl, &sasize);
return 0;
}
static void udpreset(struct clientparam *param)
{
param->redirected = 0;
param->udp_nhops = 0;
memset(param->udp_relay, 0, sizeof(param->udp_relay));
#ifndef NOIPV6
param->sinsl = *SAFAMILY(&param->req) == AF_INET6? param->srv->extsa6 : param->srv->extsa;
#else
param->sinsl = param->srv->extsa;
#endif
param->sinsr = param->req;
}
/*
* Authorize param->req and (re)build the server side of the association:
* a parent proxy chain and/or an external address may be selected by the ACL.
* Returns the authorization result. param->remsock is INVALID_SOCKET if the
* socket can not be created, in this case the association can not continue.
*/
static int udpreconnect(struct clientparam *param)
{
int res;
if (param->ctrlsocksrv != INVALID_SOCKET) {
param->srv->so._closesocket(param->sostate, param->ctrlsocksrv);
param->ctrlsocksrv = INVALID_SOCKET;
}
if (param->remsock != INVALID_SOCKET) {
param->srv->so._closesocket(param->sostate, param->remsock);
param->remsock = INVALID_SOCKET;
}
udpreset(param);
res = (*param->srv->authfunc)(param);
if (res) {
if (param->ctrlsocksrv != INVALID_SOCKET) {
param->srv->so._closesocket(param->sostate, param->ctrlsocksrv);
param->ctrlsocksrv = INVALID_SOCKET;
}
if (param->remsock != INVALID_SOCKET) {
param->srv->so._closesocket(param->sostate, param->remsock);
param->remsock = INVALID_SOCKET;
}
udpreset(param);
}
if (udpbind(param)) return res? res : 11;
return res;
}
int udpsockmap(struct clientparam *param, int timeo)
{
PROXYSOCKADDRTYPE sin;
PROXYSOCKADDRTYPE cliaddr;
PROXYSOCKADDRTYPE from;
struct pollfd fds[4];
PROXYSOCKADDRTYPE lastdst;
struct udppoll p;
SASIZETYPE sasize;
int len, res, nfds;
int nhops = param->udp_nhops;
int clisock_idx = -1, ctrlsock_idx = -1, ctrlsocksrv_idx = -1;
int len, res, nhops;
int firstpacket = 1;
int havedst = 0, lastres = 0;
char lastname[256] = "";
struct ace *lastace = NULL;
if(param->srv->service == S_UDPPM) nhops++;
if (param->srvbufsize < UDPBUFSIZE) {
unsigned char *newbuf = realloc(param->srvbuf, UDPBUFSIZE);
if (!newbuf) return 21;
param->srvbuf = newbuf;
param->srvbufsize = UDPBUFSIZE;
}
sin = param->sincr;
/* Build poll array once — sockets don't change across iterations */
nfds = 0;
fds[nfds].fd = param->remsock; /* always index 0 */
fds[nfds].events = POLLIN;
nfds++;
if (!param->waitserver64) {
fds[nfds].fd = param->clisock;
fds[nfds].events = POLLIN;
clisock_idx = nfds++;
cliaddr = param->sincr;
if(param->ctrlsock != INVALID_SOCKET){
sasize = sizeof(cliaddr);
param->srv->so._getpeername(param->sostate, param->ctrlsock, (struct sockaddr *)&cliaddr, &sasize);
}
sin = cliaddr;
if (param->ctrlsock != INVALID_SOCKET) {
fds[nfds].fd = param->ctrlsock;
fds[nfds].events = POLLIN;
ctrlsock_idx = nfds++;
}
if (param->ctrlsocksrv != INVALID_SOCKET) {
fds[nfds].fd = param->ctrlsocksrv;
fds[nfds].events = POLLIN;
ctrlsocksrv_idx = nfds++;
}
nhops = param->udp_nhops;
if(param->srv->service == S_UDPPM) nhops++;
udpfds(param, &p);
for (;;) {
res = param->srv->so._poll(param->sostate, fds, nfds, timeo * 1000);
res = param->srv->so._poll(param->sostate, p.fds, p.nfds, timeo * 1000);
if (res < 0) return 481;
if (res == 0) return 92;
/* datagram from client */
if (clisock_idx >= 0 && fds[clisock_idx].revents) {
int recvoff = 0, k;
if (p.cli >= 0 && p.fds[p.cli].revents) {
unsigned char *base = param->srvbuf + UDPHDROFF;
PROXYSOCKADDRTYPE dst;
char dstnamebuf[256];
char *dstname = NULL;
int i, k, w, off;
sasize = sizeof(sin);
for (k = 1; k < nhops; k++)
recvoff += 4 + (int)SAADDRLEN(&param->udp_relay[k]) + 2;
len = param->srv->so._recvfrom(param->sostate, param->clisock,
(char *)param->srvbuf + recvoff, UDPBUFSIZE - recvoff,
(char *)base, UDPBUFSIZE - UDPHDROFF,
0, (struct sockaddr *)&sin, &sasize);
if (len < 0 && (errno == EAGAIN || errno == EINTR)) continue;
if (len <= 0) return 482;
if (SAADDRLEN(&sin) != SAADDRLEN(&param->sincr) ||
memcmp(SAADDR(&sin), SAADDR(&param->sincr), SAADDRLEN(&sin)))
if (SAADDRLEN(&sin) != SAADDRLEN(&cliaddr) ||
memcmp(SAADDR(&sin), SAADDR(&cliaddr), SAADDRLEN(&sin)))
continue;
if (firstpacket) {
if (!SAISNULL(&param->req) && *SAPORT(&param->req) &&
@ -118,58 +245,118 @@ int udpsockmap(struct clientparam *param, int timeo)
!memcmp(SAADDR(&param->req), SAADDR(&sin), SAADDRLEN(&param->req)) &&
memcmp(SAPORT(&param->req), SAPORT(&sin), 2))
continue;
param->sincr = sin;
cliaddr = sin;
firstpacket = 0;
} else if (memcmp(SAPORT(&sin), SAPORT(&param->sincr), 2)) {
} else if (memcmp(SAPORT(&sin), SAPORT(&cliaddr), 2)) {
continue;
}
if(param->bandlimfunc && (*param->bandlimfunc)(param, 0, len)) continue;
if (nhops == 0) {
int i;
if (len < 10 || param->srvbuf[0] || param->srvbuf[1] || param->srvbuf[2])
return 483;
switch (param->srvbuf[3]) {
if (len < 10 || base[0] || base[1] || base[2]) return 483;
memset(&dst, 0, sizeof(dst));
switch (base[3]) {
case 1:
*SAFAMILY(&param->sinsr) = AF_INET;
memcpy(SAADDR(&param->sinsr), param->srvbuf + 4, 4);
*SAFAMILY(&dst) = AF_INET;
memcpy(SAADDR(&dst), base + 4, 4);
i = 8;
break;
case 4:
if (len < 22) return 484;
*SAFAMILY(&param->sinsr) = AF_INET6;
memcpy(SAADDR(&param->sinsr), param->srvbuf + 4, 16);
*SAFAMILY(&dst) = AF_INET6;
memcpy(SAADDR(&dst), base + 4, 16);
i = 20;
break;
case 3: {
int sz = param->srvbuf[4], j;
int sz = base[4];
if (len < 7 + sz) return 485;
for (j = 4; j < 4 + sz; j++) param->srvbuf[j] = param->srvbuf[j + 1];
param->srvbuf[4 + sz] = 0;
memcpy(dstnamebuf, base + 5, sz);
dstnamebuf[sz] = 0;
dstname = dstnamebuf;
i = 5 + sz;
if (!getip46(param->srv->family, param->srvbuf + 4,
(struct sockaddr *)&param->sinsr))
return 100;
if (!getip46(param->srv->family, (unsigned char *)dstnamebuf,
(struct sockaddr *)&dst)) {
if (!nhops) return 100;
memset(&dst, 0, sizeof(dst));
*SAFAMILY(&dst) = AF_INET;
}
break;
}
default: return 997;
}
memcpy(SAPORT(&param->sinsr), param->srvbuf + i, 2);
memcpy(SAPORT(&dst), base + i, 2);
i += 2;
if (!havedst
|| SAADDRLEN(&lastdst) != SAADDRLEN(&dst)
|| memcmp(SAADDR(&lastdst), SAADDR(&dst), SAADDRLEN(&dst))
|| memcmp(SAPORT(&lastdst), SAPORT(&dst), 2)
|| strncmp(lastname, dstname? dstname : "", sizeof(lastname) - 1)) {
int ares = 0, reconnect = 0;
if ((param->srv->udpauth & 1) && havedst && !lastres) {
PROXYSOCKADDRTYPE newdst = dst;
param->req = lastdst;
if (param->hostname) free(param->hostname);
param->hostname = *lastname? (unsigned char *)strdup(lastname) : NULL;
udplog(param);
dst = newdst;
}
if (param->hostname) free(param->hostname);
param->hostname = dstname? (unsigned char *)strdup(dstname) : NULL;
param->req = dst;
if (!nhops) param->sinsr = dst;
if (!havedst) reconnect = 1;
else if ((param->srv->udpauth & 2) && !param->dstindep) {
param->preauth = 2;
ares = (*param->srv->authfunc)(param);
param->preauth = 0;
if (ares == 2) {
if (param->lastace != lastace) reconnect = 1;
ares = 0;
}
else if (!ares && (param->redirected ||
(lastace && lastace->chains))) reconnect = 1;
}
if (reconnect) {
ares = udpreconnect(param);
if (param->remsock == INVALID_SOCKET) return ares;
nhops = param->udp_nhops;
if(param->srv->service == S_UDPPM) nhops++;
udpfds(param, &p);
}
lastres = ares;
lastdst = dst;
strncpy(lastname, dstname? dstname : "", sizeof(lastname) - 1);
lastname[sizeof(lastname) - 1] = 0;
havedst = 1;
if (!ares) lastace = param->lastace;
else if (param->srv->udpauth & 1) {
param->res = ares;
udplog(param);
param->res = 0;
}
}
if (lastres) continue;
if (!nhops) {
if (len > i) {
param->srv->so._sendto(param->sostate, param->remsock,
(char *)param->srvbuf + i, len - i, 0,
(char *)base + i, len - i, 0,
(struct sockaddr *)&param->sinsr, SASIZE(&param->sinsr));
param->statscli64 += (len - i);
param->nwrites++;
}
} else {
int off = 0;
off = 0;
for (k = 1; k < nhops; k++)
off += socks5_udp_build_hdr(param->srvbuf + off, &param->udp_relay[k]);
off += 4 + (int)SAADDRLEN(&param->udp_relay[k]) + 2;
for (k = 1, w = UDPHDROFF - off; k < nhops; k++)
w += socks5_udp_build_hdr(param->srvbuf + w, &param->udp_relay[k]);
param->srv->so._sendto(param->sostate, param->remsock,
(char *)param->srvbuf, off + len, 0,
(char *)param->srvbuf + (UDPHDROFF - off), off + len, 0,
(struct sockaddr *)&param->udp_relay[0], SASIZE(&param->udp_relay[0]));
param->statscli64 += len;
param->nwrites++;
@ -177,11 +364,10 @@ int udpsockmap(struct clientparam *param, int timeo)
}
/* datagram from server / parent relay */
if (fds[0].revents) {
int hdrsize = (nhops == 0) ? 4 + (int)SAADDRLEN(&param->sinsr) + 2 : 0;
if (p.fds[0].revents) {
int hdrsize = (nhops == 0) ? 4 + (int)SAADDRLEN(&param->sinsl) + 2 : 0;
int sendoff = 0, sendlen;
sasize = sizeof(from);
if (hdrsize > UDPBUFSIZE) return 468;
len = param->srv->so._recvfrom(param->sostate, param->remsock,
(char *)param->srvbuf + hdrsize, UDPBUFSIZE - hdrsize, 0,
(struct sockaddr *)&from, &sasize);
@ -201,9 +387,9 @@ int udpsockmap(struct clientparam *param, int timeo)
sendlen = len;
if (nhops == 0) {
param->srvbuf[0] = param->srvbuf[1] = param->srvbuf[2] = 0;
param->srvbuf[3] = (*SAFAMILY(&param->sinsr) == AF_INET) ? 1 : 4;
memcpy(param->srvbuf + 4, SAADDR(&param->sinsr), SAADDRLEN(&param->sinsr));
memcpy(param->srvbuf + 4 + SAADDRLEN(&param->sinsr), SAPORT(&param->sinsr), 2);
param->srvbuf[3] = (*SAFAMILY(&from) == AF_INET) ? 1 : 4;
memcpy(param->srvbuf + 4, SAADDR(&from), SAADDRLEN(&param->sinsl));
memcpy(param->srvbuf + 4 + SAADDRLEN(&param->sinsl), SAPORT(&from), 2);
sendlen = len + hdrsize;
} else if (nhops >= 2) {
int off = 0, k;
@ -224,8 +410,8 @@ int udpsockmap(struct clientparam *param, int timeo)
if (param->srv->s_option && param->srv->service == S_UDPPM) return 0;
}
if ((ctrlsock_idx >= 0 && fds[ctrlsock_idx].revents) ||
(ctrlsocksrv_idx >= 0 && fds[ctrlsocksrv_idx].revents)) return 0;
if ((p.ctrl >= 0 && p.fds[p.ctrl].revents) ||
(p.ctrlsrv >= 0 && p.fds[p.ctrlsrv].revents)) return 0;
}
return 0;
}