├── .gitignore ├── Makefile ├── README.md └── main.c /.gitignore: -------------------------------------------------------------------------------- 1 | nsroot 2 | *.o 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile taken from http://stackoverflow.com/a/1484873/1390113 2 | 3 | TARGET = nsroot 4 | LIBS = -lm 5 | CC = gcc 6 | CFLAGS = -g -Wall -std=c99 7 | 8 | .PHONY: default all clean 9 | 10 | default: $(TARGET) 11 | all: default 12 | install: default 13 | cp nsroot /usr/local/bin/nsroot 14 | 15 | OBJECTS = $(patsubst %.c, %.o, $(wildcard *.c)) 16 | HEADERS = $(wildcard *.h) 17 | 18 | %.o: %.c $(HEADERS) 19 | $(CC) $(CFLAGS) -c $< -o $@ 20 | 21 | .PRECIOUS: $(TARGET) $(OBJECTS) 22 | 23 | $(TARGET): $(OBJECTS) 24 | $(CC) $(OBJECTS) -Wall $(LIBS) -o $@ 25 | 26 | clean: 27 | -rm -f *.o 28 | -rm -f $(TARGET) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nsroot - Minimalist process isolation tool implemented with Linux namespaces 2 | 3 | ## Usage 4 | 5 | Usage: nsroot [OPTION] NEWROOT [COMMAND [ARG]...] 6 | or: nsroot [OPTION] 7 | OPTION: 8 | -v, --volume Bind mount a directory into a path under NEWROOT. 9 | Syntax: SOURCE:DEST[:OPT] where DEST is relative to 10 | NEWROOT. OPT may be 'ro' (read-only), 'rw' (read/write). 11 | This option may be specified multiple times. 12 | Example values: /home/$USER/private:/mnt 13 | /home/$USER/private:/mnt:ro # for read-only 14 | -o, --old-root=/mnt Where pivot_root should mount the old root before 15 | unmounting it. Path is relative to NEWROOT. 16 | -r --read-only Mount NEWROOT as read-only. 17 | -k --keep-old-root Do not unmount old-root after pivot_root. 18 | -M --uid-map Specify uid-map. See user_namespaces(7) and subuid(5) 19 | for details. 20 | -G --gid-map Specify gid-map. See user_namespaces(7) and subgid(5) 21 | for details. 22 | -n --net Create a new network namespace. 23 | -i --ipc Create a new IPC namespace. 24 | -h, --help 25 | 26 | If no COMMAND is given, run '${SHELL} -i' (default: '/bin/bash -i') 27 | 28 | ## Installation 29 | 30 | ### Prerequisites 31 | 32 | - Linux kernel newer than 3.10 (see *Known issues* about CentOS) 33 | 34 | **For CentOS, see *Known issues*.** 35 | 36 | ### Build and install 37 | 38 | git clone ... 39 | make && make install 40 | 41 | ### Testing the installation 42 | 43 | Simple test of network and IPC namespaces 44 | 45 | nsroot -ni / # Use network and IPC namespaces 46 | ifconfig # should return nothing 47 | 48 | curl google.com # Verify that the network is isolated 49 | curl: (6) Could not resolve host: google.com 50 | 51 | Create a new root file system (using `docker export`): 52 | 53 | # First build a root file system 54 | docker run --name nsroot-test ubuntu echo 55 | docker export nsroot-test > nsroot-test.tar 56 | tar -xf nsroot-test.tar # todo: into `ubuntu` directory 57 | 58 | # Create a directory for the old root filesystem (needed by `pivot_root`) 59 | mkdir ubuntu/.old_root 60 | 61 | Test bind mounting into the new filesystem: 62 | 63 | # Bind mount the `/etc` directory on the host into `/mnt` in the new root 64 | # with read-only permissions (notice the :ro at the end of the argument to -v) 65 | nsroot -o /.old_root -v /etc:/mnt:ro ubuntu 66 | ls /mnt # should list the contents of `/etc` on the host 67 | touch /mnt/test # should fail 68 | touch: cannot touch '/mnt/test': Read-only file system 69 | 70 | ### CentOS 71 | 72 | Currently, `nsroot` does not work on CentOS. 73 | 74 | http://rhelblog.redhat.com/2015/07/07/whats-next-for-containers-user-namespaces/ 75 | 76 | nsroot has been tested and works on Fedora 22, and Ubuntu Linux 14.04. 77 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | The general structure and some code is taken from: http://man7.org/linux/man-pages/man7/user_namespaces.7.html 3 | Introduction to namespaces: https://www.toptal.com/linux/separation-anxiety-isolating-your-system-with-linux-namespaces 4 | Other important sources: 5 | - https://www.ibm.com/developerworks/library/l-mount-namespaces/ 6 | */ 7 | 8 | #define _GNU_SOURCE 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | extern int pivot_root(const char *, const char *); 25 | 26 | const char usage_string[] = "\ 27 | Usage: %s [OPTION] NEWROOT [COMMAND [ARG]...]\n\ 28 | or: %s [OPTION]\n\ 29 | OPTION:\n\ 30 | -v, --volume Bind mount a directory into a path under NEWROOT.\n\ 31 | Syntax: SOURCE:DEST[:OPT] where DEST is relative to\n\ 32 | NEWROOT. OPT may be 'ro' (read-only), 'rw' (read/write).\n\ 33 | This option may be specified multiple times.\n\ 34 | Example values: /home/$USER/private:/mnt\n\ 35 | /home/$USER/private:/mnt:ro # for read-only\n\ 36 | -o, --old-root=/mnt Where pivot_root should mount the old root before\n\ 37 | unmounting it. Path is relative to NEWROOT.\n\ 38 | -r --read-only Mount NEWROOT as read-only.\n\ 39 | -k --keep-old-root Do not unmount old-root after pivot_root.\n\ 40 | -M --uid-map Specify uid-map. See user_namespaces(7) and subuid(5)\n\ 41 | for details.\n\ 42 | -G --gid-map Specify gid-map. See user_namespaces(7) and subgid(5)\n\ 43 | for details.\n\ 44 | -n --net Create a new network namespace.\n\ 45 | -i --ipc Create a new IPC namespace.\n\ 46 | -h, --help\n\ 47 | \n\ 48 | If no COMMAND is given, run '${SHELL} -i' (default: '%s -i')\n\ 49 | \n\ 50 | Examples:\n\ 51 | \n\ 52 | \n\ 53 | "; 54 | 55 | char *default_shell() { 56 | char *sh = getenv("SHELL"); 57 | if(sh == NULL) { 58 | sh = "/bin/sh"; 59 | } 60 | return sh; 61 | } 62 | 63 | void print_usage(char *exec_name) { 64 | printf(usage_string, exec_name, exec_name, default_shell()); 65 | } 66 | 67 | #define STACK_SIZE (1024*1024) 68 | 69 | static char child_stack[STACK_SIZE]; 70 | 71 | typedef struct mount mount_args; 72 | struct mount { 73 | mount_args *next; 74 | char *source; 75 | char *target; 76 | char *filesystemtype; 77 | unsigned long mountflags; 78 | const void *data; 79 | }; 80 | 81 | typedef struct args nsroot_args; 82 | struct args { 83 | int pipe_fd[2]; 84 | char **argv; 85 | char *new_root; 86 | char *old_root; 87 | char *uid_map; 88 | char *gid_map; 89 | enum {SR_PIVOT_ROOT, SR_CHROOT} switch_root_method; 90 | mount_args *user_bind_mounts; 91 | unsigned int clone_flags; 92 | bool read_only_root; 93 | bool keep_old_root; 94 | }; 95 | 96 | mount_args define_bind_mount(char *source, char *target) { 97 | mount_args m = { 98 | .next = NULL, 99 | .source = source, 100 | .target = target, 101 | .filesystemtype = NULL, 102 | .mountflags = MS_BIND, 103 | .data = NULL, 104 | }; 105 | return m; 106 | } 107 | 108 | void fail(char *what) { 109 | fprintf(stderr, "Error: %s failed (errno: %d): %s\n", what, errno, strerror(errno)); 110 | exit(1); 111 | } 112 | 113 | int join_paths(char *buffer, int size, char *a, char *b) { 114 | // todo: bug when a or b == "/" 115 | if(b[0] == '/') { 116 | b++; 117 | } 118 | int a_len = strlen(a); 119 | if(a[a_len-1] == '/') a[a_len-1] = 0x0; 120 | int ret = snprintf(buffer, size, "%s/%s", a, b); 121 | if (ret < 0 || ret >= size) { 122 | return -1; 123 | } else { 124 | return 0; 125 | } 126 | } 127 | 128 | int mount_all(mount_args *mo, char *source_prefix, char *target_prefix) { 129 | int ret; 130 | for(mount_args *m = mo; m != NULL; m = m->next) { 131 | char source_path_buf[PATH_MAX]; 132 | char target_path_buf[PATH_MAX]; 133 | char *source_path = m->source; 134 | char *target_path = m->target; 135 | if(source_prefix != NULL) { 136 | if(join_paths(source_path_buf, sizeof(source_path_buf), source_prefix, m->source)) { 137 | return -1; 138 | } 139 | source_path = source_path_buf; 140 | } 141 | if(target_prefix != NULL) { 142 | if(join_paths(target_path_buf, sizeof(target_path_buf), target_prefix, m->target)) { 143 | return -1; 144 | } 145 | target_path = target_path_buf; 146 | } 147 | ret = mount(source_path, target_path, m->filesystemtype, m->mountflags, m->data); 148 | if(ret) return ret; 149 | if (m->mountflags & MS_RDONLY) { 150 | ret = mount("", target_path, NULL, MS_RDONLY | MS_REMOUNT | MS_BIND, NULL); 151 | if(ret) return ret; 152 | } 153 | } 154 | return 0; 155 | } 156 | 157 | void insert_mount(mount_args **mounts, mount_args *new_mount) { 158 | new_mount->next = *mounts; 159 | *mounts = new_mount; 160 | } 161 | 162 | 163 | static int child_fun(void *_arg) { 164 | nsroot_args *args = (nsroot_args *) _arg; 165 | close(args->pipe_fd[1]); 166 | 167 | char ch; 168 | if(read(args->pipe_fd[0], &ch, 1)) { 169 | fail("reading pipe"); 170 | } 171 | 172 | char new_root_abs[PATH_MAX]; 173 | 174 | if(realpath(args->new_root, new_root_abs) == NULL) { 175 | fail("resolving new root directory"); 176 | } 177 | 178 | switch(args->switch_root_method) { 179 | case SR_CHROOT: 180 | errno = 0; 181 | if(chroot(new_root_abs)) { 182 | fail("chroot"); 183 | } 184 | if (chdir("/")) { 185 | fail("chdir(\"/\") after chroot"); 186 | } 187 | break; 188 | case SR_PIVOT_ROOT: 189 | ; 190 | char old_root_abs[PATH_MAX]; 191 | if(args->old_root[0] != '/') { 192 | fail("old root should be an absolute path"); 193 | } 194 | int ret = snprintf(old_root_abs, sizeof(old_root_abs), "%s%s", new_root_abs, args->old_root); 195 | if(ret < 0 || ret >= sizeof(old_root_abs)) { 196 | fail("snprintf: concatenate old root and new root paths"); 197 | } 198 | errno = 0; 199 | if(mount(new_root_abs, new_root_abs, NULL, MS_BIND, NULL)) { 200 | fail("mount NEWROOT"); 201 | } 202 | if(args->read_only_root) { 203 | errno = 0; 204 | if(mount("", new_root_abs, NULL, MS_BIND | MS_RDONLY | MS_REMOUNT, NULL)) { 205 | fail("remount NEWROOT readonly"); 206 | } 207 | } 208 | errno = 0; 209 | if(pivot_root(new_root_abs, old_root_abs) != 0) { 210 | fail("pivot_root"); 211 | } 212 | errno = 0; 213 | if (chdir("/")) { 214 | fail("chdir(\"/\") after pivot_root"); 215 | } 216 | errno = 0; 217 | if(mount_all(args->user_bind_mounts, args->old_root, NULL)) { 218 | fail("bind mount user volumes"); 219 | }; 220 | if(!args->keep_old_root) { 221 | errno = 0; 222 | if(mount("", args->old_root, "dontcare", MS_REC | MS_PRIVATE, "")) { 223 | fail("create private mount over old root"); 224 | } 225 | errno = 0; 226 | if(umount2(args->old_root, MNT_DETACH)) { 227 | fail("umount2(old_root)"); 228 | } 229 | } 230 | break; 231 | } 232 | 233 | if(execvp(args->argv[0], args->argv)) { 234 | fail("execvp"); 235 | } 236 | fail("this never happens"); 237 | return -1; 238 | } 239 | 240 | int write_file(char *path, char *contents) { 241 | int f = open(path, O_WRONLY | O_SYNC); 242 | if(f == -1) return -1; 243 | int ret = dprintf(f, "%s", contents); 244 | if(ret < 0 || ret == EOF) return -1; 245 | return close(f); 246 | } 247 | 248 | void replace(char *str, char old, char new) { 249 | for(int i = 0; str[i] != 0; i++) { 250 | if(str[i] == old) { 251 | str[i] = new; 252 | } 253 | } 254 | } 255 | 256 | int run(nsroot_args *args) { 257 | int ret; 258 | char *error_msg; 259 | 260 | if(pipe(args->pipe_fd)) fail("creating pipe"); 261 | 262 | pid_t child_pid = clone(child_fun, child_stack + STACK_SIZE, args->clone_flags | SIGCHLD, args); 263 | 264 | if(child_pid == -1) { 265 | fail("clone"); 266 | } 267 | 268 | char path[PATH_MAX]; 269 | 270 | if(args->uid_map != NULL) { 271 | ret = snprintf(path, sizeof(path), "/proc/%d/uid_map", child_pid); 272 | if(ret < 0 || ret > sizeof(path)) { 273 | error_msg = "snprintf"; goto fail; 274 | } 275 | if (write_file(path, args->uid_map)) { 276 | error_msg = "writing uid_map"; goto fail; 277 | } 278 | } 279 | 280 | if(args->gid_map != NULL) { 281 | snprintf(path, sizeof(path), "/proc/%d/gid_map", child_pid); 282 | if(ret < 0 || ret > sizeof(path)) { 283 | error_msg = "snprintf"; goto fail; 284 | } 285 | if(write_file(path, args->gid_map)) { 286 | error_msg = "writing gid_map"; goto fail; 287 | } 288 | } 289 | 290 | close(args->pipe_fd[1]); 291 | return waitpid(child_pid, NULL, 0); 292 | 293 | fail: 294 | kill(child_pid, SIGKILL); 295 | close(args->pipe_fd[1]); 296 | waitpid(child_pid, NULL, 0); 297 | fail(error_msg); 298 | return -1; // never happens 299 | } 300 | 301 | void argument_error(char *err) { 302 | printf("nsroot: %s See '--help' for details.\n", err); 303 | exit(-1); 304 | } 305 | 306 | int main(int argc, char *argv[], char *envp[]) { 307 | int flags = CLONE_NEWUSER | CLONE_NEWNS; 308 | 309 | nsroot_args args = { 310 | .clone_flags = flags, 311 | .argv = NULL, 312 | .new_root = NULL, 313 | .uid_map = NULL, 314 | .gid_map = NULL, 315 | .user_bind_mounts = NULL, 316 | .switch_root_method = SR_CHROOT, 317 | .old_root = "/mnt", 318 | .keep_old_root = false, 319 | }; 320 | 321 | while(1) { 322 | int option_index = 0; 323 | static struct option long_options[] = { 324 | {"help", no_argument, 0, 'h'}, 325 | {"volume", required_argument, 0, 'v'}, 326 | {"old-root", required_argument, 0, 'o'}, 327 | {"read-only", no_argument, 0, 'r'}, 328 | {"keep-old-root", no_argument, 0, 'k'}, 329 | {"uid-map", required_argument, 0, 'M'}, 330 | {"gid-map", required_argument, 0, 'G'}, 331 | {"net", no_argument, 0, 'n'}, 332 | {"ipc", no_argument, 0, 'i'} 333 | }; 334 | 335 | int c = getopt_long(argc, argv, "hv:o:rkM:G:ni", 336 | long_options, &option_index); 337 | if(c == -1) break; 338 | switch(c) { 339 | case 'h': 340 | print_usage(argc > 0 ? argv[0] : "nsroot"); 341 | exit(1); 342 | break; 343 | case 'v': 344 | ; 345 | char *msg = "Invalid parameter to -v,--volume."; 346 | char *src, *dest, *opt; 347 | char **stringp = &optarg; 348 | unsigned int flags = 0; 349 | src = strsep(stringp, ":"); 350 | if(src == NULL) argument_error(msg); 351 | dest = strsep(stringp, ":"); 352 | if(dest == NULL) argument_error(msg); 353 | opt = strsep(stringp, ":"); 354 | if(opt != NULL) { 355 | if(strcmp(opt, "ro") == 0) { 356 | flags |= MS_RDONLY; 357 | } else if(strcmp(opt, "rw") == 0) { 358 | // default - ignore 359 | } else { 360 | argument_error(msg); 361 | } 362 | if(strsep(stringp, ":") != NULL) argument_error(msg); 363 | } 364 | mount_args *m = alloca(sizeof(mount_args)); 365 | *m = define_bind_mount(src, dest); 366 | m->mountflags |= flags; 367 | insert_mount(&args.user_bind_mounts, m); 368 | args.switch_root_method = SR_PIVOT_ROOT; 369 | //printf("src: %s, dest: %s, opt: %s\n", src, dest, opt); 370 | break; 371 | case 'o': 372 | ; 373 | args.old_root = optarg; 374 | if(args.old_root[0] != '/') { 375 | argument_error("old-root must be an absolute path inside NEWROOT."); 376 | } 377 | args.switch_root_method = SR_PIVOT_ROOT; 378 | break; 379 | case 'r': 380 | args.read_only_root = true; 381 | args.switch_root_method = SR_PIVOT_ROOT; 382 | break; 383 | case 'k': 384 | args.keep_old_root = true; 385 | args.switch_root_method = SR_PIVOT_ROOT; 386 | break; 387 | case 'M': 388 | replace(optarg, ',', '\n'); 389 | args.uid_map = optarg; 390 | break; 391 | case 'G': 392 | replace(optarg, ',', '\n'); 393 | args.gid_map = optarg; 394 | break; 395 | case 'n': 396 | args.clone_flags |= CLONE_NEWNET; 397 | break; 398 | case 'i': 399 | args.clone_flags |= CLONE_NEWIPC; 400 | break; 401 | } 402 | } 403 | 404 | if(optind == argc) { 405 | args.new_root = "."; 406 | } else { 407 | args.new_root = argv[optind]; 408 | optind++; 409 | } 410 | 411 | if(optind < argc) { 412 | args.argv = &argv[optind]; 413 | } else { 414 | char *default_argv[] = {default_shell(), "-i", NULL}; 415 | args.argv = default_argv; 416 | } 417 | 418 | int ret = run(&args); 419 | return ret; 420 | } 421 | --------------------------------------------------------------------------------