From cc19bbff6da6aad5f9b1fa0542c8ff3c39828840 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Mon, 12 Oct 2026 18:18:27 +0300 Subject: [PATCH] Unify Linux namespace operations --- man/3proxy.cfg.5 | 8 ++-- src/3proxy.c | 3 ++ src/common.c | 37 +++++++++++++++--- src/conf.c | 2 +- src/dnspr.c | 2 +- src/proxy.h | 9 ++++- src/proxymain.c | 98 +++++++++--------------------------------------- src/socks.c | 7 +--- src/structures.h | 2 +- src/udpsockmap.c | 11 +----- 10 files changed, 70 insertions(+), 109 deletions(-) diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5 index c3e0954..c9a85da 100644 --- a/man/3proxy.cfg.5 +++ b/man/3proxy.cfg.5 @@ -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). .br .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). -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. -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). -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. +(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). +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. +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. +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 .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). diff --git a/src/3proxy.c b/src/3proxy.c index c2a7ce0..d17a3b4 100644 --- a/src/3proxy.c +++ b/src/3proxy.c @@ -549,6 +549,9 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int return 1; } +#ifdef __linux__ + nsopen(); /* before a chroot in the configuration takes /proc away */ +#endif _3proxy_mutex_init(&config_mutex); _3proxy_mutex_init(&runningservices_mutex); _3proxy_mutex_init(&bandlim_mutex); diff --git a/src/common.c b/src/common.c index c39ba67..847e9f0 100644 --- a/src/common.c +++ b/src/common.c @@ -45,10 +45,37 @@ int socks5_setaddr(int family, int atyp, const unsigned char *addr, PROXYSOCKADD #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); +/* Threads stay in the network namespace the process started in. A socket + belongs to the namespace it is made in, whatever thread uses it later, + so a service's other namespaces are entered for socket() alone. What a + 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 @@ -742,7 +769,7 @@ int doconnect(struct clientparam * param){ } if(!*SAPORT(¶m->sinsr))*SAPORT(¶m->sinsr) = *SAPORT(¶m->req); for(;;){ - if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(¶m->sinsr), SOCK_STREAM, + if ((param->remsock=nssocket(param->srv, ¶m->srv->so, param->sostate, NS_OUT, SASOCK(¶m->sinsr), SOCK_STREAM, #ifdef WITH_UN *SAFAMILY(¶m->sinsr) == AF_UNIX? 0 : #endif diff --git a/src/conf.c b/src/conf.c index 6e09a25..903bc08 100644 --- a/src/conf.c +++ b/src/conf.c @@ -162,7 +162,7 @@ int start_proxy_thread(struct child * chp){ #endif 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); } _3proxy_mutex_lock(&runningservices_mutex); diff --git a/src/dnspr.c b/src/dnspr.c index 6e0f917..29e51f5 100644 --- a/src/dnspr.c +++ b/src/dnspr.c @@ -46,7 +46,7 @@ void * dnsprchild(struct clientparam* param) { _3proxy_sem_unlock(udpinit); semlocked = 0; #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, ¶m->srv->so, param->sostate, NS_IN, AF_INET, SOCK_DGRAM, IPPROTO_UDP)) == INVALID_SOCKET) { RETURN(818); } ioctlsocket(param->clisock, FIONBIO, &ul); diff --git a/src/proxy.h b/src/proxy.h index 8e0e88a..7a53bc3 100644 --- a/src/proxy.h +++ b/src/proxy.h @@ -239,8 +239,15 @@ int sockmap(struct clientparam * param, int timeo, int usesplice); int udpsockmap(struct clientparam * param, int timeo); int udpbind(struct clientparam * param); 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__ -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 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); diff --git a/src/proxymain.c b/src/proxymain.c index e4b5934..ab17c90 100644 --- a/src/proxymain.c +++ b/src/proxymain.c @@ -748,6 +748,18 @@ static int srvprepare(struct srvstart *s, int argc, char **argv){ #endif 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 if((error = copyfilter(conf.filters, srv))) return error; @@ -772,7 +784,7 @@ static int srvlisten(struct srvstart *s){ #ifndef NOTCPMAIN 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 *SAFAMILY(&srv->intsa) == AF_UNIX? 0 : #endif @@ -782,7 +794,7 @@ static int srvlisten(struct srvstart *s){ #endif #ifndef NOUDPMAIN 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 if( sock == INVALID_SOCKET) { @@ -845,7 +857,7 @@ static int cblisten(struct srvstart *s){ PROXYSOCKADDRTYPE 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"); return -6; } @@ -885,7 +897,7 @@ static int tcpnext(struct srvstart *s, SOCKET *client, int *nlog){ size = sizeof(defparam->sincr); 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){ usleep(SLEEPTIME); return NEXT_RETRY; @@ -1128,10 +1140,6 @@ static int srvrun(struct srvstart *s){ #endif /* -r: a TCP service connects out for each client instead of listening */ int connectback = !s->isudp && s->iscbc; -#ifdef __linux__ - int saved_nsfd = -1; -#endif - #ifndef _WIN32 pthread_attr_init(&s->pa); pthread_attr_setstacksize(&s->pa,threadstacksize(srv->stacksize)); @@ -1145,78 +1153,7 @@ static int srvrun(struct srvstart *s){ #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; -#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 if(!s->isudp && s->iscbl && (res = cblisten(s))) goto EXIT; #endif @@ -1419,7 +1356,7 @@ void srvinit(struct srvparam * srv, struct clientparam *param){ srv->cbsock = INVALID_SOCKET; srv->needuser = 1; #ifdef __linux__ - srv->saved_nsfd = srv->i_nsfd = srv->o_nsfd = -1; + srv->i_nsfd = srv->o_nsfd = -1; #endif #ifdef WITHSPLICE srv->usesplice = 0; @@ -1530,7 +1467,6 @@ void srvfree(struct srvparam * srv){ #ifdef __linux__ if(srv->inetns) free(srv->inetns); 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->o_nsfd >= 0) { close(srv->o_nsfd); srv->o_nsfd = -1; } #endif diff --git a/src/socks.c b/src/socks.c index 4c7ceb8..4f9a453 100644 --- a/src/socks.c +++ b/src/socks.c @@ -179,7 +179,7 @@ void * sockschild(struct clientparam* param) { #endif param->operation = command == 2?BIND:UDPASSOC; if(command == 2){ - if ((param->remsock=param->srv->so._socket(param->sostate, SASOCK(¶m->req), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN (11);} + if ((param->remsock=nssocket(param->srv, ¶m->srv->so, param->sostate, NS_OUT, SASOCK(¶m->req), SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET) {RETURN (11);} setopts(param->remsock, ¶m->srv->lissockopts); if(extbindtodevice(param, param->remsock)) {RETURN (12);} } @@ -231,10 +231,7 @@ fflush(stderr); } if(command == 3) { param->ctrlsock = param->clisock; -#ifdef __linux__ - if(switch_ns(param->srv, param->srv->i_nsfd)) {RETURN(11);} -#endif - param->clisock = param->srv->so._socket(param->sostate, SASOCK(¶m->sincr), SOCK_DGRAM, IPPROTO_UDP); + param->clisock = nssocket(param->srv, ¶m->srv->so, param->sostate, NS_IN, SASOCK(¶m->sincr), SOCK_DGRAM, IPPROTO_UDP); if(param->clisock == INVALID_SOCKET) {RETURN(11);} #ifdef _WIN32 { unsigned long ul = 1; ioctlsocket(param->clisock, FIONBIO, &ul); } diff --git a/src/structures.h b/src/structures.h index c89143d..6e996a7 100644 --- a/src/structures.h +++ b/src/structures.h @@ -659,7 +659,7 @@ struct srvparam { #ifdef __linux__ char * inetns; char * onetns; - int saved_nsfd; + /* -ni and -ne namespaces, opened with the service, -1 for the main one */ int i_nsfd; int o_nsfd; #endif diff --git a/src/udpsockmap.c b/src/udpsockmap.c index 05cdd12..10f90cd 100644 --- a/src/udpsockmap.c +++ b/src/udpsockmap.c @@ -104,16 +104,7 @@ 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 + s = nssocket(param->srv, ¶m->srv->so, param->sostate, NS_OUT, SASOCK(¶m->sinsl), SOCK_DGRAM, IPPROTO_UDP); if (s == INVALID_SOCKET) return 11; #ifdef _WIN32 { unsigned long ul = 1; ioctlsocket(s, FIONBIO, &ul); }