From bebc3894e2647cc80afe38d8cbd34b0b540b68a0 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Tue, 11 Aug 2026 19:02:54 +0300 Subject: [PATCH] Support acls for UDP ASSOC, fix udppm via SOCKSv5 --- doc/html/highload.html | 133 +++++++++++++++ doc/html/howtoe.html | 76 +++++++++ doc/html/howtor.html | 79 +++++++++ doc/html/securityen.html | 20 +++ man/3proxy.cfg.5 | 61 +++++++ src/acl.c | 20 ++- src/auth.c | 4 +- src/common.c | 9 + src/proxy.h | 4 + src/proxymain.c | 8 +- src/socks.c | 32 +--- src/structures.h | 6 +- src/udppm.c | 17 +- src/udpsockmap.c | 354 +++++++++++++++++++++++++++++---------- 14 files changed, 699 insertions(+), 124 deletions(-) diff --git a/doc/html/highload.html b/doc/html/highload.html index 3f5d258..978af8f 100644 --- a/doc/html/highload.html +++ b/doc/html/highload.html @@ -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). +

Use the Authentication Cache

+ +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: +
+authcache <cachetype> <cachetime> <cachesize>
+
+'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. + +

There are two cached types: +

+ +

'cacheacl' does not work with parent proxies. 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: +

+authcache user,pass 60
+auth cache strong
+allow user1
+parent 1000 socks5 10.0.0.1 1080
+proxy
+
+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: +
+authcache user,pass,ext 60
+auth cacheacl strong
+allow user1
+parent 1000 extip 1.2.3.4 0
+proxy
+
+Without 'ext' in the key the external address falls back to the default one on +every cache hit. + +

The key must match the ACL. 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: +

+allow user1,user2 * * 80,443
+deny *
+
+the key needs 'user', 'pass' and 'dstport': +
+users $/etc/3proxy/passwd
+authcache user,pass,dstport 60 65536
+auth cacheacl strong
+allow user1,user2 * * 80,443
+deny *
+proxy
+
+ +

With RADIUS the round trip is the expensive part, so cache by whatever +identifies the client: +

+radius secret 1.2.3.4
+authcache ip,user,pass 600
+auth cache radius
+proxy
+
+'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. + +

Choose 'cachetime' to balance the load against how quickly a password or ACL +change has to take effect. Cache entries are not invalidated by a +configuration reload, so a long lifetime keeps applying the old decision to +existing keys after the new configuration is loaded. + +

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. + +

Tune UDP Destination Handling with '-U'

+ +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: +
+socks -U3    # default: log and authorize every new destination
+socks -U2    # authorize, do not log
+socks -U1    # log, do not authorize
+socks -U0    # neither (same as bare -U)
+
+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. + +

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. + +

For high load: +

+

Avoid Large Lists

Currently, 3proxy is not optimized to use large ACLs, user lists, etc. All lists diff --git a/doc/html/howtoe.html b/doc/html/howtoe.html index 88cf80e..117e460 100644 --- a/doc/html/howtoe.html +++ b/doc/html/howtoe.html @@ -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%.

+
  • How to apply ACLs to UDP traffic +

    +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. +

    +

    +Differences from TCP: +

    + +

    +Limit the destinations a client may send datagrams to: +

    +auth iponly
    +allow * * 8.8.8.8 53
    +deny *
    +socks -p1080
    +
    +

    +The ASSOCIATE request succeeds, datagrams to 8.8.8.8:53 are relayed, datagrams +to any other destination are dropped. +

    +

    +The same with 'strong' (or RADIUS, or plugin) authentication, where every new +destination would otherwise be authenticated again: +

    +users user1:CL:password
    +authcache user,pass,dstaddr,dstport 60
    +auth cacheacl strong
    +allow user1 * 8.8.8.8 53
    +deny *
    +socks -p1080
    +
    +

    +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. +

  • How to limit bandwidth

    3proxy supports bandwidth filters. Use the bandlimin/bandlimout and diff --git a/doc/html/howtor.html b/doc/html/howtor.html index fa364cc..2e55ad0 100644 --- a/doc/html/howtor.html +++ b/doc/html/howtor.html @@ -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

    +
  • Как применять ACL к UDP трафику +

    +Запрос SOCKSv5 UDP ASSOCIATE лишь просит 3proxy открыть порт для ретрансляции +и не содержит адреса назначения. Адрес назначения берется из каждой +датаграммы, отправленной клиентом, поэтому одна ассоциация может обращаться +к любому числу адресов назначения. 3proxy авторизует адрес назначения каждой +датаграммы, поэтому ACL, ограничивающие адрес, имя хоста или порт +назначения, применяются и к UDP трафику. +

    +

    +Отличия от TCP: +

    + +

    +Ограничить адреса назначения, куда клиент может отправлять датаграммы: +

    +auth iponly
    +allow * * 8.8.8.8 53
    +deny *
    +socks -p1080
    +
    +

    +Запрос ASSOCIATE выполняется успешно, датаграммы к 8.8.8.8:53 передаются, +датаграммы к любому другому адресу назначения отбрасываются. +

    +

    +То же самое с аутентификацией 'strong' (или RADIUS, или через плагин), при +которой иначе каждый новый адрес назначения аутентифицировался бы заново: +

    +users user1:CL:password
    +authcache user,pass,dstaddr,dstport 60
    +auth cacheacl strong
    +allow user1 * 8.8.8.8 53
    +deny *
    +socks -p1080
    +
    +

    +При 'cacheacl' результат проверки ACL не вычисляется заново при попадании +в кэш, поэтому каждое поле, ограничиваемое в ACL, должно входить в ключ +кэша: 'user' и 'pass' для списка пользователей, 'dstaddr' и 'dstport' для списка +адресов и списка портов назначения, 'ip' для списка источников, 'dsthost' +для имен хостов в списке назначения, 'dstoper' для списка операций. Без +адреса назначения в ключе результат, закэшированный для одного адреса +назначения, применяется к любому другому, и адрес назначения не +проверяется вообще. Используйте 'cache' вместо 'cacheacl', если ACL могут +изменяться в течение времени жизни кэша или если используется вышестоящий +прокси: при попадании в кэш 'cacheacl' обход ACL не выполняется, поэтому +вышестоящий прокси не выбирается и датаграммы отправляются напрямую. +

  • Как ограничивать скорости приема

    3proxy позволяет устанавливать фильтры ширины потребляемого канала. Для этого diff --git a/doc/html/securityen.html b/doc/html/securityen.html index 81ff3e3..95451d9 100644 --- a/doc/html/securityen.html +++ b/doc/html/securityen.html @@ -25,6 +25,26 @@ with an unreachable DNS server (because gethostbyname will block other threads). user requests can be logged.

  • Use -xyz+A character filtering sequences for 'logformat', especially with ODBC logging, to prevent SQL and log record injections. +
  • 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. +
  • '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. +
  • 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.
  • Immediately report all service crashes to the developers.
  • Participate in code audit :) diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5 index 76fa4f7..416bb17 100644 --- a/man/3proxy.cfg.5 +++ b/man/3proxy.cfg.5 @@ -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 diff --git a/src/acl.c b/src/acl.c index 207a470..3c78ad6 100644 --- a/src/acl.c +++ b/src/acl.c @@ -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(¶m->req) || param->operation==BIND)) || (acentry->dstnames && param->hostname)) { + if(preauth && (acentry->dst || acentry->dstnames)) { + dstdep = 1; + } + else if((acentry->dst && (!SAISNULL(¶m->req) || param->operation==BIND)) || (acentry->dstnames && param->hostname)) { for(ipentry = acentry->dst; ipentry; ipentry = ipentry->next) if(IPInentry((struct sockaddr *)¶m->req, ipentry)) { break; @@ -93,7 +98,10 @@ int ACLmatches(struct ace* acentry, struct clientparam * param){ } if(!ipentry && !hstentry) return 0; } - if(acentry->ports && (*SAPORT(¶m->req) || param->operation == BIND)) { + if(preauth && acentry->ports) { + dstdep = 1; + } + else if(acentry->ports && (*SAPORT(¶m->req) || param->operation == BIND)) { for (portentry = acentry->ports; portentry; portentry = portentry->next) if(ntohs(*SAPORT(¶m->req)) >= portentry->startport && ntohs(*SAPORT(¶m->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; } } diff --git a/src/auth.c b/src/auth.c index 33bef3b..f986c46 100644 --- a/src/auth.c +++ b/src/auth.c @@ -102,6 +102,7 @@ int cacheauth(struct clientparam * param){ *(SAFAMILY(¶m->sinsl)) = ac.sinsl_family; memcpy(SAADDR(¶m->sinsl), ac.sinsl_addr, SAADDRLEN(¶m->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; diff --git a/src/common.c b/src/common.c index 0c21ff2..55af748 100644 --- a/src/common.c +++ b/src/common.c @@ -9,6 +9,15 @@ #include "proxy.h" +#ifdef __linux__ +#include + +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; diff --git a/src/proxy.h b/src/proxy.h index d5b1ec9..5702012 100644 --- a/src/proxy.h +++ b/src/proxy.h @@ -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); diff --git a/src/proxymain.c b/src/proxymain.c index ec65ca9..b050930 100644 --- a/src/proxymain.c +++ b/src/proxymain.c @@ -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(¶m->srv->counter_mutex); diff --git a/src/socks.c b/src/socks.c index 1874745..f60b0dc 100644 --- a/src/socks.c +++ b/src/socks.c @@ -7,15 +7,6 @@ */ #include "proxy.h" -#ifdef __linux__ -#include - -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(¶m->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 *)¶m->sinsl,SASIZE(¶m->sinsl))) { *SAPORT(¶m->sinsl) = 0; if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)¶m->sinsl,SASIZE(¶m->sinsl)))RETURN (12); @@ -260,7 +246,8 @@ fflush(stderr); } sasize = SASIZE(¶m->sinsl); param->srv->so._getsockname(param->sostate, param->remsock, (struct sockaddr *)¶m->sinsl, &sasize); - if(command == 3) { + } + if(command == 3) { param->ctrlsock = param->clisock; #ifdef __linux__ if(switch_ns(param->srv, param->srv->i_nsfd)) {RETURN(11);} @@ -283,7 +270,6 @@ fprintf(stderr, "%hu binded to communicate with client\n", ); fflush(stderr); #endif - } } param->res = 0; diff --git a/src/structures.h b/src/structures.h index 8d7eeb2..21cea79 100644 --- a/src/structures.h +++ b/src/structures.h @@ -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; }; diff --git a/src/udppm.c b/src/udppm.c index 6d6b4bb..a0567c0 100644 --- a/src/udppm.c +++ b/src/udppm.c @@ -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(¶m->sinsl, *SAFAMILY(¶m->req) == AF_INET6 ? (struct sockaddr *)¶m->srv->extsa6 : (struct sockaddr *)¶m->srv->extsa, SASIZE(¶m->req)); + param->sinsl = *SAFAMILY(¶m->req) == AF_INET6? param->srv->extsa6 : param->srv->extsa; #else - memcpy(¶m->sinsl, (struct sockaddr *)¶m->srv->extsa, SASIZE(¶m->req)); + param->sinsl = param->srv->extsa; #endif *SAPORT(¶m->sinsl) = 0; - param->remsock = param->srv->so._socket(param->srv->so.state, SASOCK(¶m->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 *)¶m->sinsl, SASIZE(¶m->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(¶m->sinsr, ¶m->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, ¶m, MAX_COUNTER_TIME); if(!param->srvbuf){ if(!(param->srvbuf = malloc(UDPBUFSIZE)))RETURN(11); diff --git a/src/udpsockmap.c b/src/udpsockmap.c index af2c39f..d3a0f45 100644 --- a/src/udpsockmap.c +++ b/src/udpsockmap.c @@ -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(¶m->req), SAADDR(¶m->req), (char *)buf + len, 64); + sprintf((char *)buf + len, ":%hu", ntohs(*SAPORT(¶m->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(¶m->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 *)¶m->sinsl, SASIZE(¶m->sinsl))) { + *SAPORT(¶m->sinsl) = 0; + if (param->srv->so._bind(param->sostate, param->remsock, + (struct sockaddr *)¶m->sinsl, SASIZE(¶m->sinsl))) { + param->srv->so._closesocket(param->sostate, param->remsock); + param->remsock = INVALID_SOCKET; + return 12; + } + } + sasize = SASIZE(¶m->sinsl); + param->srv->so._getsockname(param->sostate, param->remsock, + (struct sockaddr *)¶m->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(¶m->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; - - if(param->srv->service == S_UDPPM) nhops++; + int havedst = 0, lastres = 0; + char lastname[256] = ""; + struct ace *lastace = NULL; + 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(¶m->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(¶m->sincr) || - memcmp(SAADDR(&sin), SAADDR(¶m->sincr), SAADDRLEN(&sin))) + if (SAADDRLEN(&sin) != SAADDRLEN(&cliaddr) || + memcmp(SAADDR(&sin), SAADDR(&cliaddr), SAADDRLEN(&sin))) continue; if (firstpacket) { if (!SAISNULL(¶m->req) && *SAPORT(¶m->req) && @@ -118,70 +245,129 @@ int udpsockmap(struct clientparam *param, int timeo) !memcmp(SAADDR(¶m->req), SAADDR(&sin), SAADDRLEN(¶m->req)) && memcmp(SAPORT(¶m->req), SAPORT(&sin), 2)) continue; - param->sincr = sin; + cliaddr = sin; firstpacket = 0; - } else if (memcmp(SAPORT(&sin), SAPORT(¶m->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]) { - case 1: - *SAFAMILY(¶m->sinsr) = AF_INET; - memcpy(SAADDR(¶m->sinsr), param->srvbuf + 4, 4); - i = 8; - break; - case 4: - if (len < 22) return 484; - *SAFAMILY(¶m->sinsr) = AF_INET6; - memcpy(SAADDR(¶m->sinsr), param->srvbuf + 4, 16); - i = 20; - break; - case 3: { - int sz = param->srvbuf[4], j; - 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; - i = 5 + sz; - if (!getip46(param->srv->family, param->srvbuf + 4, - (struct sockaddr *)¶m->sinsr)) - return 100; - break; + if (len < 10 || base[0] || base[1] || base[2]) return 483; + memset(&dst, 0, sizeof(dst)); + switch (base[3]) { + case 1: + *SAFAMILY(&dst) = AF_INET; + memcpy(SAADDR(&dst), base + 4, 4); + i = 8; + break; + case 4: + if (len < 22) return 484; + *SAFAMILY(&dst) = AF_INET6; + memcpy(SAADDR(&dst), base + 4, 16); + i = 20; + break; + case 3: { + int sz = base[4]; + if (len < 7 + sz) return 485; + memcpy(dstnamebuf, base + 5, sz); + dstnamebuf[sz] = 0; + dstname = dstnamebuf; + i = 5 + sz; + if (!getip46(param->srv->family, (unsigned char *)dstnamebuf, + (struct sockaddr *)&dst)) { + if (!nhops) return 100; + memset(&dst, 0, sizeof(dst)); + *SAFAMILY(&dst) = AF_INET; } - default: return 997; + break; + } + default: return 997; + } + 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; } - memcpy(SAPORT(¶m->sinsr), param->srvbuf + i, 2); - i += 2; + 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, - (struct sockaddr *)¶m->sinsr, SASIZE(¶m->sinsr)); + (char *)base + i, len - i, 0, + (struct sockaddr *)¶m->sinsr, SASIZE(¶m->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, ¶m->udp_relay[k]); + off += 4 + (int)SAADDRLEN(¶m->udp_relay[k]) + 2; + for (k = 1, w = UDPHDROFF - off; k < nhops; k++) + w += socks5_udp_build_hdr(param->srvbuf + w, ¶m->udp_relay[k]); param->srv->so._sendto(param->sostate, param->remsock, - (char *)param->srvbuf, off + len, 0, - (struct sockaddr *)¶m->udp_relay[0], SASIZE(¶m->udp_relay[0])); + (char *)param->srvbuf + (UDPHDROFF - off), off + len, 0, + (struct sockaddr *)¶m->udp_relay[0], SASIZE(¶m->udp_relay[0])); param->statscli64 += len; param->nwrites++; } } /* datagram from server / parent relay */ - if (fds[0].revents) { - int hdrsize = (nhops == 0) ? 4 + (int)SAADDRLEN(¶m->sinsr) + 2 : 0; + if (p.fds[0].revents) { + int hdrsize = (nhops == 0) ? 4 + (int)SAADDRLEN(¶m->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(¶m->sinsr) == AF_INET) ? 1 : 4; - memcpy(param->srvbuf + 4, SAADDR(¶m->sinsr), SAADDRLEN(¶m->sinsr)); - memcpy(param->srvbuf + 4 + SAADDRLEN(¶m->sinsr), SAPORT(¶m->sinsr), 2); + param->srvbuf[3] = (*SAFAMILY(&from) == AF_INET) ? 1 : 4; + memcpy(param->srvbuf + 4, SAADDR(&from), SAADDRLEN(¶m->sinsl)); + memcpy(param->srvbuf + 4 + SAADDRLEN(¶m->sinsl), SAPORT(&from), 2); sendlen = len + hdrsize; } else if (nhops >= 2) { int off = 0, k; @@ -219,13 +405,13 @@ int udpsockmap(struct clientparam *param, int timeo) } if (sendlen > 0) param->srv->so._sendto(param->sostate, param->clisock, - (char *)param->srvbuf + sendoff, sendlen, 0, - (struct sockaddr *)&sin, SASIZE(&sin)); + (char *)param->srvbuf + sendoff, sendlen, 0, + (struct sockaddr *)&sin, SASIZE(&sin)); 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; }