Unify Linux namespace operations

This commit is contained in:
Vladimir Dubrovin 2026-10-12 18:18:27 +03:00
parent f1b8d30799
commit cc19bbff6d
10 changed files with 70 additions and 109 deletions

View File

@ -165,10 +165,10 @@ syntax.
bind internal (\fB-Di\fR) / external (\fB-De\fR) interface to given INTERFACE (e.g. eth0) if \fBSO_BINDTODEVICE\fR is supported by the system. You may need to run as root or have \fBCAP_NET_RAW\fR capability in order to bind to an interface, depending on the system, so this option may require root privileges and can be incompatible with some configuration commands like \fBchroot\fR and \fBsetuid\fR (and \fBdaemon\fR if setcap is used). bind internal (\fB-Di\fR) / external (\fB-De\fR) interface to given INTERFACE (e.g. eth0) if \fBSO_BINDTODEVICE\fR is supported by the system. You may need to run as root or have \fBCAP_NET_RAW\fR capability in order to bind to an interface, depending on the system, so this option may require root privileges and can be incompatible with some configuration commands like \fBchroot\fR and \fBsetuid\fR (and \fBdaemon\fR if setcap is used).
.br .br
.B -ni\fIPATH\fB, -ne\fIPATH\fR .B -ni\fIPATH\fB, -ne\fIPATH\fR
(Linux only) Switch to the network namespace identified by the filesystem path \fIPATH\fR (e.g. \fI/var/run/netns/myns\fR or \fI/proc/PID/ns/net\fR) for the listening socket (\fB-ni\fR) or for outgoing connections (\fB-ne\fR). (Linux only) Use the network namespace identified by the filesystem path \fIPATH\fR (e.g. \fI/var/run/netns/myns\fR or \fI/proc/PID/ns/net\fR) for the sockets which face the clients (\fB-ni\fR: the listening socket, and the socket SOCKS UDP ASSOCIATE talks to the client on) or the destinations (\fB-ne\fR: outgoing connections, SOCKS BIND and the UDP sockets towards the destination, and the connect back sockets of \fB-r\fR and \fB-R\fR).
With \fB-ni\fR the current namespace is saved before opening the listening socket and restored immediately after binding, so that the rest of the process (outgoing connections, child threads) runs in the original namespace unless \fB-ne\fR is also given. Threads stay in the namespace 3proxy was started in and enter the configured one only to create such a socket. Everything else is done in the original namespace: name resolution, both through \fBnserver\fR and through the system, including the queries \fBdnspr\fR forwards, logging and RADIUS.
With \fB-ne\fR the process switches to the specified namespace after the listening socket is bound (and after restoring from \fB-ni\fR if applicable). The paths are opened when the service line is read, so for a service configured after \fBchroot\fR they are looked up inside it; a path which is not a network namespace stops the service from starting.
Requires \fBCAP_SYS_ADMIN\fR (or \fBCAP_NET_ADMIN\fR on recent kernels) and is incompatible with \fBchroot\fR/\fBsetuid\fR/\fBdaemon\fR if privileges are dropped before the switch takes effect. Entering a namespace requires \fBCAP_SYS_ADMIN\fR for as long as the service runs, so these options cannot be combined with \fBsetuid\fR to an unprivileged user.
.br .br
.B -Ne .B -Ne
(for socks) External NAT address (between 3proxy and destination server) to report to client for CONNECT and BIND. By default external address is reported. It's only useful in the case of IP-IP NAT (will not work for PAT). (for socks) External NAT address (between 3proxy and destination server) to report to client for CONNECT and BIND. By default external address is reported. It's only useful in the case of IP-IP NAT (will not work for PAT).

View File

@ -549,6 +549,9 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
return 1; return 1;
} }
#ifdef __linux__
nsopen(); /* before a chroot in the configuration takes /proc away */
#endif
_3proxy_mutex_init(&config_mutex); _3proxy_mutex_init(&config_mutex);
_3proxy_mutex_init(&runningservices_mutex); _3proxy_mutex_init(&runningservices_mutex);
_3proxy_mutex_init(&bandlim_mutex); _3proxy_mutex_init(&bandlim_mutex);

View File

@ -45,10 +45,37 @@ int socks5_setaddr(int family, int atyp, const unsigned char *addr, PROXYSOCKADD
#ifdef __linux__ #ifdef __linux__
#include <sched.h> #include <sched.h>
int switch_ns(struct srvparam *srv, int target_fd) { /* Threads stay in the network namespace the process started in. A socket
if(target_fd < 0) return 0; belongs to the namespace it is made in, whatever thread uses it later,
if(srv->saved_nsfd >= 0 && setns(srv->saved_nsfd, CLONE_NEWNET)) return -1; so a service's other namespaces are entered for socket() alone. What a
return setns(target_fd, CLONE_NEWNET); thread does without making a socket of its own, a name looked up through
the system, a log line, RADIUS, happens in the main namespace. */
int mainnsfd = -1;
/* Opens the main namespace, once and while /proc can still be reached: a
chroot may take it away. */
int nsopen(void){
if(mainnsfd < 0) mainnsfd = open("/proc/self/ns/net", O_RDONLY | O_CLOEXEC);
return mainnsfd < 0;
}
SOCKET nssocket(struct srvparam *srv, struct sockfuncs *sf, void *state, int side, int domain, int type, int protocol){
int nsfd = (side == NS_OUT)? srv->o_nsfd : srv->i_nsfd;
SOCKET s;
int err;
if(nsfd < 0) return sf->_socket(state, domain, type, protocol);
if(setns(nsfd, CLONE_NEWNET)) return INVALID_SOCKET;
s = sf->_socket(state, domain, type, protocol);
err = errno;
if(setns(mainnsfd, CLONE_NEWNET)){
/* Left in the other namespace: no socket from here, and the
connection fails. The thread ends with it. */
if(s != INVALID_SOCKET) sf->_closesocket(state, s);
return INVALID_SOCKET;
}
errno = err;
return s;
} }
#endif #endif
@ -742,7 +769,7 @@ int doconnect(struct clientparam * param){
} }
if(!*SAPORT(&param->sinsr))*SAPORT(&param->sinsr) = *SAPORT(&param->req); if(!*SAPORT(&param->sinsr))*SAPORT(&param->sinsr) = *SAPORT(&param->req);
for(;;){ for(;;){
if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(&param->sinsr), SOCK_STREAM, if ((param->remsock=nssocket(param->srv, &param->srv->so, param->sostate, NS_OUT, SASOCK(&param->sinsr), SOCK_STREAM,
#ifdef WITH_UN #ifdef WITH_UN
*SAFAMILY(&param->sinsr) == AF_UNIX? 0 : *SAFAMILY(&param->sinsr) == AF_UNIX? 0 :
#endif #endif

View File

@ -162,7 +162,7 @@ int start_proxy_thread(struct child * chp){
#endif #endif
if((res = mainprepare(chp->argc, (char **)chp->argv, &service))) { if((res = mainprepare(chp->argc, (char **)chp->argv, &service))) {
fprintf(stderr, "Service not started on line: %d%s\n", linenum, res == 2? ": insufficient memory": res == 3? ": too many filters":""); fprintf(stderr, "Service not started on line: %d%s\n", linenum, res == 2? ": insufficient memory": res == 3? ": too many filters": res == 4? ": network namespace unavailable":"");
return(40); return(40);
} }
_3proxy_mutex_lock(&runningservices_mutex); _3proxy_mutex_lock(&runningservices_mutex);

View File

@ -46,7 +46,7 @@ void * dnsprchild(struct clientparam* param) {
_3proxy_sem_unlock(udpinit); _3proxy_sem_unlock(udpinit);
semlocked = 0; semlocked = 0;
#if defined(_WIN32) && !defined(SHARE_UDP_SOCKET) #if defined(_WIN32) && !defined(SHARE_UDP_SOCKET)
if((param->clisock=param->srv->so._socket(param->sostate, AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET) { if((param->clisock=nssocket(param->srv, &param->srv->so, param->sostate, NS_IN, AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET) {
RETURN(818); RETURN(818);
} }
ioctlsocket(param->clisock, FIONBIO, &ul); ioctlsocket(param->clisock, FIONBIO, &ul);

View File

@ -239,8 +239,15 @@ int sockmap(struct clientparam * param, int timeo, int usesplice);
int udpsockmap(struct clientparam * param, int timeo); int udpsockmap(struct clientparam * param, int timeo);
int udpbind(struct clientparam * param); int udpbind(struct clientparam * param);
int socks5_setaddr(int family, int atyp, const unsigned char *addr, PROXYSOCKADDRTYPE *sa); int socks5_setaddr(int family, int atyp, const unsigned char *addr, PROXYSOCKADDRTYPE *sa);
/* which of a service's network namespaces a socket is made in */
#define NS_IN 0 /* -ni: what faces the clients */
#define NS_OUT 1 /* -ne: what faces the destinations */
#ifdef __linux__ #ifdef __linux__
int switch_ns(struct srvparam *srv, int target_fd); extern int mainnsfd;
int nsopen(void);
SOCKET nssocket(struct srvparam *srv, struct sockfuncs *sf, void *state, int side, int domain, int type, int protocol);
#else
#define nssocket(srv, sf, state, side, domain, type, protocol) ((sf)->_socket((state), (domain), (type), (protocol)))
#endif #endif
int socksend(struct clientparam *param, SOCKET sock, unsigned char * buf, int bufsize, int to); 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 socksendto(struct clientparam *param, SOCKET sock, struct sockaddr * sin, unsigned char * buf, int bufsize, int to);

View File

@ -748,6 +748,18 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){
#endif #endif
if(hostname)parsehostname(hostname, defparam, childdef.port); if(hostname)parsehostname(hostname, defparam, childdef.port);
#ifdef __linux__
/* Opened here, where the configuration is read, before a chroot can hide
the paths, and each entered once to be sure it is a network namespace
this process may use: a mistake shows now, not on a client's connection. */
if(srv->inetns || srv->onetns){
if(nsopen()) return 4;
if(srv->inetns && (srv->i_nsfd = open(srv->inetns, O_RDONLY | O_CLOEXEC)) < 0) return 4;
if(srv->onetns && (srv->o_nsfd = open(srv->onetns, O_RDONLY | O_CLOEXEC)) < 0) return 4;
if(srv->i_nsfd >= 0 && (setns(srv->i_nsfd, CLONE_NEWNET) || setns(mainnsfd, CLONE_NEWNET))) return 4;
if(srv->o_nsfd >= 0 && (setns(srv->o_nsfd, CLONE_NEWNET) || setns(mainnsfd, CLONE_NEWNET))) return 4;
}
#endif
#ifndef STDMAIN #ifndef STDMAIN
if((error = copyfilter(conf.filters, srv))) return error; if((error = copyfilter(conf.filters, srv))) return error;
@ -772,7 +784,7 @@ static int srvlisten(struct srvstart *s){
#ifndef NOTCPMAIN #ifndef NOTCPMAIN
if(!s->isudp){ if(!s->isudp){
sock=srv->so._socket(srv->so.state, SASOCK(&srv->intsa), SOCK_STREAM, sock=nssocket(srv, &srv->so, srv->so.state, NS_IN, SASOCK(&srv->intsa), SOCK_STREAM,
#ifdef WITH_UN #ifdef WITH_UN
*SAFAMILY(&srv->intsa) == AF_UNIX? 0 : *SAFAMILY(&srv->intsa) == AF_UNIX? 0 :
#endif #endif
@ -782,7 +794,7 @@ static int srvlisten(struct srvstart *s){
#endif #endif
#ifndef NOUDPMAIN #ifndef NOUDPMAIN
if(s->isudp){ if(s->isudp){
sock=srv->so._socket(srv->so.state, SASOCK(&srv->intsa), SOCK_DGRAM, IPPROTO_UDP); sock=nssocket(srv, &srv->so, srv->so.state, NS_IN, SASOCK(&srv->intsa), SOCK_DGRAM, IPPROTO_UDP);
} }
#endif #endif
if( sock == INVALID_SOCKET) { if( sock == INVALID_SOCKET) {
@ -845,7 +857,7 @@ static int cblisten(struct srvstart *s){
PROXYSOCKADDRTYPE cbsa; PROXYSOCKADDRTYPE cbsa;
parsehost(srv->family, s->cbl_string, (struct sockaddr *)&cbsa); parsehost(srv->family, s->cbl_string, (struct sockaddr *)&cbsa);
if((srv->cbsock=srv->so._socket(srv->so.state, SASOCK(&cbsa), SOCK_STREAM, IPPROTO_TCP))==INVALID_SOCKET) { if((srv->cbsock=nssocket(srv, &srv->so, srv->so.state, NS_OUT, SASOCK(&cbsa), SOCK_STREAM, IPPROTO_TCP))==INVALID_SOCKET) {
dolog(defparam, (unsigned char *)"Failed to allocate connect back socket"); dolog(defparam, (unsigned char *)"Failed to allocate connect back socket");
return -6; return -6;
} }
@ -885,7 +897,7 @@ static int tcpnext(struct srvstart *s, SOCKET *client, int *nlog){
size = sizeof(defparam->sincr); size = sizeof(defparam->sincr);
if(s->iscbc){ if(s->iscbc){
new_sock=so._socket(so.state, SASOCK(&defparam->sincr), SOCK_STREAM, IPPROTO_TCP); new_sock=nssocket(&s->srv, &so, so.state, NS_OUT, SASOCK(&defparam->sincr), SOCK_STREAM, IPPROTO_TCP);
if(new_sock == INVALID_SOCKET){ if(new_sock == INVALID_SOCKET){
usleep(SLEEPTIME); usleep(SLEEPTIME);
return NEXT_RETRY; return NEXT_RETRY;
@ -1128,10 +1140,6 @@ static int srvrun(struct srvstart *s){
#endif #endif
/* -r: a TCP service connects out for each client instead of listening */ /* -r: a TCP service connects out for each client instead of listening */
int connectback = !s->isudp && s->iscbc; int connectback = !s->isudp && s->iscbc;
#ifdef __linux__
int saved_nsfd = -1;
#endif
#ifndef _WIN32 #ifndef _WIN32
pthread_attr_init(&s->pa); pthread_attr_init(&s->pa);
pthread_attr_setstacksize(&s->pa,threadstacksize(srv->stacksize)); pthread_attr_setstacksize(&s->pa,threadstacksize(srv->stacksize));
@ -1145,78 +1153,7 @@ static int srvrun(struct srvstart *s){
#endif #endif
#endif #endif
#ifdef __linux__
if(srv->inetns) {
saved_nsfd = open("/proc/self/ns/net", O_RDONLY);
if(saved_nsfd == -1) {
dolog(defparam, (unsigned char *)"failed to open /proc/self/ns/net");
res = -13;
goto EXIT;
}
{
int nsfd = open(srv->inetns, O_RDONLY);
if(nsfd == -1) {
dolog(defparam, (unsigned char *)"failed to open inetns");
close(saved_nsfd);
res = -13;
goto EXIT;
}
if(setns(nsfd, CLONE_NEWNET)) {
dolog(defparam, (unsigned char *)"failed to setns inetns");
close(nsfd);
close(saved_nsfd);
res = -13;
goto EXIT;
}
if(srv->service == S_SOCKS) srv->i_nsfd = nsfd;
else close(nsfd);
}
if(srv->service == S_SOCKS) srv->saved_nsfd = saved_nsfd;
}
#endif
if(!connectback && (res = srvlisten(s))) goto EXIT; if(!connectback && (res = srvlisten(s))) goto EXIT;
#ifdef __linux__
if(saved_nsfd != -1) {
if(setns(saved_nsfd, CLONE_NEWNET)) {
dolog(defparam, (unsigned char *)"failed to restore netns");
if(srv->service == S_SOCKS) {
if(srv->i_nsfd >= 0) { close(srv->i_nsfd); srv->i_nsfd = -1; }
srv->saved_nsfd = -1;
}
close(saved_nsfd);
res = -14;
goto EXIT;
}
if(srv->service != S_SOCKS) {
close(saved_nsfd);
saved_nsfd = -1;
}
}
if(srv->onetns) {
int nsfd = open(srv->onetns, O_RDONLY);
if(nsfd == -1) {
dolog(defparam, (unsigned char *)"failed to open onetns");
if(srv->service == S_SOCKS) {
if(srv->saved_nsfd >= 0) { close(srv->saved_nsfd); srv->saved_nsfd = -1; }
if(srv->i_nsfd >= 0) { close(srv->i_nsfd); srv->i_nsfd = -1; }
}
res = -14;
goto EXIT;
}
if(setns(nsfd, CLONE_NEWNET)) {
dolog(defparam, (unsigned char *)"failed to setns onetns");
close(nsfd);
if(srv->service == S_SOCKS) {
if(srv->saved_nsfd >= 0) { close(srv->saved_nsfd); srv->saved_nsfd = -1; }
if(srv->i_nsfd >= 0) { close(srv->i_nsfd); srv->i_nsfd = -1; }
}
res = -14;
goto EXIT;
}
if(srv->service == S_SOCKS) srv->o_nsfd = nsfd;
else close(nsfd);
}
#endif
#ifndef NOTCPMAIN #ifndef NOTCPMAIN
if(!s->isudp && s->iscbl && (res = cblisten(s))) goto EXIT; if(!s->isudp && s->iscbl && (res = cblisten(s))) goto EXIT;
#endif #endif
@ -1419,7 +1356,7 @@ void srvinit(struct srvparam * srv, struct clientparam *param){
srv->cbsock = INVALID_SOCKET; srv->cbsock = INVALID_SOCKET;
srv->needuser = 1; srv->needuser = 1;
#ifdef __linux__ #ifdef __linux__
srv->saved_nsfd = srv->i_nsfd = srv->o_nsfd = -1; srv->i_nsfd = srv->o_nsfd = -1;
#endif #endif
#ifdef WITHSPLICE #ifdef WITHSPLICE
srv->usesplice = 0; srv->usesplice = 0;
@ -1530,7 +1467,6 @@ void srvfree(struct srvparam * srv){
#ifdef __linux__ #ifdef __linux__
if(srv->inetns) free(srv->inetns); if(srv->inetns) free(srv->inetns);
if(srv->onetns) free(srv->onetns); if(srv->onetns) free(srv->onetns);
if(srv->saved_nsfd >= 0) { close(srv->saved_nsfd); srv->saved_nsfd = -1; }
if(srv->i_nsfd >= 0) { close(srv->i_nsfd); srv->i_nsfd = -1; } if(srv->i_nsfd >= 0) { close(srv->i_nsfd); srv->i_nsfd = -1; }
if(srv->o_nsfd >= 0) { close(srv->o_nsfd); srv->o_nsfd = -1; } if(srv->o_nsfd >= 0) { close(srv->o_nsfd); srv->o_nsfd = -1; }
#endif #endif

View File

@ -179,7 +179,7 @@ void * sockschild(struct clientparam* param) {
#endif #endif
param->operation = command == 2?BIND:UDPASSOC; param->operation = command == 2?BIND:UDPASSOC;
if(command == 2){ if(command == 2){
if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(&param->req), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN (11);} if ((param->remsock=nssocket(param->srv, &param->srv->so, param->sostate, NS_OUT, SASOCK(&param->req), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN (11);}
setopts(param->remsock, &param->srv->lissockopts); setopts(param->remsock, &param->srv->lissockopts);
if(extbindtodevice(param, param->remsock)) {RETURN (12);} if(extbindtodevice(param, param->remsock)) {RETURN (12);}
} }
@ -231,10 +231,7 @@ fflush(stderr);
} }
if(command == 3) { if(command == 3) {
param->ctrlsock = param->clisock; param->ctrlsock = param->clisock;
#ifdef __linux__ param->clisock = nssocket(param->srv, &param->srv->so, param->sostate, NS_IN, SASOCK(&param->sincr), SOCK_DGRAM, IPPROTO_UDP);
if(switch_ns(param->srv, param->srv->i_nsfd)) {RETURN(11);}
#endif
param->clisock = param->srv->so._socket(param->sostate, SASOCK(&param->sincr), SOCK_DGRAM, IPPROTO_UDP);
if(param->clisock == INVALID_SOCKET) {RETURN(11);} if(param->clisock == INVALID_SOCKET) {RETURN(11);}
#ifdef _WIN32 #ifdef _WIN32
{ unsigned long ul = 1; ioctlsocket(param->clisock, FIONBIO, &ul); } { unsigned long ul = 1; ioctlsocket(param->clisock, FIONBIO, &ul); }

View File

@ -659,7 +659,7 @@ struct srvparam {
#ifdef __linux__ #ifdef __linux__
char * inetns; char * inetns;
char * onetns; char * onetns;
int saved_nsfd; /* -ni and -ne namespaces, opened with the service, -1 for the main one */
int i_nsfd; int i_nsfd;
int o_nsfd; int o_nsfd;
#endif #endif

View File

@ -104,16 +104,7 @@ int udpbind(struct clientparam *param)
SOCKET s; SOCKET s;
SASIZETYPE sasize; SASIZETYPE sasize;
#ifdef __linux__ s = nssocket(param->srv, &param->srv->so, param->sostate, NS_OUT, SASOCK(&param->sinsl), SOCK_DGRAM, IPPROTO_UDP);
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; if (s == INVALID_SOCKET) return 11;
#ifdef _WIN32 #ifdef _WIN32
{ unsigned long ul = 1; ioctlsocket(s, FIONBIO, &ul); } { unsigned long ul = 1; ioctlsocket(s, FIONBIO, &ul); }