diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5 index 416bb17..6f88c67 100644 --- a/man/3proxy.cfg.5 +++ b/man/3proxy.cfg.5 @@ -879,6 +879,10 @@ with probability of 0.7) for outgoing web connections. Chains are only applied t type is one of: .br \fBextip\fR does not actually redirect the request; it sets the external address for this request to \fI\fR. It can be chained with another parent type. It's useful to set the external IP based on ACL or make it random. +.br + \fBextport\fR does not redirect the request; it sets the range the local port of outgoing connections is taken from, given as \fIFIRST-LAST\fR inclusive in place of the port argument, with 0.0.0.0 as the address, for example \fBparent 1000 extport 0.0.0.0 40000-40100\fR. Where the system can be asked to pick the port itself (Linux \fBIP_LOCAL_PORT_RANGE\fR) it does, otherwise a port is picked at random from the range and retried if it is already in use, up to ten times. It can be chained with another parent type, and the access rule it belongs to decides which requests it applies to, so \fBallow * * * * UDPASSOC\fR followed by \fBparent 1000 extport 0.0.0.0 40000-40100\fR limits it to UDP associations. The range is applied when the outgoing connection is made, so a kept alive connection carrying several requests uses the rule that matched when it was opened. +.br + \fBintport\fR is the same for sockets bound on the side facing the client: the port a UDP association tells the client to send its datagrams to, and the FTP proxy data connection. .br \fBtcp\fR simply redirect connection. TCP is always last in chain. This type of proxy is a simple TCP redirection, it does not support parent authentication. .br diff --git a/src/acl.c b/src/acl.c index 3c78ad6..53f50ca 100644 --- a/src/acl.c +++ b/src/acl.c @@ -164,7 +164,10 @@ int checkACL(struct clientparam * param){ continue; } param->lastace = acentry; - if(param->preauth) return 2; + if(param->preauth) { + applyportranges(param, acentry); + return 2; + } if((param->operation == UDPASSOC)? (param->ctrlsocksrv != INVALID_SOCKET) : (param->remsock != INVALID_SOCKET)) { return 0; } diff --git a/src/common.c b/src/common.c index ea5e472..1502546 100644 --- a/src/common.c +++ b/src/common.c @@ -746,7 +746,7 @@ int doconnect(struct clientparam * param){ #ifdef WITH_UN if(*SAFAMILY(¶m->sinsl) != AF_UNIX) #endif - if(param->srv->so._bind(param->sostate, param->remsock, (struct sockaddr*)¶m->sinsl, SASIZE(¶m->sinsl))==-1) { + if(bindwithrange(param, param->remsock, ¶m->sinsl, param->extport)==-1) { return 12; } @@ -767,6 +767,46 @@ int doconnect(struct clientparam * param){ return 0; } +/* Number of ports tried before giving up when the range has to be searched by + * hand. The kernel option picks a free port itself and needs no retries. */ +#define RANGETRIES 10 + +/* Bind sock to sa, taking the local port from the range if one is set. The + * range is packed as first | last << 16. + * + * IP_LOCAL_PORT_RANGE leaves the choice to the kernel, which knows which ports + * are free. Where the option does not exist, or the kernel refuses it, or the + * address family is not one it covers, pick a port at random instead and retry + * on failure, since the one picked may already be taken. + */ +int bindwithrange(struct clientparam *param, SOCKET sock, PROXYSOCKADDRTYPE *sa, uint32_t range) +{ + uint16_t first, last; + int i; + + if(!range) return param->srv->so._bind(param->sostate, sock, (struct sockaddr *)sa, SASIZE(sa)); + +#ifdef IP_LOCAL_PORT_RANGE + if(*SAFAMILY(sa) == AF_INET && + !param->srv->so._setsockopt(param->sostate, sock, IPPROTO_IP, IP_LOCAL_PORT_RANGE, + (char *)&range, sizeof(range))){ + *SAPORT(sa) = 0; + return param->srv->so._bind(param->sostate, sock, (struct sockaddr *)sa, SASIZE(sa)); + } +#endif + + first = (uint16_t)(range & 0xffff); + last = (uint16_t)(range >> 16); + + for(i = 0; i < RANGETRIES; i++){ + *SAPORT(sa) = htons((uint16_t)(first + (myrand() % (unsigned)(last - first + 1)))); + if(!param->srv->so._bind(param->sostate, sock, (struct sockaddr *)sa, SASIZE(sa))) return 0; + } + + *SAPORT(sa) = 0; + return -1; +} + int scanaddr(const unsigned char *s, uint32_t * ip, uint32_t * mask) { unsigned d1, d2, d3, d4, m; int res; diff --git a/src/conf.c b/src/conf.c index fc74b22..aac6317 100644 --- a/src/conf.c +++ b/src/conf.c @@ -767,12 +767,32 @@ struct redirdesc redirs[] = { {R_SOCKS5B, "socks5b", sockschild}, {R_ADMIN, "admin", adminchild}, {R_EXTIP, "extip", NULL}, + {R_EXTPORT, "extport", NULL}, + {R_INTPORT, "intport", NULL}, {R_TLS, "tls", tlsprchild}, {R_HA, "ha", NULL}, {R_DNS, "dns", dnsprchild}, {0, NULL, NULL} }; +/* Parses an inclusive FIRST-LAST local port range into first | last << 16. */ +static int parserange(unsigned char *arg, uint32_t *range) +{ + char *end; + unsigned long first, last; + + errno = 0; + first = strtoul((char *)arg, &end, 10); + if(errno || *end != '-' || !first || first > 65535) return 1; + + errno = 0; + last = strtoul(end + 1, &end, 10); + if(errno || *end || !last || last > 65535 || last < first) return 1; + + *range = (uint32_t)first | ((uint32_t)last << 16); + return 0; +} + static int h_parent(int argc, unsigned char **argv){ struct ace *acl = NULL; struct chain *chains; @@ -838,7 +858,21 @@ static int h_parent(int argc, unsigned char **argv){ *cidr = '/'; chains->cidr = atoi(cidr + 1); } - *SAPORT(&chains->addr) = htons((uint16_t)atoi((char *)argv[4])); + if(chains->type == R_EXTPORT || chains->type == R_INTPORT){ + if(!SAISNULL(&chains->addr)){ + fprintf(stderr, "Chaining error: chain type (%s) sets a local port range, it requires 0.0.0.0 as address on line %d\n", argv[2], linenum); + free(chains->exthost); + free(chains); + return(4); + } + if(parserange(argv[4], &chains->range)){ + fprintf(stderr, "Chaining error: bad port range (%s) on line %d\n", argv[4], linenum); + free(chains->exthost); + free(chains); + return(3); + } + } + else *SAPORT(&chains->addr) = htons((uint16_t)atoi((char *)argv[4])); switch(chains->type){ case R_POP3: case R_SMTP: diff --git a/src/dnspr.c b/src/dnspr.c index d6372e7..c5c06b5 100644 --- a/src/dnspr.c +++ b/src/dnspr.c @@ -149,7 +149,7 @@ void * dnsprchild(struct clientparam* param) { } memset(¶m->sinsl, 0, sizeof(param->sinsl)); *SAFAMILY(¶m->sinsl) = *SAFAMILY(&nservers[0].addr); - if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)¶m->sinsl,SASIZE(¶m->sinsl))) { + if(bindwithrange(param, param->remsock, ¶m->sinsl, param->extport)) { RETURN(819); } param->sinsr = nservers[0].addr; diff --git a/src/ftppr.c b/src/ftppr.c index fbd2b4f..609f904 100644 --- a/src/ftppr.c +++ b/src/ftppr.c @@ -121,7 +121,7 @@ void * ftpprchild(struct clientparam* param) { } if ((clidatasock=socket(SASOCK(¶m->sincl), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN(821);} *SAPORT(¶m->sincl) = 0; - if(param->srv->so._bind(param->sostate, clidatasock, (struct sockaddr *)¶m->sincl, SASIZE(¶m->sincl))){RETURN(822);} + if(bindwithrange(param, clidatasock, ¶m->sincl, param->intport)){RETURN(822);} if (pasv) { if(param->srv->so._listen(param->sostate, clidatasock, 1)) {RETURN(823);} sasize = sizeof(param->sincl); diff --git a/src/proxy.h b/src/proxy.h index 7a65f75..4841c93 100644 --- a/src/proxy.h +++ b/src/proxy.h @@ -352,6 +352,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); +int bindwithrange(struct clientparam *param, SOCKET sock, PROXYSOCKADDRTYPE *sa, uint32_t range); +void applyportranges(struct clientparam * param, struct ace * acentry); uint32_t myrand(void); diff --git a/src/redirect.c b/src/redirect.c index 209312d..1cdeeb2 100644 --- a/src/redirect.c +++ b/src/redirect.c @@ -259,6 +259,20 @@ int clientnegotiate(struct chain * redir, struct clientparam * param, struct soc } +/* The local port ranges do not depend on the destination, so they can be taken + * as soon as a rule matches. UDP ASSOCIATE is authorized before the destination + * is known and returns before the chain is walked, which would otherwise leave + * the socket the client sends its datagrams to outside the configured range. + */ +void applyportranges(struct clientparam * param, struct ace * acentry){ + struct chain *cur; + + for(cur = acentry->chains; cur; cur = cur->next){ + if(cur->type == R_EXTPORT) param->extport = cur->range; + else if(cur->type == R_INTPORT) param->intport = cur->range; + } +} + int handleredirect(struct clientparam * param, struct ace * acentry){ int connected = 0; int weight = 1000; @@ -285,7 +299,8 @@ int handleredirect(struct clientparam * param, struct ace * acentry){ } continue; } - if(cur->type != R_EXTIP && cur->type != R_HA) param->redirected++; + if(cur->type != R_EXTIP && cur->type != R_HA && + cur->type != R_EXTPORT && cur->type != R_INTPORT) param->redirected++; done = 1; if(weight <= 0) { weight += 1000; @@ -293,6 +308,12 @@ int handleredirect(struct clientparam * param, struct ace * acentry){ r2 = (myrand()%1000); } if(!connected){ + if(cur->type == R_EXTPORT || cur->type == R_INTPORT){ + if(cur->type == R_EXTPORT) param->extport = cur->range; + else param->intport = cur->range; + if(cur->next)continue; + return 0; + } if(cur->type == R_EXTIP){ param->sinsl = cur->addr; if(SAISNULL(¶m->sinsl) && (*SAFAMILY(¶m->sincr) == AF_INET || *SAFAMILY(¶m->sincr) == AF_INET6))param->sinsl = param->sincr; diff --git a/src/socks.c b/src/socks.c index 76e77d3..a3576a0 100644 --- a/src/socks.c +++ b/src/socks.c @@ -218,7 +218,10 @@ void * sockschild(struct clientparam* param) { if((res = udpbind(param))) {RETURN(res);} } else if(command == 2) { - if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)¶m->sinsl,SASIZE(¶m->sinsl))) { + if(bindwithrange(param, param->remsock, ¶m->sinsl, param->extport)) { + /* a range has already been searched, retrying on any port would + ignore what was asked for */ + if(param->extport) RETURN (12); *SAPORT(¶m->sinsl) = 0; if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)¶m->sinsl,SASIZE(¶m->sinsl)))RETURN (12); #if SOCKSTRACE > 0 @@ -243,7 +246,8 @@ fflush(stderr); #endif sin = param->sincl; *SAPORT(&sin) = 0; - if(param->srv->so._bind(param->sostate, param->clisock,(struct sockaddr *)&sin,SASIZE(&sin))) {RETURN (12);} + /* the port the client is told to send its datagrams to */ + if(bindwithrange(param, param->clisock, &sin, param->intport)) {RETURN (12);} sasize = SASIZE(&sin); param->srv->so._getsockname(param->sostate, param->clisock, (struct sockaddr *)&sin, &sasize); #if SOCKSTRACE > 0 diff --git a/src/structures.h b/src/structures.h index 21cea79..aab1371 100644 --- a/src/structures.h +++ b/src/structures.h @@ -313,7 +313,9 @@ typedef enum { R_TLS, R_HA, R_DNS, - R_IMAP + R_IMAP, + R_EXTPORT, + R_INTPORT } REDIRTYPE; struct redirdesc { @@ -335,6 +337,8 @@ struct chain { unsigned char * extpass; unsigned short weight; unsigned short cidr; + /* local port range for extport/intport, first in the low half */ + uint32_t range; }; struct period { @@ -669,6 +673,7 @@ struct clientparam { maxtrafout64; PROXYSOCKADDRTYPE sincl, sincr; PROXYSOCKADDRTYPE sinsl, sinsr, req; + uint32_t extport, intport; uint64_t statscli64, statssrv64; diff --git a/src/udpsockmap.c b/src/udpsockmap.c index 46048f7..9729d73 100644 --- a/src/udpsockmap.c +++ b/src/udpsockmap.c @@ -121,8 +121,12 @@ int udpbind(struct clientparam *param) 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))) { + if (bindwithrange(param, param->remsock, ¶m->sinsl, param->extport)) { + if (param->extport) { + param->srv->so._closesocket(param->sostate, param->remsock); + param->remsock = INVALID_SOCKET; + return 12; + } *SAPORT(¶m->sinsl) = 0; if (param->srv->so._bind(param->sostate, param->remsock, (struct sockaddr *)¶m->sinsl, SASIZE(¶m->sinsl))) {