├── .gitignore ├── .gitmodules ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── config.mk.in ├── configure.ac ├── img ├── runns-logo.png └── runns-logo.xcf ├── runns.c ├── runns.h ├── runnsctl.c └── tests ├── Makefile ├── prepare_env.sh ├── queue.c └── runnsctl.c /.gitignore: -------------------------------------------------------------------------------- 1 | a.out 2 | runns 3 | runnsctl 4 | client 5 | *.o 6 | *.so 7 | configure 8 | tests/test_* 9 | tags 10 | chroot/ 11 | autom4te.cache/ 12 | config.* 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tau"] 2 | path = tau 3 | url = https://github.com/jasmcaus/tau 4 | branch = dev 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux AS runns_test 2 | 3 | ARG SRC 4 | 5 | # Install necessary packages 6 | RUN pacman -Sy 7 | RUN pacman -S --noconfirm iptables iproute2 gawk gcc binutils m4 autoconf make cpio 8 | # Create a group and a user to run runnsctl 9 | RUN groupadd runns_user 10 | RUN useradd -m -g runns_user -G users runns_user 11 | # Create a group for runns 12 | RUN groupadd runns 13 | # Switch to the user 14 | USER runns_user:runns_user 15 | WORKDIR /home/runns_user 16 | COPY "$SRC" . 17 | RUN cpio -idv < "$SRC" 18 | # Configure RUNNS 19 | RUN autoconf && ./configure && make 20 | # Prepare netns 21 | #USER root:root 22 | #RUN ./tests/prepare_env.sh 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 Nikita Ermakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include config.mk 2 | 3 | all: $(DAEMON) $(CLIENT) $(HELPER_LIB) 4 | 5 | $(DAEMON): runns.o 6 | $(CC) -o $@ $< 7 | 8 | $(CLIENT): $(CLIENT).o 9 | $(CC) -o $@ $< 10 | 11 | $(HELPER_LIB): librunns.c 12 | $(CC) -o $@ -shared -fPIC $< 13 | 14 | .PHONY: tests 15 | tests: container 16 | 17 | container: 18 | @ git submodule status | grep -q -v '^-' || { echo 'tau submodule is not initialized'; exit 1; } 19 | @ git ls-tree -r --name-only @ | cpio -o -H newc > runns.cpio 20 | @ git --git-dir tau/.git ls-tree -r --name-only @ | sed -n 's;.*;tau/&;p' | cpio -o -A -H newc -F runns.cpio 21 | @ docker build -t runns_test --build-arg SRC=runns.cpio . 22 | 23 | tests_build: 24 | $(MAKE) -C tests/ build 25 | 26 | tests_run: 27 | $(MAKE) -C tests/ run 28 | 29 | .PHONY: clean 30 | clean: 31 | rm -f $(DAEMON) $(CLIENT) $(HELPER_LIB) *.o 32 | $(MAKE) -C tests/ clean 33 | 34 | .PHONY: install 35 | install: $(DAEMON) $(CLIENT) $(HELPER) 36 | mkdir -p $(DESTDIR)/$(PREFIX)/bin 37 | cp $^ $(DESTDIR)/$(PREFIX)/bin/ 38 | chmod 755 $(DESTDIR)/$(PREFIX)/bin/{$(subst $(_),$(comma),$^)} 39 | 40 | .PHONY: uninstall 41 | uninstall: $(CLIENT) $(DAEMON) $(HELPER) 42 | rm -f $(DESTDIR)/$(PREFIX)/bin/{$(subst $(_),$(comma),$^)} 43 | 44 | .PHONY: distclean 45 | distclean: 46 | rm -vrf $(DAEMON) $(CLIENT) *.o autom4te.cache config.log config.status config.mk configure 47 | 48 | define HELP_MESSAGE 49 | The following rules are available: 50 | 51 | all (default) -- build the daemon, client and the helper libs 52 | tests -- build and run the unit tests in container 53 | tests_build -- just build the unit tests 54 | tests_run -- just run the unit tests 55 | clean -- remove object files, the built binaries including unit tests 56 | install -- install the daemon and client to the system 57 | uninstall -- revert install 58 | distclean -- remove all built binaries and configuration 59 | endef 60 | 61 | .PHONY: help 62 | help: 63 | @ $(info $(HELP_MESSAGE)) 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![GitHub Logo](/img/runns-logo.png) 2 | 3 | ## Contents 4 | * [About](#about) 5 | * [How to use it](#how-to-use-it) 6 | * [Troubleshooting](#troubleshooting) 7 | * [Example use-case](#example-use-case) 8 | * [Acknowledgement](#acknowledgement) 9 | * [References](#references) 10 | 11 | ## About 12 | The **RUNNS** provides daemon (*runns*), client (*runnsctl*) and helper scripts (*build-net* and *clean-net*) for GNU/Linux to 13 | easy create and delete network namespaces [1], connect this network namespace to default via veth pair [2] and setup iptables NAT rules. 14 | 15 | **Why would you need this?** Let's assume a simple situation - you have a VPN with 0.0.0.0/1 default route (i.e. all of your traffic goes through VPN). However, VPN bandwidth is low and you don't want to pass some connections through VPN, for example SSH to another machine, or you simply don't want that all of your traffic goes through VPN. With runns it is easy to isolate programs inside a Linux network namespace with a VPN. 16 | 17 | ## How to use it 18 | ### Building 19 | To build and run this project one need to have at least the following things: 20 | * GNU make [3] 21 | * GCC [4] 22 | * libc (glibc) [5] 23 | * binutils [6] 24 | * POSIX compatible shell 25 | * iptables [7] 26 | * iproute2 [8] 27 | * gawk [9] 28 | * autoconf [10] 29 | 30 | The building procedure is straight forward: 31 | 32 | * Run autoconf in the source directory to get configure script: `$ autoconf`. 33 | * Conifgure: `$ ./configure`. 34 | * Build program with GNU make: `$ make`. 35 | * Install: `$ make install`. 36 | * To uninstall use the following command: `$ make uninstall`. 37 | 38 | The **runns** daemon will create a socket for communication with clients. 39 | Communication with this socket allowed only for users in the *runns* group. 40 | * Create *runns* group: `groupadd runns`. 41 | * Add a user USERNAME to the *runns* group: `usermod -a -G runns USERNAME`. 42 | ### runns 43 | This is a main daemon. This daemon opens an UNIX socket, by default in `/var/run/runns/runns.socket`, and provides logs via *syslog*. 44 | ### runnsctl 45 | This is a client for **runns** daemon. It allows to run a program inside the specified network namespace. 46 | It will copy all user shell environment variables and program path to the daemon. 47 | To add argv to the program enter them after the '--': `runnsctl --program foo -- --arg1 --arg2=bar`. 48 | 49 | For example, to run a *chromium* inside the *foo* network namespace with temporary profile one could run: 50 | 51 | `runnsctl --program /usr/bin/chromium --set-netns /var/run/netns/foo -- --temp-profile` 52 | 53 | To stop the daemon: 54 | 55 | `runnsctl --stop` 56 | 57 | To list PIDs runned by the user: 58 | 59 | `runnsctl --list` 60 | 61 | The other options could been seen with following command: 62 | 63 | `runnsctl --help` 64 | 65 | ### build-net 66 | This helper script allow user to easy create a network namespace. 67 | If run without arguments it will create a network namespace with a name vpn**X**, where **X** 68 | is the maximum number + 1 of namespaces with names `vpn[0-9]+`. Also, it will create a veth pair and assign one 69 | to the vpn**X** (it will be called eth0) and another to the default network namespace 70 | (it will be called the same as namespace -- vpn**X**). The script will assign an IPv4 address to the veth pair: 71 | * vpn**X** in the default namespace -- 172.24.**X**.1/24 72 | * eth0 in the vpn**X** namespace -- 172.24.**X**.2/24 73 | 74 | One could explicitly set the name of the veth interface in the default namespace and network namespace with the 75 | `--int` and `--name` options. 76 | 77 | Another useful feature is the `--resolve` option. 78 | With this option it is possible to set different resolv.conf files for each network namespace. 79 | 80 | At the end the script will also create an iptables NAT rule and setup resolv.conf if 81 | it was mentioned in the command line arguments. The script will also automatically enable IPv4 forwarding. 82 | 83 | This script also contains --section option. This option allows to load a specific section from the /etc/runns.conf 84 | file. This file a plain case insensitive INI file and could contains the following options: 85 | **NetworkNamespace**, **InterfaceIn**, **InterfaceOut**, **Resolve**. 86 | For example: 87 | ```shell 88 | $ cat /etc/runns.conf 89 | [work] 90 | NetworkNamespace=vpnWork 91 | InterfaceIn=vpnWorkd 92 | vpn=/etc/openvpn/work.conf 93 | vpn=/etc/openvpn/myvps.conf 94 | ``` 95 | Each vpn option specifies configuration files for OpenVPN [13]. OpenVPN will be started as a daemon 96 | with a "openvpn-**NetworkNamespaceName**" name in the system logger. 97 | 98 | ### clean-net 99 | This helper script is needed to easy delete and clean network namespace created by **build-net**. 100 | This script will check if any program is running inside the network namespace and if so it will ask to try 101 | to kill them all automatically. 102 | Please check the options before use: `clean-net --help`. 103 | 104 | ## Example use-case 105 | 106 | Let's assume that there is a /etc/runns.conf configuration file from above. 107 | From **root** user: 108 | 109 | ```shell 110 | root$ build-net -s work 111 | root$ runns 112 | ``` 113 | 114 | From **iddqd** user: 115 | ```shell 116 | iddqd$ runnsctl --program /usr/bin/chromium --set-netns /var/run/netns/vpnWork 117 | ``` 118 | 119 | To clean-up: 120 | ```shell 121 | root$ clean-net --name vpnWork -f 122 | root$ runnsctl -s 123 | ``` 124 | 125 | ### Run tmux session inside network namespace 126 | 127 | ```shell 128 | user$ # Run new tmux session with vpn123.socket 129 | user$ runnsctl --create-ptms --program /usr/bin/tmux --set-netns /var/run/netns/vpn123 -- -L vpn123.socket 130 | user$ # Attach tmux to the vpn123.socket 131 | user$ tmux -2 -L vpn123.socket attach -d 132 | ``` 133 | 134 | Now you are inside the network namespace in the new tmux session. 135 | 136 | ## Troubleshooting 137 | 138 | ### Network managers 139 | 140 | Some network managers have an issue with interfaces created by build-net and clean-net. 141 | To resolve these issues please add the vpn interfaces (or according to yours naming convention) 142 | to network manager's skip list. 143 | 144 | For example in the case of connman [12] one could add "vpn" to the **NetworkInterfaceBlacklist** 145 | in /etc/connman/main.conf. 146 | 147 | ```shell 148 | $ grep ^NetworkInterfaceBlacklist /etc/connman/main.conf 149 | NetworkInterfaceBlacklist = vmnet,vboxnet,virbr,ifb,ve-,vb-,vpn 150 | ``` 151 | 152 | ## Acknowledgement 153 | 154 | Thanks for the nice font [11] by Amazingmax which is used in the logo. 155 | 156 | ## References 157 | 1 -- https://lwn.net/Articles/580893 158 | 159 | 2 -- https://lwn.net/Articles/237087 160 | 161 | 3 -- https://www.gnu.org/software/make 162 | 163 | 4 -- https://gcc.gnu.org 164 | 165 | 5 -- https://www.gnu.org/software/libc 166 | 167 | 6 -- https://www.gnu.org/software/binutils 168 | 169 | 7 -- http://www.netfilter.org/projects/iptables 170 | 171 | 8 -- https://wiki.linuxfoundation.org/networking/iproute2 172 | 173 | 9 -- https://www.gnu.org/software/gawk 174 | 175 | 10 -- https://www.gnu.org/software/autoconf 176 | 177 | 11 -- https://fonts2u.com/amazdoomright.font 178 | 179 | 12 -- https://01.org/connman 180 | 181 | 13 -- https://openvpn.net 182 | -------------------------------------------------------------------------------- /config.mk.in: -------------------------------------------------------------------------------- 1 | CC=@CC@ 2 | LD=@CC@ 3 | PREFIX=@prefix@ 4 | 5 | CFLAGS=@CFLAGS@ 6 | DAEMON=runns 7 | CLIENT=runnsctl 8 | LIBRUNNS_ENABLE=@LIBRUNNS@ 9 | HELPER_LIB=$(if $(filter-out $(LIBRUNNS_ENABLE),no),librunns.so) 10 | HELPER=build-net clean-net 11 | _ := $() $() 12 | comma := , 13 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | dnl 2 | dnl Process this file in order to make a configure script 3 | dnl 4 | 5 | dnl Setup autoconf 6 | AC_INIT([runns], [1.41], [sh1r4s3@pm.me], [runns], [https://github.com/sh1r4s3/runns]) 7 | dnl AM_INIT_AUTOMAKE 8 | 9 | dnl Debug option 10 | AC_ARG_ENABLE(debug, 11 | AS_HELP_STRING([--enable-debug],[enable debugging, defult: no]), 12 | [case "${enableval}" in 13 | yes) debug=true ;; 14 | no) debug=false ;; 15 | *) AC_MSG_ERROR([bad value ${enablevalue} for --enable-debug]) ;; 16 | esac], [debug=false]) 17 | 18 | dnl Optional librunns 19 | AC_ARG_WITH(librunns, 20 | AS_HELP_STRING([--with-librunns],[build experimental librunns, default: no]), [librunns="$withval"], [librunns=no]) 21 | 22 | dnl Check dependences 23 | echo "Cheking dependences" 24 | AC_LANG(C) 25 | AC_CHECK_TOOL(IPTABLES, iptables, "", PATH="$PATH:/sbin") 26 | AC_CHECK_TOOL(IP, ip) 27 | AC_CHECK_TOOL(GAWK, gawk) 28 | 29 | dnl Disable all CFLAGS if it was not declarated 30 | if [ test -z "$CFLAGS"; ] then 31 | if [ test "x$debug" == "xtrue" ]; then 32 | CFLAGS="-g -O0 -DENABLE_DEBUG" 33 | else 34 | CFLAGS="-O2" 35 | fi 36 | fi 37 | 38 | AC_SUBST(LIBRUNNS, "$librunns") 39 | 40 | AC_PROG_CC 41 | 42 | if [ test "x$IPTABLES" == "x"; ] then 43 | AC_MSG_ERROR([iptables is required]) 44 | fi 45 | 46 | if [ test "x$IP" == "x"; ] then 47 | AC_MSG_ERROR([ip from iproute2 is required]) 48 | fi 49 | 50 | if [ test "x$GAWK" == "x"; ] then 51 | AC_MSG_ERROR([gawk is required]) 52 | fi 53 | 54 | dnl Get source code path 55 | dnl AC_DEFINE_UNQUOTED([SRC_PATH], ["$srcdir"], [source path]) 56 | 57 | dnl Configure main configuration for makefiles 58 | AC_CONFIG_FILES([config.mk]) 59 | 60 | AC_OUTPUT 61 | -------------------------------------------------------------------------------- /img/runns-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sh1r4s3/runns/76070c10c1679533f081c50001427a3205cd3c08/img/runns-logo.png -------------------------------------------------------------------------------- /img/runns-logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sh1r4s3/runns/76070c10c1679533f081c50001427a3205cd3c08/img/runns-logo.xcf -------------------------------------------------------------------------------- /runns.c: -------------------------------------------------------------------------------- 1 | /* 2 | * vim:et:sw=2: 3 | * 4 | * Copyright (c) 2019-2025 Nikita Ermakov 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include "runns.h" 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | // Emit log message 25 | #define ERR(format, ...) \ 26 | do { \ 27 | syslog(LOG_INFO | LOG_DAEMON, "runns.c:%d / errno=%d / " format "\n", __LINE__, errno, ##__VA_ARGS__); \ 28 | stop_daemon(0); \ 29 | } while (0) 30 | 31 | #define WARN(format, ...) \ 32 | do { \ 33 | syslog(LOG_INFO | LOG_DAEMON, "runns.c:%d / warning / " format "\n", __LINE__, ##__VA_ARGS__); \ 34 | } while (0) 35 | 36 | #define INFO(format, ...) \ 37 | { \ 38 | syslog(LOG_INFO | LOG_DAEMON, "runns.c:%d / info / " format "\n", __LINE__, ##__VA_ARGS__); \ 39 | } while (0) 40 | 41 | struct runns_header hdr = {0}; 42 | int sockfd = 0; 43 | struct runns_child childs[MAX_CHILDS] = {0}; 44 | unsigned int childs_run = 0; 45 | char *program = 0; 46 | char *netns = 0; 47 | char *resolv = 0; 48 | char **args = 0; 49 | char **envs = 0; 50 | int *glob_pid = 0; 51 | char runns_socket[PATH_MAX] = DEFAULT_RUNNS_SOCKET; 52 | char runns_socket_dir[PATH_MAX] = {0}; 53 | enum is_default_dir {default_dir, not_default_dir} defdir = default_dir; 54 | struct ucred cred; 55 | 56 | int drop_priv(uid_t _uid); 57 | void stop_daemon(int flag); 58 | int clean_pids(); 59 | void free_tvars(); 60 | int create_ptms(); 61 | int clean_socket(); 62 | int parse_flag(int data_sockfd); 63 | int do_netns(int data_sockfd); 64 | 65 | 66 | struct option opts[] = 67 | { 68 | { .name = "help", .has_arg = 0, .flag = 0, .val = 'h' }, 69 | { .name = "dir", .has_arg = 1, .flag = 0, .val = 'd' }, 70 | { .name = "socket", .has_arg = 1, .flag = 0, .val = 's' }, 71 | { 0, 0, 0, 0 } 72 | }; 73 | 74 | void help_me() { 75 | const char *hstr = \ 76 | "runns [options]\n" \ 77 | "Options:\n" \ 78 | "-h|--help help\n" \ 79 | "-s|--socket override default runns socket path (" DEFAULT_RUNNS_SOCKET ")\n"; 80 | 81 | puts(hstr); 82 | exit(EXIT_SUCCESS); 83 | } 84 | 85 | 86 | int main(int argc, char **argv) { 87 | const char *optstring = "hs:"; 88 | int opt; 89 | int len; 90 | 91 | while ((opt = getopt_long(argc, argv, optstring, opts, 0)) != -1) { 92 | switch (opt) { 93 | case 'h': 94 | help_me(); 95 | break; 96 | case 's': 97 | // Get absolute path 98 | if (!realpath(dirname(optarg), runns_socket)) { 99 | fputs("Can't get a real path\n", stderr); 100 | ERR("Can't get a real path of the socket filename"); 101 | } 102 | len = strlen(runns_socket); 103 | runns_socket[len] = '/'; 104 | runns_socket[++len] = '\0'; 105 | 106 | strncat(runns_socket, 107 | basename(optarg), 108 | PATH_MAX - len - 1 /* subtract len and new line */); 109 | len = strlen(runns_socket); 110 | if (len >= RUNNS_MAXLEN) { 111 | fputs("Socket file name is too long > " STR_TOKEN(RUNNS_MAXLEN) "\n", stderr); 112 | ERR("Socket file name length is greater than " STR_TOKEN(RUNNS_MAXLEN)); 113 | } 114 | defdir = not_default_dir; 115 | break; 116 | default: 117 | ERR("Wrong option: %c", (char)opt); 118 | } 119 | } 120 | 121 | struct sockaddr_un addr = {.sun_family = AF_UNIX, .sun_path = {0}}; 122 | memcpy(addr.sun_path, runns_socket, strlen(runns_socket) + 1); 123 | char *last_slash = strrchr(runns_socket, '/'); 124 | if (!last_slash) 125 | ERR("Can't deduce directory name in %s", runns_socket); 126 | memcpy(runns_socket_dir, runns_socket, last_slash - runns_socket); 127 | 128 | // Check the root 129 | if (getuid() != 0) { 130 | extern FILE *stderr; 131 | fputs("Please run as a root user\n", stderr); 132 | ERR("Only root can run the runns daemon"); 133 | } 134 | 135 | glob_pid = mmap(NULL, sizeof(glob_pid), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); 136 | if (!glob_pid) 137 | ERR("Can't allocate memory"); 138 | 139 | if (daemon(0, 0)) 140 | ERR("Can't daemonize the process"); 141 | 142 | // Set safe umask and create directory. 143 | umask(0022); 144 | if (defdir == default_dir) { 145 | if (!access(runns_socket, F_OK)) { 146 | WARN("Old socket file %s has been found", runns_socket); 147 | if (unlink(runns_socket)) 148 | ERR("Can't remove the socket file"); 149 | else 150 | INFO("Old socket file has been removed"); 151 | } 152 | else { 153 | if (access(runns_socket_dir, F_OK)) { 154 | if (mkdir(runns_socket_dir, 0755) < 0) 155 | ERR("Can't create directory %s", runns_socket_dir); 156 | } 157 | } 158 | } 159 | 160 | // Up daemon socket. 161 | sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); 162 | if (sockfd == -1) 163 | ERR("Something gone very wrong, socket = %d", sockfd); 164 | if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) 165 | ERR("Can't bind socket to %s", addr.sun_path); 166 | // Switch permissions and group. 167 | struct group *group; 168 | group = getgrnam("runns"); 169 | if (!group) 170 | ERR("Can't get runns group\n"); 171 | if (chown(addr.sun_path, 0, group->gr_gid) || 172 | chmod(addr.sun_path, 0775)) { 173 | 174 | ERR("Can't chown/chmod"); 175 | } 176 | 177 | INFO("runns daemon has started"); 178 | 179 | while (1) { 180 | if (listen(sockfd, 16) == -1) 181 | ERR("Can't start listen socket %d (%s)", sockfd, addr.sun_path); 182 | 183 | int data_sockfd = accept(sockfd, 0, 0); 184 | if (data_sockfd == -1) 185 | ERR("Can't accept connection"); 186 | const socklen_t cred_len = (socklen_t)sizeof(struct ucred); 187 | if (getsockopt(data_sockfd, SOL_SOCKET, SO_PEERCRED, &cred, &cred_len) == -1) { 188 | WARN("Can't get user credentials"); 189 | close(data_sockfd); 190 | continue; 191 | } 192 | 193 | int ret = read(data_sockfd, (void *)&hdr, sizeof(hdr)); 194 | if (ret == -1) 195 | WARN("Can't read data"); 196 | 197 | if (parse_flag(data_sockfd)) 198 | continue; 199 | 200 | // Read the operational mode 201 | INFO("op mode = %d", hdr.op_mode); 202 | switch (hdr.op_mode) { 203 | case OP_MODE_FWD_PORT: 204 | // TODO: Handle OP_MODE_FWD_PORT 205 | INFO("Nothing can be done for OP_MODE_FWD_PORT yet. Skip"); 206 | break; 207 | case OP_MODE_NETNS: 208 | do_netns(data_sockfd); 209 | break; 210 | default: 211 | WARN("Skipping. Unknown op mode"); 212 | } 213 | 214 | } 215 | 216 | return 0; 217 | } 218 | 219 | int drop_priv(uid_t _uid) { 220 | struct passwd *pw = getpwuid(_uid); 221 | if (pw) { 222 | uid_t uid = pw->pw_uid; 223 | gid_t gid = pw->pw_gid; 224 | 225 | if (initgroups(pw->pw_name, gid) != 0) 226 | ERR("Couldn't initialize the supplementary group list"); 227 | endpwent(); 228 | 229 | if (setgid(gid) != 0 || setuid(uid) != 0) { 230 | ERR("Couldn't change to '%.32s' uid=%lu gid=%lu", 231 | pw->pw_name, 232 | (unsigned long)uid, 233 | (unsigned long)gid); 234 | } 235 | 236 | if (chdir(pw->pw_dir)) { 237 | ERR("Couldn't chdir to %s for '%.32s' uid=%lu gid=%lu", 238 | pw->pw_dir, 239 | pw->pw_name, 240 | (unsigned long)uid, 241 | (unsigned long)gid); 242 | } 243 | } 244 | else 245 | ERR("Couldn't find a user with UID=%d", _uid); 246 | } 247 | 248 | 249 | void stop_daemon(int flag) { 250 | INFO("runns daemon going down"); 251 | if (sockfd) { 252 | close(sockfd); 253 | unlink(runns_socket); 254 | if (defdir == default_dir) 255 | rmdir(runns_socket_dir); 256 | } 257 | free_tvars(); 258 | munmap(glob_pid, sizeof(glob_pid)); 259 | 260 | int ret = flag ? flag & RUNNS_STOP : EXIT_FAILURE; 261 | exit(ret); 262 | } 263 | 264 | int clean_pids() { 265 | for (unsigned int i = childs_run; i > 0 ; i--) 266 | { 267 | if (kill(childs[i - 1].pid, 0)) 268 | { 269 | if (i != childs_run) 270 | { 271 | memcpy(childs + i - 1, childs + childs_run - 1, sizeof(struct runns_child)); 272 | } 273 | --childs_run; 274 | } 275 | } 276 | } 277 | 278 | 279 | void free_tvars() { 280 | free(program); 281 | free(netns); 282 | if (resolv) { 283 | free(resolv); 284 | resolv = 0; 285 | } 286 | if (args) 287 | for (size_t i = 1; i < hdr.args_sz - 3; free(args[i++])); 288 | if (envs) 289 | for (size_t i = 0; i < hdr.env_sz - 2; free(envs[i++])); 290 | free(envs); 291 | free(args); 292 | memset((void *)&hdr, 1, sizeof(hdr)); 293 | } 294 | 295 | 296 | int create_ptms() { 297 | int ptmfd = open("/dev/ptmx", O_RDWR); 298 | char ptsname[0xff]; 299 | if (ptsname_r(ptmfd, ptsname, 0xff)) { 300 | WARN("Fail to get ptsname, errno=%d", errno); 301 | return errno; 302 | } 303 | if (grantpt(ptmfd)) { 304 | WARN("Fail to grant access to the slave pt, errno=%d", errno); 305 | return errno; 306 | } 307 | if (unlockpt(ptmfd)) { 308 | WARN("Fail to unlock a pt, errno=%d", errno); 309 | return errno; 310 | } 311 | int ptsfd = open(ptsname, O_RDWR); 312 | tcsetattr(ptsfd, TCSANOW, &hdr.tmode); 313 | if (dup2(ptsfd, STDIN_FILENO) == -1) { 314 | WARN("Fail to dup2 pt for stdin, errno=%d", errno); 315 | return errno; 316 | } 317 | if (dup2(ptsfd, STDOUT_FILENO) == -1) { 318 | WARN("Fail to dup2 pt for stdout, errno=%d", errno); 319 | return errno; 320 | } 321 | if (dup2(ptsfd, STDERR_FILENO) == -1) { 322 | WARN("Fail to dup2 pt for stderr, errno=%d", errno); 323 | return errno; 324 | } 325 | 326 | return 0; 327 | } 328 | 329 | 330 | int parse_flag(int data_sockfd) { 331 | // Stop daemon on demand. 332 | if (hdr.flag & RUNNS_STOP) { 333 | if (cred.uid == 0) { 334 | INFO("closing"); 335 | close(data_sockfd); 336 | stop_daemon(hdr.flag); // Should never returns 337 | } 338 | else { 339 | WARN("Client with %d UID tried to kill the daemon ", cred.uid); 340 | return 1; 341 | } 342 | } 343 | 344 | // Transfer list of childs 345 | if (hdr.flag & RUNNS_LIST) { 346 | // TODO rets 347 | INFO("uid=%d ask for pid list", cred.uid); 348 | clean_pids(); 349 | // Count the number of jobs for uid 350 | unsigned int jobs = 0; 351 | for (int i = 0; i < childs_run; i++) { 352 | if (childs[i].uid == cred.uid) 353 | ++jobs; 354 | } 355 | 356 | if (write(data_sockfd, (void *)&jobs, sizeof(jobs)) == -1) 357 | ERR("Can't send number of jobs to the client %d", cred.uid); 358 | for (unsigned int i = 0; i < jobs; i++) { 359 | if (write(data_sockfd, (void *)&childs[i], sizeof(struct runns_child)) == -1) 360 | ERR("Can't send child info to the client %d", cred.uid); 361 | } 362 | close(data_sockfd); 363 | return 1; 364 | } 365 | 366 | return 0; 367 | } 368 | 369 | 370 | int do_netns(int data_sockfd) { 371 | int ret; 372 | 373 | // Read program name and network namespace name 374 | program = (char *)malloc(hdr.prog_sz); 375 | netns = (char *)malloc(hdr.netns_sz); 376 | if (!program || !netns) 377 | ERR("Can't allocate memory (program=%p, netns=%p", program, netns); 378 | if (hdr.resolv_sz) { 379 | resolv = (char *)malloc(hdr.resolv_sz); 380 | if (!resolv) { 381 | ERR("Can't allocate memory for resolv.conf path"); 382 | } 383 | } 384 | ret = read(data_sockfd, (void *)program, hdr.prog_sz); 385 | ret = read(data_sockfd, (void *)netns, hdr.netns_sz); 386 | if (hdr.resolv_sz) { 387 | ret = read(data_sockfd, (void *)resolv, hdr.resolv_sz); 388 | } 389 | INFO("uid=%d program=%s netns=%s resolv=%s", cred.uid, program, netns, hdr.resolv_sz ? resolv : "inherited"); 390 | 391 | // Read argv for the program 392 | if (hdr.args_sz) { 393 | hdr.args_sz += 2; // program name + null at the end 394 | args = (char **)malloc(hdr.args_sz*sizeof(char *)); 395 | if (!args) 396 | ERR("Can't allocate memory (args = %p)", args); 397 | args[0] = program; 398 | args[hdr.args_sz - 1] = 0; 399 | for (int i = 1; i < hdr.args_sz - 1; i++) { 400 | size_t sz; 401 | ret = read(data_sockfd, (void *)&sz, sizeof(size_t)); 402 | args[i] = (char *)malloc(sz); 403 | ret = read(data_sockfd, (void *)args[i], sz); 404 | } 405 | } 406 | else 407 | args = 0; 408 | 409 | // Read environment variables 410 | if (hdr.env_sz) { 411 | envs = (char **)malloc(++hdr.env_sz*sizeof(char *)); 412 | for (int i = 0; i < hdr.env_sz - 1; i++) { 413 | size_t env_sz; 414 | ret = read(data_sockfd, (void *)&env_sz, sizeof(size_t)); 415 | envs[i] = (char *)malloc(env_sz); 416 | ret = read(data_sockfd, (void *)envs[i], env_sz); 417 | } 418 | envs[hdr.env_sz - 1] = 0; 419 | } 420 | else 421 | envs = 0; 422 | 423 | close(data_sockfd); 424 | 425 | clean_pids(); 426 | if (childs_run < MAX_CHILDS) { 427 | // Make fork 428 | pid_t child = fork(); 429 | if (child == -1) 430 | ERR("Fail on fork"); 431 | 432 | // Child 433 | if (child == 0) { 434 | child = fork(); 435 | if (child != 0) { 436 | *glob_pid = child; 437 | exit(0); 438 | } 439 | 440 | // Un-map shared memory from the parent. 441 | munmap(glob_pid, sizeof(glob_pid)); 442 | 443 | // Detach child from parent 444 | setsid(); 445 | 446 | // Redirect stdin, stdout, stderr to new PTS 447 | if (hdr.flag & RUNNS_NPTMS) { 448 | int err; 449 | if (err = create_ptms()) { 450 | exit(err); 451 | } 452 | } 453 | 454 | // Set netns 455 | int netfd = open(netns, 0); 456 | setns(netfd, CLONE_NEWNET); 457 | 458 | // Unshare mount namespace 459 | if (resolv) { 460 | if (unshare(CLONE_NEWNS) < 0) { 461 | WARN("Can't unshare mount namespace, errno=%d", errno); 462 | exit(EXIT_FAILURE); 463 | } 464 | 465 | if (mount(resolv, "/etc/resolv.conf", NULL, MS_BIND, NULL) < 0) { 466 | WARN("Can't mount %s to /etc/resolv.conf, errno=%d", resolv, errno); 467 | exit(EXIT_FAILURE); 468 | } 469 | } 470 | 471 | // Drop privileges and execute command 472 | drop_priv(cred.uid); 473 | if (execve(program, (char * const *)args, (char * const *)envs) == -1) { 474 | WARN("Can not run %s, execve failed with errno=%d", program, errno); 475 | exit(EXIT_FAILURE); 476 | } 477 | } 478 | else 479 | waitpid(child, 0, 0); 480 | 481 | // Save child. 482 | INFO("Forked %d", *glob_pid); 483 | childs[childs_run].uid = cred.uid; 484 | childs[childs_run].pid = *glob_pid; 485 | ++childs_run; 486 | free_tvars(); 487 | } 488 | else 489 | INFO("Maximum number of childs has been reached."); 490 | } 491 | -------------------------------------------------------------------------------- /runns.h: -------------------------------------------------------------------------------- 1 | /* 2 | * vim:et:sw=2: 3 | * 4 | * Copyright (c) 2019-2025 Nikita Ermakov 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | #ifndef RUNNS_H 9 | #define RUNNS_H 10 | 11 | #define __USE_GNU 12 | #define _XOPEN_SOURCE 13 | #define __USE_XOPEN_EXTENDED 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #define STR_TOKEN(x) #x 29 | 30 | // Linux socket default file. 31 | #define DEFAULT_RUNNS_SOCKET "/var/run/runns/runns.socket" 32 | #define RUNNS_MAXLEN sizeof(((struct sockaddr_un *)0)->sun_path) 33 | 34 | // Maximum number of childs 35 | #define MAX_CHILDS 1024 36 | 37 | // librunns 38 | #define ENV_SEPARATOR ':' 39 | 40 | // Definitions of the flag bits: 41 | // RUNNS_STOP -- wait for childs to exit and then exit. 42 | // RUNNS_LIST -- list childs runned by runns. 43 | // RUNNS_NPTS -- create control terminal for forked process. 44 | #define RUNNS_STOP (int)1 << 1 45 | #define RUNNS_LIST (int)1 << 2 46 | #define RUNNS_NPTMS (int)1 << 3 47 | 48 | typedef enum { 49 | OP_MODE_UNK = 0, 50 | OP_MODE_NETNS, 51 | OP_MODE_FWD_PORT 52 | } OP_MODES; 53 | 54 | // common header for server and client 55 | struct runns_header { 56 | size_t prog_sz; 57 | size_t netns_sz; 58 | size_t resolv_sz; 59 | size_t env_sz; 60 | size_t args_sz; 61 | unsigned int flag; 62 | struct termios tmode; 63 | OP_MODES op_mode; 64 | }; 65 | 66 | struct runns_child { 67 | uid_t uid; 68 | pid_t pid; 69 | }; 70 | 71 | // Structures for librunns 72 | typedef enum { 73 | L4_PROTOCOL_UNK = 0, 74 | L4_PROTOCOL_TCP, 75 | L4_PROTOCOL_UDP 76 | } L4_PROTOCOLS; 77 | 78 | struct netns { 79 | unsigned char ip[sizeof(struct in6_addr)]; 80 | int fd; // File descriptor to clone a netns from 81 | sa_family_t family; // AF_INET or AF_INET6 82 | L4_PROTOCOLS proto; // TCP, UDP or unknown 83 | in_port_t port; 84 | }; 85 | 86 | struct netns_list { 87 | struct netns node; 88 | struct netns_list *pnext; 89 | }; 90 | 91 | #endif 92 | -------------------------------------------------------------------------------- /runnsctl.c: -------------------------------------------------------------------------------- 1 | /* 2 | * vim:et:sw=2: 3 | * 4 | * Copyright (c) 2019-2025 Nikita Ermakov 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | 8 | #include "runns.h" 9 | #include 10 | #include 11 | #include 12 | 13 | #define CLIENT_NAME "runnsctl" 14 | 15 | /* 16 | * Macros for logging 17 | */ 18 | void cleanup(); 19 | #define ERR(format, ...) \ 20 | do { \ 21 | fprintf(stderr, CLIENT_NAME ":%d / errno=%d / " format "\n", __LINE__, errno, ##__VA_ARGS__); \ 22 | cleanup(); \ 23 | exit(0); \ 24 | } while (0) 25 | #define WARN(format, ...) \ 26 | do { \ 27 | fprintf(stderr, CLIENT_NAME ":%d / warning / " format "\n", __LINE__, ##__VA_ARGS__); \ 28 | } while (0) 29 | #define INFO(format, ...) \ 30 | do { \ 31 | printf(CLIENT_NAME ":%d / info / " format "\n", __LINE__, ##__VA_ARGS__); \ 32 | } while (0) 33 | // This macro could be set from the configure script 34 | #ifdef ENABLE_DEBUG 35 | # define DEBUG(format, ...) \ 36 | do { \ 37 | printf(CLIENT_NAME ":%d / debug / " format "\n", __LINE__, ##__VA_ARGS__); \ 38 | } while (0) 39 | #else 40 | # define DEBUG(...) 41 | #endif 42 | 43 | /* 44 | * Type definitions 45 | */ 46 | enum wide_opts { 47 | OPT_SET_NETNS = 0xFF01, 48 | OPT_RESOLV = 0xFF02, 49 | OPT_SOCKET = 0xFFAA 50 | }; 51 | 52 | /* 53 | * Global variables 54 | */ 55 | int netns_size = 0; 56 | int sockfd = 0; 57 | struct netns_list *ns_head = NULL; 58 | struct runns_header hdr = {0}; 59 | const char *prog = 0, *netns = 0, *resolv = 0; 60 | struct sockaddr_un addr = {.sun_family = AF_UNIX, .sun_path = DEFAULT_RUNNS_SOCKET}; 61 | char verbose = 0; 62 | extern char **environ; 63 | 64 | void help_me() { 65 | const char *hstr = \ 66 | "client [options] \n" \ 67 | "Options: \n" \ 68 | "-h|--help help\n" \ 69 | "-s|--stop stop daemon (only root)\n" \ 70 | "-l|--list list childs\n" \ 71 | "-p|--program program to run in desired netns\n" \ 72 | "-t|--create-ptms create control terminal\n" \ 73 | "-f|--forward-port :::\n" \ 74 | " could be 4 or 6\n" \ 75 | " path to the netns fd\n" \ 76 | "--set-netns network namespace to switch\n" \ 77 | "--resolv path to resolv.conf to be used in program" \ 78 | "--socket path to the runns socket\n" \ 79 | "-v|--verbose be verbose\n"; 80 | 81 | puts(hstr); 82 | exit(EXIT_SUCCESS); 83 | } 84 | 85 | void parse_l4_proto(char *l4_proto_str, L4_PROTOCOLS *l4_proto, sa_family_t *family) { 86 | // Basic checks and calc the size of the str 87 | if (!l4_proto_str || !l4_proto || !family) { 88 | ERR("NULL slipped in 0x%x 0x%x 0x%x", l4_proto_str, l4_proto, family); 89 | } 90 | *l4_proto_str++ = '\0'; 91 | *family = AF_INET; 92 | *l4_proto = L4_PROTOCOL_UNK; 93 | size_t l4_proto_str_sz = strlen(l4_proto_str); 94 | if (l4_proto_str_sz != 4) { 95 | WARN("L4 protocol has wrong size %d != 4. Skip", l4_proto_str_sz); 96 | return; 97 | } 98 | // Get IP family 99 | switch (l4_proto_str[3]) { 100 | case '4': 101 | *family = AF_INET; 102 | break; 103 | case '6': 104 | *family = AF_INET6; 105 | break; 106 | default: 107 | WARN("L4 protocol has wrong IP family %c (only 4 and 6 are allowed). Fallback to 4", l4_proto_str[3]); 108 | } 109 | // Get proto 110 | if (strncasecmp(l4_proto_str, "tcp", 3) == 0) { 111 | *l4_proto = L4_PROTOCOL_TCP; 112 | } else if (strncasecmp(l4_proto_str, "udp", 3) == 0) { 113 | *l4_proto = L4_PROTOCOL_UDP; 114 | } else { 115 | WARN("L4 protocol %s is not correct", l4_proto_str); 116 | } 117 | 118 | DEBUG("l4_proto_str=%s l4_proto=%d family=%d", l4_proto_str, l4_proto, family); 119 | } 120 | 121 | void add_netns(char *ip) { 122 | if (!ip) { 123 | ERR("forward string is empty"); 124 | } 125 | char *port = NULL, *netns_path = NULL; 126 | size_t ip_len = strlen(ip); 127 | port = strchr(ip, ENV_SEPARATOR); 128 | if (port && (size_t)port < ((size_t)ip + ip_len)) 129 | netns_path = strchr(port + 1, ENV_SEPARATOR); 130 | if (!port || !netns_path) { // Mandatory fields 131 | WARN("skipping %s -- port and netns_path are mandatory fields", ip); 132 | return; 133 | } 134 | *port++ = '\0'; 135 | *netns_path++ = '\0'; 136 | // An optional field for level 4 protocols according to the OSI model 137 | char *l4_proto_str = strchr(netns_path + 1, ENV_SEPARATOR); 138 | L4_PROTOCOLS l4_proto = L4_PROTOCOL_UNK; 139 | sa_family_t family = AF_UNSPEC; 140 | parse_l4_proto(l4_proto_str, &l4_proto, &family); 141 | 142 | // Open a netns file to get a fd 143 | int netns_fd = open(netns_path, 0); 144 | if (netns_fd < 0) { 145 | ERR("Can't open netns %s errno: %s", netns_path, strerror(errno)); 146 | } 147 | 148 | // Create a new netns 149 | struct netns_list *ns = (struct netns_list *)malloc(sizeof(struct netns_list)); 150 | inet_pton(family, ip, ns->node.ip); 151 | ns->node.port = atoi(port); 152 | ns->node.fd = netns_fd; 153 | ns->node.family = family; 154 | ns->node.proto = l4_proto; 155 | ns->pnext = NULL; 156 | DEBUG("adding port=%d ip=%s(0x%x), netns=%s, proto=%d", ns->node.port, ip, *((int *)ns->node.ip), ns->node.netns, ns->node.proto); 157 | ++netns_size; 158 | // Insert into the list of netns 159 | if (ns_head) { 160 | struct netns_list *p; 161 | for (p = ns_head; p != NULL; p = p->pnext); 162 | p->pnext = ns; 163 | } else { 164 | ns_head = ns; 165 | } 166 | } 167 | 168 | void cleanup() { 169 | for (struct netns_list *p = ns_head; p != NULL;) { 170 | struct netns_list *ptmp = p->pnext; 171 | free(p); 172 | p = ptmp; 173 | } 174 | 175 | if (sockfd) 176 | close(sockfd); 177 | } 178 | 179 | void send_netns(int argc, char **argv) { 180 | // TODO: either transer prog + netns or a list of netns 181 | // this should depend on the current operation mode 182 | // OP_MODE_FWD_PORT -- forward ports (a list of netns) 183 | // OP_MODE_NETNS -- the "classic" mode to run prog in netns 184 | if (write(sockfd, (void *)prog, hdr.prog_sz) == -1 || 185 | write(sockfd, (void *)netns, hdr.netns_sz) == -1) { 186 | 187 | ERR("Can't send program name or network namespace name to the daemon"); 188 | } 189 | 190 | if (resolv) { 191 | if (write(sockfd, (void *)resolv, hdr.resolv_sz) == -1) { 192 | ERR("Can't send path to resolv.conf"); 193 | } 194 | } 195 | 196 | // Transfer argv 197 | if (hdr.args_sz > 0) { 198 | for (int i = optind; i < argc; i++) { 199 | size_t sz = strlen(argv[i]) + 1; // strlen + \0 200 | if (write(sockfd, (void *)&sz, sizeof(size_t)) == -1 || 201 | write(sockfd, (void *)argv[i], sz) == -1) { 202 | 203 | ERR("Can't send argv to the daemon"); 204 | } 205 | } 206 | } 207 | 208 | // Transfer environment variables 209 | for (int i = 0; i < hdr.env_sz; i++) { 210 | size_t sz = strlen(environ[i]) + 1; // strlen + \0 211 | if (write(sockfd, (void *)&sz, sizeof(size_t)) == -1 || 212 | write(sockfd, (void *)environ[i], sz) == -1) { 213 | 214 | ERR("Can't send envs to the daemon"); 215 | } 216 | } 217 | int eof = 0; 218 | if (write(sockfd, &eof, sizeof(int)) == -1) 219 | ERR("Can't send EOF to the daemon"); 220 | } 221 | 222 | void parse_cmdline(int argc, char **argv) { 223 | struct option opts[] = { 224 | { .name = "help", .has_arg = 0, .flag = 0, .val = 'h' }, 225 | { .name = "program", .has_arg = 1, .flag = 0, .val = 'p' }, 226 | { .name = "verbose", .has_arg = 0, .flag = 0, .val = 'v' }, 227 | { .name = "stop", .has_arg = 0, .flag = 0, .val = 's' }, 228 | { .name = "list", .has_arg = 0, .flag = 0, .val = 'l' }, 229 | { .name = "create-ptms", .has_arg = 0, .flag = 0, .val = 't' }, 230 | { .name = "forward-port", .has_arg = 1, .flag = 0, .val = 'f' }, 231 | { .name = "set-netns", .has_arg = 1, .flag = 0, .val = OPT_SET_NETNS }, 232 | { .name = "resolv", .has_arg = 1, .flag = 0, .val = OPT_RESOLV }, 233 | { .name = "socket", .has_arg = 1, .flag = 0, .val = OPT_SOCKET }, 234 | { 0, 0, 0, 0 } 235 | }; 236 | const char *optstring = "hp:vsltf:"; 237 | int opt, len; 238 | // Parse command line options 239 | if (argc <= 1) 240 | ERR("For the help message try: runnsctl --help"); 241 | 242 | while ((opt = getopt_long(argc, argv, optstring, opts, 0)) != -1) { 243 | switch (opt) { 244 | case 'h': 245 | help_me(); 246 | break; 247 | case 's': 248 | hdr.flag |= RUNNS_STOP; 249 | break; 250 | case 'l': 251 | hdr.flag |= RUNNS_LIST; 252 | break; 253 | case 'p': 254 | prog = optarg; 255 | hdr.prog_sz = strlen(prog) + 1; 256 | break; 257 | case 't': 258 | hdr.flag |= RUNNS_NPTMS; 259 | break; 260 | case 'f': 261 | if (hdr.op_mode == OP_MODE_NETNS) { 262 | ERR("--forward-port and --set-netns mutually exclusive"); 263 | } 264 | hdr.op_mode = OP_MODE_FWD_PORT; 265 | add_netns(optarg); 266 | break; 267 | case 'v': 268 | verbose = 1; 269 | break; 270 | case OPT_SET_NETNS: 271 | if (hdr.op_mode == OP_MODE_FWD_PORT) { 272 | ERR("--forward-port and --set-netns mutually exclusive"); 273 | } 274 | hdr.op_mode = OP_MODE_NETNS; 275 | netns = optarg; 276 | hdr.netns_sz = strlen(netns) + 1; 277 | break; 278 | case OPT_RESOLV: 279 | resolv = optarg; 280 | hdr.resolv_sz = strlen(resolv) + 1; 281 | break; 282 | case OPT_SOCKET: 283 | len = strlen(optarg); 284 | if (len >= RUNNS_MAXLEN) 285 | ERR("Socket file name is too long > " STR_TOKEN(RUNNS_MAXLEN) "\n"); 286 | memcpy(addr.sun_path, optarg, len + 1); 287 | break; 288 | default: 289 | ERR("Wrong option: %c", (char)opt); 290 | } 291 | } 292 | } 293 | 294 | // Don't define main() for unit tests 295 | #ifndef TAU_TEST 296 | int main(int argc, char **argv) { 297 | parse_cmdline(argc, argv); 298 | // Sanity checks 299 | if (hdr.op_mode == OP_MODE_FWD_PORT && !ns_head) { 300 | ERR("Nothing to forward"); 301 | } 302 | if (hdr.op_mode == OP_MODE_NETNS && !hdr.flag && (!netns || !prog)) { 303 | ERR("Please check that you set network namespace and program"); 304 | } 305 | 306 | // Output parameters in the case of verbose option 307 | if (netns && verbose) { 308 | if (hdr.flag & (RUNNS_STOP | RUNNS_LIST)) { // flags related to runns daemon 309 | char *str = NULL; 310 | 311 | if (hdr.flag & RUNNS_STOP) str = "RUNNS_STOP"; 312 | if (hdr.flag & RUNNS_LIST) str = "RUNNS_LIST"; 313 | if (str) 314 | printf("Command to runns daemon: %s\n", str); 315 | } else { 316 | printf("network namespace to switch is: %s\n" 317 | "program to run: %s\n", 318 | netns, prog); 319 | } 320 | } 321 | 322 | // Count number of environment variables 323 | for (hdr.env_sz = 0; environ[hdr.env_sz] != 0; ++hdr.env_sz); 324 | 325 | // Up socket 326 | sockfd = socket(AF_UNIX, SOCK_STREAM, 0); 327 | if (sockfd == -1) { 328 | ERR("Something gone very wrong, socket = %d", sockfd); 329 | } 330 | if (connect(sockfd, (const struct sockaddr *)&addr, sizeof(addr)) == -1) { 331 | ERR("Can't connect to runns daemon"); 332 | } 333 | // Calculate number of non-options 334 | hdr.args_sz = argc - optind; 335 | 336 | // Get termios 337 | tcgetattr(STDIN_FILENO, &hdr.tmode); 338 | 339 | if (write(sockfd, (void *)&hdr, sizeof(hdr)) == -1) 340 | ERR("Can't send header to the daemon"); 341 | // Stop daemon 342 | if (hdr.flag & RUNNS_STOP) { 343 | cleanup(); 344 | return EXIT_SUCCESS; 345 | } 346 | // Print list of children and exit 347 | if (hdr.flag & RUNNS_LIST) { 348 | unsigned int childs_run; 349 | struct runns_child child; 350 | if (read(sockfd, (void *)&childs_run, sizeof(childs_run)) == -1) 351 | ERR("Can't read number of childs from the daemon"); 352 | for (int i = 0; i < childs_run; i++) { 353 | if (read(sockfd, (void *)&child, sizeof(child)) == -1) 354 | ERR("Can't read child info from the daemon"); 355 | printf("%d\n", child.pid); 356 | } 357 | cleanup(); 358 | return EXIT_SUCCESS; 359 | } 360 | 361 | switch (hdr.op_mode) { 362 | case OP_MODE_NETNS: 363 | send_netns(argc, argv); 364 | break; 365 | default: 366 | ERR("Unknown OP_MODE: %d", hdr.op_mode); 367 | } 368 | 369 | cleanup(); 370 | return EXIT_SUCCESS; 371 | } 372 | #endif 373 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | include ../config.mk 2 | 3 | all: build run 4 | 5 | run: build 6 | @ find . -maxdepth 1 -type f -executable | while read test_file; do \ 7 | echo -e "===\n=== starting $$test_file\n==="; \ 8 | ./$$test_file || :; \ 9 | done; 10 | 11 | build: test_queue test_runnsctl 12 | 13 | test_%: ../%.c %.c 14 | $(CC) -DTAU_TEST -I.. -I../tau/ -o test_$@ $^ 15 | 16 | .PHONY: clean 17 | clean: 18 | find . -maxdepth 1 -executable -type f -delete 19 | -------------------------------------------------------------------------------- /tests/prepare_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | 3 | TEST_NETNS=test_netns 4 | VETH_DEF=test_veth_default 5 | VETH_NETNS=test_veth_netns 6 | 7 | fatal() { 8 | printf "fatal error: %s\n" "${1-}" 9 | exit 1 10 | } 11 | 12 | clean() { 13 | ip netns exec "$TEST_NETNS" ip link del "$VETH_NETNS" || : 14 | ip link del "$VETH_DEF" || : 15 | ip netns del "$TEST_NETNS" || : 16 | } 17 | 18 | trap clean EXIT HUP PIPE INT QUIT TERM 19 | 20 | # Check user 21 | [ "$UID" = "0" ] || fatal "User $USER is not root" 22 | 23 | # Create NETNS and test veth interfaces 24 | ip netns add "$TEST_NETNS" 25 | ip link add "$VETH_DEF" type veth peer name "$VETH_NETNS" 26 | ip link set "$VETH_NETNS" netns "$TEST_NETNS" 27 | 28 | # Set IPs 29 | ip addr add "172.24.0.1/24" dev "$VETH_DEF" 30 | ip netns exec "TEST_NETNS" ip addr add "172.24.1.2/24" dev "$VETH_NETNS" 31 | ip netns exec "TEST_NETNS" ip link set "$VETH_NETNS" up 32 | ip link set "$VETH_DEF" up 33 | 34 | # Enable IPv4 forwarding 35 | echo "1" > /proc/sys/net/ipv4/ip_forward 36 | -------------------------------------------------------------------------------- /tests/queue.c: -------------------------------------------------------------------------------- 1 | /* 2 | * vim:et:sw=2: 3 | * 4 | * Copyright (c) 2023-2025 Nikita Ermakov 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | #include 8 | #include "tau/tau.h" 9 | #include "queue.h" 10 | 11 | TAU_MAIN(); 12 | 13 | TEST(queue, open_close) { 14 | const char qname[] = "/runnsqueue"; 15 | struct queue *q = queue_open(qname); 16 | REQUIRE(q, "queue_open() returns NULL ptr"); 17 | QUEUE_RET ret = queue_close(q); 18 | REQUIRE(ret == QUEUE_OK); 19 | } 20 | 21 | TEST(queue, send_recv) { 22 | const char qname[] = "/runnsqueue"; 23 | const char test_msg[] = "test message"; 24 | pid_t pid = fork(); 25 | REQUIRE(pid >= 0, "fork failed"); 26 | 27 | if (pid == 0) { // child 28 | struct queue *q = queue_open(qname); 29 | REQUIRE(q, "queue_open() returns NULL ptr"); 30 | 31 | struct queue_msg msg = {0}; 32 | msg.msg = strdup(test_msg); 33 | msg.size = sizeof(test_msg); 34 | QUEUE_RET ret = queue_send(q, &msg); 35 | CHECK(ret == QUEUE_OK, "Can't send message"); 36 | free(msg.msg); 37 | 38 | ret = queue_close(q); 39 | REQUIRE(ret == QUEUE_OK); 40 | } else { // parent 41 | struct queue *q = queue_open(qname); 42 | REQUIRE(q, "queue_open() returns NULL ptr"); 43 | 44 | struct queue_msg *msgs = queue_recv(q); 45 | CHECK(msgs != NULL, "Received NULL"); 46 | if (msgs != NULL) { 47 | CHECK(strcmp(msgs->msg, test_msg) == 0, "Messages are not equal"); 48 | CHECK(msgs->pnext == NULL, "pnext is not NULL"); 49 | queue_free_msgs(msgs); 50 | } 51 | 52 | QUEUE_RET ret = queue_close(q); 53 | REQUIRE(ret == QUEUE_OK); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /tests/runnsctl.c: -------------------------------------------------------------------------------- 1 | /* 2 | * vim:et:sw=2: 3 | * 4 | * Copyright (c) 2023-2025 Nikita Ermakov 5 | * SPDX-License-Identifier: MIT 6 | */ 7 | #include 8 | #include "tau/tau.h" 9 | #include "runns.h" 10 | 11 | TAU_MAIN(); 12 | 13 | TEST(runnsctl, add_netns) { 14 | extern int netns_size; 15 | extern struct netns *ns_head; 16 | void add_netns(char *ip); 17 | 18 | const char ip[] = "127.0.0.1:1234:/foo/bar:TCP4"; 19 | add_netns(ip); 20 | REQUIRE(netns_size == 1); 21 | } 22 | --------------------------------------------------------------------------------