port range support (parent extport / intport)

This commit is contained in:
Vladimir Dubrovin 2026-08-25 14:23:33 +03:00
parent 1565c67c13
commit 6ca4a2686d
11 changed files with 128 additions and 11 deletions

View File

@ -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<ip>\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

View File

@ -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;
}

View File

@ -746,7 +746,7 @@ int doconnect(struct clientparam * param){
#ifdef WITH_UN
if(*SAFAMILY(&param->sinsl) != AF_UNIX)
#endif
if(param->srv->so._bind(param->sostate, param->remsock, (struct sockaddr*)&param->sinsl, SASIZE(&param->sinsl))==-1) {
if(bindwithrange(param, param->remsock, &param->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;

View File

@ -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:

View File

@ -149,7 +149,7 @@ void * dnsprchild(struct clientparam* param) {
}
memset(&param->sinsl, 0, sizeof(param->sinsl));
*SAFAMILY(&param->sinsl) = *SAFAMILY(&nservers[0].addr);
if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)&param->sinsl,SASIZE(&param->sinsl))) {
if(bindwithrange(param, param->remsock, &param->sinsl, param->extport)) {
RETURN(819);
}
param->sinsr = nservers[0].addr;

View File

@ -121,7 +121,7 @@ void * ftpprchild(struct clientparam* param) {
}
if ((clidatasock=socket(SASOCK(&param->sincl), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN(821);}
*SAPORT(&param->sincl) = 0;
if(param->srv->so._bind(param->sostate, clidatasock, (struct sockaddr *)&param->sincl, SASIZE(&param->sincl))){RETURN(822);}
if(bindwithrange(param, clidatasock, &param->sincl, param->intport)){RETURN(822);}
if (pasv) {
if(param->srv->so._listen(param->sostate, clidatasock, 1)) {RETURN(823);}
sasize = sizeof(param->sincl);

View File

@ -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);

View File

@ -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(&param->sinsl) && (*SAFAMILY(&param->sincr) == AF_INET || *SAFAMILY(&param->sincr) == AF_INET6))param->sinsl = param->sincr;

View File

@ -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 *)&param->sinsl,SASIZE(&param->sinsl))) {
if(bindwithrange(param, param->remsock, &param->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(&param->sinsl) = 0;
if(param->srv->so._bind(param->sostate, param->remsock,(struct sockaddr *)&param->sinsl,SASIZE(&param->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

View File

@ -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;

View File

@ -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 *)&param->sinsl, SASIZE(&param->sinsl))) {
if (bindwithrange(param, param->remsock, &param->sinsl, param->extport)) {
if (param->extport) {
param->srv->so._closesocket(param->sostate, param->remsock);
param->remsock = INVALID_SOCKET;
return 12;
}
*SAPORT(&param->sinsl) = 0;
if (param->srv->so._bind(param->sostate, param->remsock,
(struct sockaddr *)&param->sinsl, SASIZE(&param->sinsl))) {