├── .envrc ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── flake.lock ├── flake.nix ├── shell.nix └── src └── main.rs /.envrc: -------------------------------------------------------------------------------- 1 | mkdir -p .direnv 2 | # reload when these files change 3 | watch_file flake.nix 4 | watch_file flake.lock 5 | watch_file shell.nix 6 | watch_file default.nix 7 | # load the flake devShell 8 | eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile")" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bitflags" 5 | version = "1.2.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 8 | 9 | [[package]] 10 | name = "cc" 11 | version = "1.0.50" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 14 | 15 | [[package]] 16 | name = "cfg-if" 17 | version = "0.1.10" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 20 | 21 | [[package]] 22 | name = "libc" 23 | version = "0.2.77" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" 26 | 27 | [[package]] 28 | name = "netns-exec" 29 | version = "0.2.2" 30 | dependencies = [ 31 | "nix", 32 | ] 33 | 34 | [[package]] 35 | name = "nix" 36 | version = "0.18.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" 39 | dependencies = [ 40 | "bitflags", 41 | "cc", 42 | "cfg-if", 43 | "libc", 44 | ] 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "netns-exec" 3 | version = "0.2.2" 4 | authors = ["John Axel Eriksson "] 5 | edition = "2018" 6 | description = "Execute process in Linux network namespace" 7 | repository = "https://github.com/johnae/netns-exec" 8 | readme = "README.md" 9 | license = "MIT" 10 | keywords = ["cli", "terminal"] 11 | categories = ["command-line-utilities"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | nix = "0.18" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NetNS Exec 2 | 3 | This is a super simple command for executing a process within a Linux network namespace. I personally use it to run my whole desktop within a namespace where there's only a [wireguard](https://www.wireguard.com/) interface, but you could use it for other reasons as well. 4 | 5 | The [wireguard](https://www.wireguard.com/) dev himself suggests creating all unencrypted network interfaces (like `eth0` or `wlan0`) together with the wireguard interface within a certain network namespace and then you move the wireguard interface out of there into the `init` (eg. main) network namespace while leaving its socket in the original namespace (together with the unencrypted ones). That way, your `init` network namespace will only have a wireguard interface so everything goes over that interface (and no fiddling with routes etc needed). 6 | This is obviously really cool and what you'd probably want to do if you can... unfortunately it can be a bit difficult to make all that work, starting dhcpd, wpa_supplicant or iwd in a different network namespace. So, this instead enables me to run my desktop within a namespace into which I've moved only the wireguard interface, leaving the wlan0 etc. in the `init` namespace. 7 | 8 | If you wish to enter a named network namespace, you must ofc create the network namespace before you can run this command, when you've created it - you can run this like so: 9 | 10 | ```sh 11 | netns-exec cmdline here 12 | ``` 13 | 14 | A more concrete example would be: 15 | ```sh 16 | netns-exec private sway 17 | ``` 18 | 19 | It is also possible to enter the network namespace of any process via its pid - for example, entering pid 1:s network namespace would be done like this: 20 | 21 | ```sh 22 | netns-exec 1 bash 23 | ``` 24 | 25 | That would get you a bash shell in the network namespace of pid 1 (which basically means the "main" or "global" network namespace). 26 | 27 | For this to be runnable as a normal user without sudo, you need to set the `setuid` bit (and the executable should be owned by root ofc). As soon as we've switched network namespace (a privileged operation), we drop privileges. -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1598466101, 6 | "narHash": "sha256-JPhv+Ay98KMWRVRFlLEK9+eLpvNjhTBGWdFKZsE97ck=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "a586a6b966d59f53f45a04f8891fbc017dc09dbe", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nix-misc": { 19 | "inputs": { 20 | "nixpkgs": [ 21 | "nixpkgs" 22 | ] 23 | }, 24 | "locked": { 25 | "lastModified": 1596783463, 26 | "narHash": "sha256-Fucbc/QUYrDBFRo1aau59Qe8feHhru76vaNLWpWQelI=", 27 | "owner": "johnae", 28 | "repo": "nix-misc", 29 | "rev": "0da4fdf62e9273205c8c773101ad36fc67215d87", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "johnae", 34 | "repo": "nix-misc", 35 | "type": "github" 36 | } 37 | }, 38 | "nixpkgs": { 39 | "locked": { 40 | "lastModified": 1599486375, 41 | "narHash": "sha256-1Mk+605nLIgfQxMtqev/msrHt+C/w0OkC5wgbviP0nU=", 42 | "path": "/nix/store/8jdcp4ilj7h4swa89w9c9hxyl78qa5ia-source", 43 | "rev": "a31736120c5de6e632f5a0ba1ed34e53fc1c1b00", 44 | "type": "path" 45 | }, 46 | "original": { 47 | "id": "nixpkgs", 48 | "type": "indirect" 49 | } 50 | }, 51 | "root": { 52 | "inputs": { 53 | "flake-utils": "flake-utils", 54 | "nix-misc": "nix-misc", 55 | "nixpkgs": "nixpkgs" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "netns-exec - run a process in a Linux network namespace"; 3 | 4 | inputs.flake-utils.url = "github:numtide/flake-utils"; 5 | inputs.nix-misc = { 6 | url = "github:johnae/nix-misc"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, ... }@inputs: 11 | inputs.flake-utils.lib.simpleFlake { 12 | inherit self nixpkgs; 13 | name = "snowflake"; 14 | preOverlays = [ 15 | inputs.nix-misc.overlay 16 | ]; 17 | systems = inputs.flake-utils.lib.defaultSystems; 18 | shell = ./shell.nix; 19 | } 20 | ; 21 | } 22 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | let 3 | RUST_SRC_PATH = pkgs.stdenv.mkDerivation { 4 | inherit (pkgs.rustc) src; 5 | inherit (pkgs.rustc.src) name; 6 | phases = [ "unpackPhase" "installPhase" ]; 7 | installPhase = "cp -r src $out"; 8 | }; 9 | in 10 | pkgs.mkShell { 11 | buildInputs = with pkgs; [ rustc cargo clippy rustfmt ]; 12 | inherit RUST_SRC_PATH; 13 | } 14 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate nix; 2 | use nix::fcntl::{open, OFlag}; 3 | use nix::sched::{setns, unshare, CloneFlags}; 4 | use nix::{ 5 | sys::stat::Mode, 6 | unistd::{setgid, setuid, Gid, Uid}, 7 | }; 8 | use std::env; 9 | use std::ffi::CString; 10 | 11 | fn main() { 12 | let nsname = if env::args().len() > 2 { 13 | env::args().nth(1).unwrap() 14 | } else { 15 | panic!("Please supply at least 2 arguments - the network namespace name or the pid of a process whose netns you want to enter, then the command and any arguments to that command"); 16 | }; 17 | 18 | unshare(CloneFlags::CLONE_NEWNET).expect("Failed to unshare network namespace"); 19 | 20 | let nspath = if nsname.parse::().is_ok() { 21 | format!("/proc/{}/ns/net", nsname) 22 | } else { 23 | format!("/var/run/netns/{}", nsname) 24 | }; 25 | 26 | let nsfd = open(nspath.as_str(), OFlag::O_RDONLY, Mode::empty()) 27 | .expect(&format!("Could not open netns file: {}", nspath)); 28 | 29 | setns(nsfd, CloneFlags::CLONE_NEWNET).expect("Couldn't set network namespace"); 30 | // drop privs now - these MUST happen in the below order, otherwise 31 | // dropping group privileges might fail as the user privs may have 32 | // changed so that the user can no longer set the gid 33 | setgid(Gid::current()).expect("Couldn't drop group privileges"); 34 | setuid(Uid::current()).expect("Couldn't drop user privileges"); 35 | 36 | let args: Vec<_> = env::args() 37 | .into_iter() 38 | .skip(2) 39 | .map(|arg| CString::new(arg.as_str()).unwrap()) 40 | .collect(); 41 | 42 | let c_args: Vec<_> = args.iter().map(|arg| arg.as_c_str()).collect(); 43 | 44 | nix::unistd::execvp(&c_args.first().unwrap(), c_args.as_slice()) 45 | .expect("something went wrong executing the given command, perhaps it couldn't be found?"); 46 | } 47 | --------------------------------------------------------------------------------