├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── package └── self-extracting │ └── installer.sh └── src ├── bin └── bottled.rs ├── env.rs ├── lib.rs ├── main.rs ├── shell.rs └── systemd.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /pkg 3 | /.build 4 | 5 | .vscode 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 28 | dependencies = [ 29 | "hermit-abi", 30 | "libc", 31 | "winapi", 32 | ] 33 | 34 | [[package]] 35 | name = "autocfg" 36 | version = "1.0.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 39 | 40 | [[package]] 41 | name = "bitflags" 42 | version = "1.2.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 45 | 46 | [[package]] 47 | name = "bottled-shell" 48 | version = "0.1.0" 49 | dependencies = [ 50 | "clap", 51 | "libc", 52 | "literal", 53 | "log", 54 | "nix", 55 | "pretty_env_logger", 56 | "thiserror", 57 | ] 58 | 59 | [[package]] 60 | name = "cc" 61 | version = "1.0.72" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" 64 | 65 | [[package]] 66 | name = "cfg-if" 67 | version = "1.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 70 | 71 | [[package]] 72 | name = "clap" 73 | version = "2.33.3" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 76 | dependencies = [ 77 | "ansi_term", 78 | "atty", 79 | "bitflags", 80 | "strsim", 81 | "textwrap", 82 | "unicode-width", 83 | "vec_map", 84 | ] 85 | 86 | [[package]] 87 | name = "env_logger" 88 | version = "0.7.1" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 91 | dependencies = [ 92 | "atty", 93 | "humantime", 94 | "log", 95 | "regex", 96 | "termcolor", 97 | ] 98 | 99 | [[package]] 100 | name = "hermit-abi" 101 | version = "0.1.19" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 104 | dependencies = [ 105 | "libc", 106 | ] 107 | 108 | [[package]] 109 | name = "humantime" 110 | version = "1.3.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 113 | dependencies = [ 114 | "quick-error", 115 | ] 116 | 117 | [[package]] 118 | name = "libc" 119 | version = "0.2.108" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" 122 | 123 | [[package]] 124 | name = "literal" 125 | version = "0.2.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "1098cb40d9a755c5c61003c7d4b5fb890d393bc14cc14024b4000b184e6c9669" 128 | 129 | [[package]] 130 | name = "log" 131 | version = "0.4.14" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 134 | dependencies = [ 135 | "cfg-if", 136 | ] 137 | 138 | [[package]] 139 | name = "memchr" 140 | version = "2.4.1" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 143 | 144 | [[package]] 145 | name = "memoffset" 146 | version = "0.6.4" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" 149 | dependencies = [ 150 | "autocfg", 151 | ] 152 | 153 | [[package]] 154 | name = "nix" 155 | version = "0.22.2" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "d3bb9a13fa32bc5aeb64150cd3f32d6cf4c748f8f8a417cce5d2eb976a8370ba" 158 | dependencies = [ 159 | "bitflags", 160 | "cc", 161 | "cfg-if", 162 | "libc", 163 | "memoffset", 164 | ] 165 | 166 | [[package]] 167 | name = "pretty_env_logger" 168 | version = "0.4.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" 171 | dependencies = [ 172 | "env_logger", 173 | "log", 174 | ] 175 | 176 | [[package]] 177 | name = "proc-macro2" 178 | version = "1.0.32" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" 181 | dependencies = [ 182 | "unicode-xid", 183 | ] 184 | 185 | [[package]] 186 | name = "quick-error" 187 | version = "1.2.3" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 190 | 191 | [[package]] 192 | name = "quote" 193 | version = "1.0.10" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 196 | dependencies = [ 197 | "proc-macro2", 198 | ] 199 | 200 | [[package]] 201 | name = "regex" 202 | version = "1.5.4" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" 205 | dependencies = [ 206 | "aho-corasick", 207 | "memchr", 208 | "regex-syntax", 209 | ] 210 | 211 | [[package]] 212 | name = "regex-syntax" 213 | version = "0.6.25" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 216 | 217 | [[package]] 218 | name = "strsim" 219 | version = "0.8.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 222 | 223 | [[package]] 224 | name = "syn" 225 | version = "1.0.81" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" 228 | dependencies = [ 229 | "proc-macro2", 230 | "quote", 231 | "unicode-xid", 232 | ] 233 | 234 | [[package]] 235 | name = "termcolor" 236 | version = "1.1.2" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 239 | dependencies = [ 240 | "winapi-util", 241 | ] 242 | 243 | [[package]] 244 | name = "textwrap" 245 | version = "0.11.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 248 | dependencies = [ 249 | "unicode-width", 250 | ] 251 | 252 | [[package]] 253 | name = "thiserror" 254 | version = "1.0.30" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 257 | dependencies = [ 258 | "thiserror-impl", 259 | ] 260 | 261 | [[package]] 262 | name = "thiserror-impl" 263 | version = "1.0.30" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 266 | dependencies = [ 267 | "proc-macro2", 268 | "quote", 269 | "syn", 270 | ] 271 | 272 | [[package]] 273 | name = "unicode-width" 274 | version = "0.1.9" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 277 | 278 | [[package]] 279 | name = "unicode-xid" 280 | version = "0.2.2" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 283 | 284 | [[package]] 285 | name = "vec_map" 286 | version = "0.8.2" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 289 | 290 | [[package]] 291 | name = "winapi" 292 | version = "0.3.9" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 295 | dependencies = [ 296 | "winapi-i686-pc-windows-gnu", 297 | "winapi-x86_64-pc-windows-gnu", 298 | ] 299 | 300 | [[package]] 301 | name = "winapi-i686-pc-windows-gnu" 302 | version = "0.4.0" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 305 | 306 | [[package]] 307 | name = "winapi-util" 308 | version = "0.1.5" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 311 | dependencies = [ 312 | "winapi", 313 | ] 314 | 315 | [[package]] 316 | name = "winapi-x86_64-pc-windows-gnu" 317 | version = "0.4.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 320 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bottled-shell" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = "2.33.3" 10 | libc = "0.2.108" 11 | literal = "0.2.0" 12 | log = "0.4.14" 13 | nix = "0.22.2" 14 | pretty_env_logger = "0.4.0" 15 | thiserror = "1.0.30" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Liang Ge 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/opt/bottled-shell 2 | TARGET=target/release/bottled-shell target/release/bottled 3 | SOURCES=$(wildcard src/**/*.rs) 4 | TEMPDIR=.build 5 | PACKAGE=$(TEMPDIR)/installer.sh 6 | CARGO?=cargo 7 | 8 | 9 | .PHONY: all package install clean 10 | 11 | all: $(TARGET) 12 | 13 | package: $(PACKAGE) 14 | 15 | $(TARGET): $(SOURCES) 16 | $(CARGO) build --release --locked --all-features 17 | 18 | $(PACKAGE): $(TARGET) 19 | install -Dm555 target/release/bottled $(TEMPDIR)/prefix/bin/bottled && \ 20 | tar --owner=root --group=root --mode=4555 -C $(TEMPDIR)/prefix \ 21 | -cf $(TEMPDIR)/snapshot.tar bin/bottled && \ 22 | install -Dm555 target/release/bottled-shell $(TEMPDIR)/prefix/bin/bottled-shell && \ 23 | tar --owner=root --group=root -C $(TEMPDIR)/prefix \ 24 | -uf $(TEMPDIR)/snapshot.tar bin/bottled-shell && \ 25 | cat package/self-extracting/installer.sh $(TEMPDIR)/snapshot.tar >$(TEMPDIR)/installer.sh 26 | 27 | install: $(PACKAGE) 28 | bash $(PACKAGE) 29 | 30 | clean: 31 | $(RM) -R $(TEMPDIR) 32 | $(CARGO) clean 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bottled-shell: Run systemd in WSL2 2 | 3 | Run systemd with namespace in WSL2. Inspired by [subsystemctl](https://github.com/sorah/subsystemctl) 4 | 5 | ## Difference from other solutions 6 | 7 | - Launch systemd-enabled shell from start menu 8 | - Support Visual Studio Code(Remote - WSL) extension 9 | 10 | ## Install 11 | 12 | 1. Install package 13 | 14 | Download installer from [release page](https://github.com/lungothrin/bottled-shell/releases), and execute it with root privilege. 15 | 16 | ```bash 17 | curl -LJO https://github.com/lungothrin/bottled-shell/releases/download/v0.1.0-alpha/installer-v0.1.0-alpha.sh 18 | sudo bash installer-v0.1.0-alpha.sh 19 | ``` 20 | 21 | 2. Set your login shell to `bottled-shell` 22 | 23 | Suppose you are using `bash` as login shell. 24 | 25 | Create an alias for bottled-shell. 26 | 27 | ```bash 28 | sudo ln -s /opt/bottled-shell/bin/bottled-shell /opt/bottled-shell/bin/bottled-bash 29 | ``` 30 | 31 | Edit `/etc/passwd`, set your login shell to `bottled-bash`. 32 | 33 | ``` 34 | username:x:1000:1000::/home/username:/opt/bottled-shell/bin/bottled-bash 35 | ``` 36 | 37 | 3. All done. 38 | 39 | Try open a shell from start menu, or open a Visual Studio Code(Remote - WSL) window. -------------------------------------------------------------------------------- /package/self-extracting/installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEST="${PREFIX:=/opt/bottled-shell}" 4 | echo "installing bottled-shell into $DEST" 5 | 6 | mkdir -p ${DEST} 7 | 8 | ARCHIVE=$(awk '/^__ARCHIVE__/ {print NR + 1; exit 0; }' "${0}") 9 | tail -n+${ARCHIVE} "${0}" | tar -xpv -C ${DEST} 10 | 11 | echo "done" 12 | 13 | exit 0 14 | 15 | __ARCHIVE__ 16 | -------------------------------------------------------------------------------- /src/bin/bottled.rs: -------------------------------------------------------------------------------- 1 | use bottled_shell::systemd; 2 | use bottled_shell::shell; 3 | 4 | fn main() { 5 | if let Err(_) = std::env::var("BOTTLED_SHELL_LOG") { 6 | std::env::set_var("BOTTLED_SHELL_LOG", "info"); 7 | } 8 | pretty_env_logger::init_custom_env("BOTTLED_SHELL_LOG"); 9 | 10 | let mut app = clap::App::new(clap::crate_name!()) 11 | .version(clap::crate_version!()) 12 | .about(clap::crate_description!()) 13 | .setting(clap::AppSettings::SubcommandRequired) 14 | .subcommand( 15 | clap::SubCommand::with_name("is-inside") 16 | .about("Return 0 if invoked inside a systemd-enabled namespace") 17 | ) 18 | .subcommand( 19 | clap::SubCommand::with_name("is-running") 20 | .about("Return 0 if systemd is running") 21 | ) 22 | .subcommand( 23 | clap::SubCommand::with_name("start") 24 | .about("Start systemd in a Linux namespace(mount, pid)") 25 | ) 26 | .subcommand( 27 | clap::SubCommand::with_name("stop") 28 | .about("Stop systemd") 29 | ) 30 | .subcommand( 31 | clap::SubCommand::with_name("shell") 32 | .about("Start a shell inside systemd-enabled namespace using machinectl-shell") 33 | .arg( 34 | clap::Arg::with_name("shell") 35 | .short("s") 36 | .long("shell") 37 | .value_name("SHELL") 38 | .help("Specify interactive shell") 39 | .takes_value(true) 40 | ) 41 | .arg( 42 | clap::Arg::with_name("shell-options") 43 | .raw(true) 44 | ) 45 | ); 46 | let matches = app.get_matches_from_safe_borrow(std::env::args_os()).unwrap_or_else(|e| { 47 | if e.use_stderr() { 48 | eprintln!("{}", e.message); 49 | std::process::exit(1); 50 | } 51 | e.exit() 52 | }); 53 | 54 | match matches.subcommand() { 55 | ("is-inside", _) => { 56 | if systemd::is_associated_with_systemd() { 57 | log::info!("is-inside=true"); 58 | std::process::exit(libc::EXIT_SUCCESS); 59 | } else { 60 | log::info!("is-inside=false"); 61 | std::process::exit(libc::EXIT_FAILURE); 62 | } 63 | } 64 | ("is-running", _) => { 65 | if systemd::is_associated_with_systemd() { 66 | log::info!("is-running=true"); 67 | std::process::exit(libc::EXIT_SUCCESS); 68 | } else if let Ok(Some(pid)) = systemd::get_systemd_pid() { 69 | log::info!("is-running=true, PID={}", pid); 70 | std::process::exit(libc::EXIT_SUCCESS); 71 | } else { 72 | log::info!("is-running=false"); 73 | std::process::exit(libc::EXIT_FAILURE); 74 | } 75 | } 76 | ("start", _) => { 77 | systemd::start_systemd().unwrap(); 78 | } 79 | ("stop", _) => { 80 | systemd::stop_systemd().unwrap(); 81 | } 82 | ("shell", Some(m)) => { 83 | let mut shell = "bash"; 84 | if let Some(s) = m.value_of("shell") { 85 | shell = s.clone(); 86 | }; 87 | log::debug!("specified shell: {}", shell); 88 | 89 | let bottled_shell = if let Some((c, _)) = clap::crate_name!().rsplit_once('-') { 90 | format!("{}-{}", c, shell) 91 | } else { 92 | clap::crate_name!().to_string() 93 | }; 94 | let bottled_shell_path = std::env::current_exe() 95 | .unwrap_or_else(|_| std::path::PathBuf::from(app.get_bin_name().unwrap())) 96 | .parent().unwrap() 97 | .join(bottled_shell) 98 | .to_str().unwrap() 99 | .to_string(); 100 | 101 | let mut args: Vec = Vec::new(); 102 | for v in m.values_of_lossy("shell-options").unwrap_or(Vec::new()) { 103 | args.push(v); 104 | } 105 | 106 | if !systemd::is_associated_with_systemd() && None == systemd::get_systemd_pid().unwrap() { 107 | log::trace!("starting bottled systemd"); 108 | systemd::start_systemd().unwrap(); 109 | } 110 | 111 | log::trace!("starting login shell: {}", shell); 112 | shell::launch_login_shell(&bottled_shell_path, &shell.to_string(), &args).unwrap(); 113 | } 114 | _ => unreachable!() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/env.rs: -------------------------------------------------------------------------------- 1 | 2 | pub fn get_preserved_env() -> Vec { 3 | use std::collections::BTreeSet; 4 | use literal::{set,SetLiteral}; 5 | 6 | let preserved_env: BTreeSet = set!{ 7 | "WSLENV", 8 | "WSL_INTEROP", 9 | "WSL_DISTRO_NAME", 10 | "WSL_NAME", 11 | "WT_SESSION", 12 | "WT_PROFILE_ID", 13 | "PULSE_SERVER", 14 | "WAYLAND_DISPLAY", 15 | "BOTTLED_SHELL_LOG", 16 | }; 17 | 18 | std::env::vars() 19 | .into_iter() 20 | .filter_map(|(k, v)| { 21 | if preserved_env.contains(&k) { 22 | return Some(format!("{}={}", k, v)) 23 | } 24 | for p in k.split('_') { 25 | if p == "WT" { 26 | return Some(format!("{}={}", k, v)) 27 | } else if p.starts_with("WSL") { 28 | return Some(format!("{}={}", k, v)) 29 | } else if p.ends_with("WSL") || p.ends_with("WSL2") { 30 | return Some(format!("{}={}", k, v)) 31 | } 32 | } 33 | None 34 | }) 35 | .collect::>() 36 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod env; 2 | pub mod systemd; 3 | pub mod shell; -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | if let Err(_) = std::env::var("BOTTLED_SHELL_LOG") { 3 | std::env::set_var("BOTTLED_SHELL_LOG", "info"); 4 | } 5 | pretty_env_logger::init_custom_env("BOTTLED_SHELL_LOG"); 6 | 7 | let mut app = clap::App::new(clap::crate_name!()) 8 | .version(clap::crate_version!()) 9 | .about("launch a systemd-enabled shell") 10 | .setting(clap::AppSettings::TrailingVarArg) 11 | .setting(clap::AppSettings::DontDelimitTrailingValues) 12 | .setting(clap::AppSettings::AllowLeadingHyphen) 13 | .setting(clap::AppSettings::DisableVersion) 14 | .arg( 15 | clap::Arg::with_name("shell-options") 16 | .multiple(true) 17 | .allow_hyphen_values(true) 18 | ); 19 | let matches = app.get_matches_from_safe_borrow(std::env::args_os()).unwrap_or_else(|e| { 20 | if e.use_stderr() { 21 | eprintln!("{}", e.message); 22 | std::process::exit(1); 23 | } 24 | e.exit() 25 | }); 26 | 27 | for (key, value) in std::env::vars() { 28 | log::debug!("env {}: {}", key, value); 29 | } 30 | 31 | let bottled_cmd = if let Some((c, _)) = clap::crate_name!().rsplit_once('-') { 32 | c.clone() 33 | } else { 34 | clap::crate_name!() 35 | }; 36 | let bottled_cmd_path = std::env::current_exe().unwrap() 37 | .parent().unwrap() 38 | .join(bottled_cmd) 39 | .to_str().unwrap() 40 | .to_string(); 41 | let mut shell = "bash"; 42 | if let Some(b) = app.get_bin_name() { 43 | if let Some((_, s)) = b.rsplit_once('-') { 44 | shell = s.clone(); 45 | } 46 | } 47 | let mut args: Vec = vec![ 48 | std::ffi::CString::new(bottled_cmd_path.clone()).unwrap(), 49 | std::ffi::CString::new("shell").unwrap(), 50 | std::ffi::CString::new("-s").unwrap(), 51 | std::ffi::CString::new(shell).unwrap(), 52 | ]; 53 | if matches.is_present("shell-options") { 54 | args.push(std::ffi::CString::new("--").unwrap()); 55 | for v in matches.values_of_lossy("shell-options").unwrap() { 56 | args.push(std::ffi::CString::new(v.as_str()).unwrap()); 57 | } 58 | } 59 | 60 | log::trace!("executing bottled shell: {:?}", args); 61 | nix::unistd::execv( 62 | &std::ffi::CString::new(bottled_cmd_path.as_str()).unwrap().as_c_str(), 63 | &args 64 | ).unwrap(); 65 | } 66 | -------------------------------------------------------------------------------- /src/shell.rs: -------------------------------------------------------------------------------- 1 | use crate::env; 2 | use crate::systemd; 3 | 4 | #[derive(thiserror::Error, Debug)] 5 | pub enum ShellError { 6 | #[error("shell not found for '{0}'")] 7 | ShellNotFound(String), 8 | 9 | #[error(transparent)] 10 | IOError(#[from] std::io::Error), 11 | 12 | #[error(transparent)] 13 | NixErrno(#[from] nix::errno::Errno), 14 | } 15 | 16 | fn get_shell_path(shell: &String) -> Result { 17 | use std::io::BufRead; 18 | 19 | for l in std::io::BufReader::new(std::fs::File::open("/etc/shells")?).lines() { 20 | let line = l?.clone(); 21 | let p = line.trim(); 22 | if let Some((_, s)) = p.rsplit_once('/') { 23 | if s == shell { 24 | return Ok(p.to_string()); 25 | } 26 | } 27 | } 28 | Err(ShellError::ShellNotFound(shell.clone())) 29 | } 30 | 31 | pub fn launch_login_shell(bottled_shell_path: &String, shell: &String, args: &Vec) -> Result<(), ShellError> { 32 | use std::ffi::CString; 33 | 34 | if systemd::is_associated_with_systemd() { 35 | log::trace!("already associated with bottled systemd"); 36 | 37 | log::trace!("releasing privilege"); 38 | nix::unistd::setegid(nix::unistd::getgid()).unwrap(); 39 | nix::unistd::seteuid(nix::unistd::getuid()).unwrap(); 40 | 41 | let executable = get_shell_path(&shell)?; 42 | let mut expanded_args: Vec = vec![ 43 | CString::new(shell.as_str()).unwrap(), 44 | ]; 45 | for v in args { 46 | expanded_args.push(CString::new(v.as_str()).unwrap()); 47 | } 48 | 49 | log::trace!("executing shell: {} {:?}", shell, expanded_args); 50 | nix::unistd::execv(&CString::new(executable).unwrap(), &expanded_args)?; 51 | } else { 52 | let uid = libc::uid_t::from(nix::unistd::getuid()); 53 | let pwent = unsafe { libc::getpwuid(uid) }; 54 | let pw_name = unsafe { std::ffi::CStr::from_ptr((*pwent).pw_name) }.to_str().unwrap(); 55 | log::trace!("username acquired: {}(UID={})", pw_name, uid); 56 | 57 | log::trace!("associating with bottled systemd"); 58 | systemd::associate_with_systemd().unwrap(); 59 | 60 | let executable = systemd::get_machinectl_bin().unwrap(); 61 | let mut expanded_args: Vec = vec![ 62 | CString::new("machinectl").unwrap(), 63 | CString::new("shell").unwrap(), 64 | ]; 65 | expanded_args.push(CString::new("-q").unwrap()); 66 | for e in env::get_preserved_env() { 67 | expanded_args.push(CString::new("-E").unwrap()); 68 | expanded_args.push(CString::new(e).unwrap()); 69 | } 70 | expanded_args.push(CString::new(format!("{}@.host", pw_name)).unwrap()); 71 | if !args.is_empty() { 72 | expanded_args.push(CString::new(bottled_shell_path.as_str()).unwrap()); 73 | for v in args { 74 | expanded_args.push(CString::new(v.as_str()).unwrap()); 75 | } 76 | } 77 | 78 | log::trace!("launch session: {} {:?}", executable, expanded_args); 79 | nix::unistd::execv(&CString::new(executable).unwrap(), &expanded_args)?; 80 | } 81 | unreachable!(); 82 | } 83 | -------------------------------------------------------------------------------- /src/systemd.rs: -------------------------------------------------------------------------------- 1 | use crate::env; 2 | 3 | static RUN_DIR: &str = "/run/bottled-shell"; 4 | static PID_FILE: &str = "/run/bottled-shell/systemd.pid"; 5 | 6 | #[derive(thiserror::Error, Debug)] 7 | pub enum SystemdError { 8 | #[error("systemd not found in standard locations")] 9 | SystemdNotFound, 10 | 11 | #[error("systemd not running")] 12 | SystemdNotRunning, 13 | 14 | #[error("no enough permission, required seteuid")] 15 | NoEnoughPermission, 16 | 17 | #[error(transparent)] 18 | ParseIntError(#[from] std::num::ParseIntError), 19 | 20 | #[error(transparent)] 21 | FromUTF8Error(#[from] std::string::FromUtf8Error), 22 | 23 | #[error(transparent)] 24 | IOError(#[from] std::io::Error), 25 | 26 | #[error(transparent)] 27 | NixErrno(#[from] nix::errno::Errno), 28 | } 29 | 30 | fn check_permission() -> Result { 31 | if nix::unistd::geteuid().is_root() { 32 | return Ok(true) 33 | } 34 | log::error!("no enough permission, required seteuid on program"); 35 | Err(SystemdError::NoEnoughPermission) 36 | } 37 | 38 | fn check_systemd_proc(pid: libc::pid_t) -> bool { 39 | let path = std::format!("/proc/{}/cmdline", pid); 40 | if let Ok(buffer) = std::fs::read(path.clone()) { 41 | let cmdline = String::from_utf8(buffer).unwrap(); 42 | if cmdline.split('\0').next().unwrap() == get_systemd_bin().unwrap() { 43 | log::trace!("check {}: match", path); 44 | return true; 45 | } 46 | } 47 | log::trace!("check {}: mismatch", path); 48 | false 49 | } 50 | 51 | pub fn is_associated_with_systemd() -> bool { 52 | check_systemd_proc(1) 53 | } 54 | 55 | fn get_systemd_bin() -> Result { 56 | let search_location = [ 57 | "/lib/systemd/systemd", 58 | "/usr/lib/systemd/systemd", 59 | ]; 60 | for l in search_location { 61 | if std::fs::metadata(l).is_ok() { 62 | return Ok(l.to_string()); 63 | } 64 | } 65 | Err(SystemdError::SystemdNotFound) 66 | } 67 | 68 | pub fn get_machinectl_bin() -> Result { 69 | let search_location = [ 70 | "/usr/bin/machinectl", 71 | "/bin/machinectl", 72 | ]; 73 | for l in search_location { 74 | if std::fs::metadata(l).is_ok() { 75 | return Ok(l.to_string()); 76 | } 77 | } 78 | Err(SystemdError::SystemdNotFound) 79 | } 80 | 81 | pub fn get_systemd_pid() -> Result, SystemdError> { 82 | let buffer = std::fs::read(PID_FILE); 83 | match buffer { 84 | Ok(b) => { 85 | let pid = String::from_utf8(b)?.trim().parse()?; 86 | log::trace!("check {}: PID={}", PID_FILE, pid); 87 | if check_systemd_proc(pid) { 88 | return Ok(Some(pid)); 89 | } 90 | Ok(None) 91 | } 92 | Err(e) => { 93 | if e.kind() == std::io::ErrorKind::NotFound { 94 | log::trace!("check {}: missing", PID_FILE); 95 | Ok(None) 96 | } else { 97 | Err(SystemdError::IOError(e)) 98 | } 99 | } 100 | } 101 | } 102 | 103 | fn put_systemd_pid(pid: libc::pid_t) -> std::io::Result<()> { 104 | std::fs::create_dir_all(RUN_DIR)?; 105 | std::fs::write(PID_FILE, format!("{}\n", pid)) 106 | } 107 | 108 | fn updated_systemd_envs() -> std::io::Result<()> { 109 | let envs = env::get_preserved_env().join(" "); 110 | let config = format!("[Manager]\nDefaultEnvironment={}\n", envs); 111 | log::trace!("updating systemd environment variables: {}", envs); 112 | 113 | std::fs::create_dir_all("/run/systemd/system.conf.d")?; 114 | std::fs::write("/run/systemd/system.conf.d/10-bottled-shell-env.conf", &config)?; 115 | 116 | std::fs::create_dir_all("/run/systemd/user.conf.d")?; 117 | std::fs::write("/run/systemd/user.conf.d/10-bottled-shell-env.conf", &config)?; 118 | 119 | Ok(()) 120 | } 121 | 122 | pub fn start_systemd() -> Result<(), SystemdError> { 123 | use nix::fcntl::OFlag; 124 | use nix::poll::PollFlags; 125 | use nix::sched::CloneFlags; 126 | use nix::unistd::ForkResult; 127 | 128 | if is_associated_with_systemd() { 129 | log::info!("systemd already started"); 130 | return Ok(()); 131 | } 132 | 133 | if let Ok(Some(pid)) = get_systemd_pid() { 134 | log::info!("systemd already started, PID={}", pid); 135 | return Ok(()); 136 | } 137 | 138 | check_permission()?; 139 | 140 | let systemd_bin = std::ffi::CString::new(get_systemd_bin().unwrap()).unwrap(); 141 | log::trace!("systemd location = {}", systemd_bin.to_str().unwrap()); 142 | 143 | updated_systemd_envs().unwrap(); 144 | 145 | let (rfd, wfd) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap(); 146 | match unsafe { nix::unistd::fork() } { 147 | Ok(ForkResult::Parent { .. }) => { 148 | nix::unistd::close(wfd).unwrap(); 149 | 150 | let start = std::time::Instant::now(); 151 | let timeout = std::time::Duration::from_secs(10); 152 | let mut fds = [nix::poll::PollFd::new(rfd, PollFlags::POLLIN)]; 153 | loop { 154 | let elapsed = start.elapsed(); 155 | if elapsed >= timeout { 156 | log::error!("systemd not started in time"); 157 | return Err(SystemdError::SystemdNotRunning) 158 | } 159 | 160 | let remaining = timeout - elapsed; 161 | if nix::poll::poll(&mut fds, remaining.as_millis() as libc::c_int)? > 0 { 162 | if let Some(ev) = fds[0].revents() { 163 | if ev.contains(PollFlags::POLLHUP) { 164 | if let Ok(Some(pid)) = get_systemd_pid() { 165 | log::info!("systemd(PID={}) started", pid); 166 | 167 | log::trace!("sending SIGRTMIN + 0 to systemd(PID={})", pid); 168 | unsafe { nix::libc::kill(pid, libc::SIGRTMIN() + 0); } 169 | std::thread::sleep(std::time::Duration::from_secs(1)); 170 | 171 | return Ok(()); 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | Ok(ForkResult::Child) => { 179 | nix::unistd::close(rfd).unwrap(); 180 | 181 | log::trace!("updating UID & GID"); 182 | nix::unistd::setgid(nix::unistd::Gid::from_raw(0)).unwrap(); 183 | nix::unistd::setuid(nix::unistd::Uid::from_raw(0)).unwrap(); 184 | 185 | log::trace!("creating new namespace"); 186 | nix::sched::unshare(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWPID).unwrap(); 187 | 188 | log::trace!("creating new session group"); 189 | nix::unistd::setsid().unwrap(); 190 | 191 | match unsafe { nix::unistd::fork() } { 192 | Ok(ForkResult::Parent { child, .. }) => { 193 | nix::unistd::close(wfd).unwrap(); 194 | 195 | log::trace!("updating PID file with {}", child); 196 | put_systemd_pid(libc::pid_t::from(child)).unwrap(); 197 | 198 | log::trace!( 199 | "session group leader(PID={}) terminated successfully", 200 | nix::unistd::getpid() 201 | ); 202 | std::process::exit(libc::EXIT_SUCCESS); 203 | } 204 | Ok(ForkResult::Child) => { 205 | exec_systemd(systemd_bin); 206 | std::process::exit(libc::EXIT_FAILURE); 207 | } 208 | Err(e) => Err(SystemdError::NixErrno(e)) 209 | } 210 | } 211 | Err(e) => Err(SystemdError::NixErrno(e)) 212 | } 213 | } 214 | 215 | fn exec_systemd(systemd_bin: std::ffi::CString) { 216 | use std::ffi::OsStr; 217 | use nix::fcntl::OFlag; 218 | use nix::mount::MsFlags; 219 | use nix::sys::stat::Mode; 220 | 221 | log::trace!("mounting filesystem"); 222 | nix::mount::mount( 223 | Some(OsStr::new("none")), 224 | OsStr::new("/"), 225 | None as Option<&[u8]>, 226 | MsFlags::MS_REC | MsFlags::MS_SHARED, 227 | None as Option<&[u8]>, 228 | ) 229 | .unwrap(); 230 | nix::mount::mount( 231 | Some(OsStr::new("none")), 232 | OsStr::new("/proc"), 233 | None as Option<&[u8]>, 234 | MsFlags::MS_REC | MsFlags::MS_PRIVATE, 235 | None as Option<&[u8]>, 236 | ) 237 | .unwrap(); 238 | nix::mount::mount( 239 | Some(OsStr::new("proc")), 240 | OsStr::new("/proc"), 241 | Some(OsStr::new("proc")), 242 | MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC, 243 | None as Option<&[u8]>, 244 | ) 245 | .unwrap(); 246 | 247 | log::trace!("switch working directory"); 248 | nix::unistd::chdir("/").unwrap(); 249 | 250 | log::trace!("updating STDIN, STDOUT, STDERR"); 251 | { 252 | let fd = nix::fcntl::open(OsStr::new("/dev/null"), OFlag::O_RDONLY, Mode::empty()).unwrap(); 253 | nix::unistd::dup2(fd, libc::STDIN_FILENO).unwrap(); 254 | nix::unistd::close(fd).unwrap(); 255 | } 256 | { 257 | let fd = nix::fcntl::open(OsStr::new("/dev/null"), OFlag::O_WRONLY, Mode::empty()).unwrap(); 258 | nix::unistd::dup2(fd, libc::STDOUT_FILENO).unwrap(); 259 | nix::unistd::close(fd).unwrap(); 260 | } 261 | { 262 | let fd = nix::fcntl::open(OsStr::new("/dev/null"), OFlag::O_WRONLY, Mode::empty()).unwrap(); 263 | nix::unistd::dup2(fd, libc::STDERR_FILENO).unwrap(); 264 | nix::unistd::close(fd).unwrap(); 265 | } 266 | 267 | log::trace!("launching systemd"); 268 | nix::unistd::execve(systemd_bin.as_c_str(), &[systemd_bin.as_c_str()], &[] as &[std::ffi::CString]).unwrap(); 269 | } 270 | 271 | pub fn stop_systemd() -> Result<(), SystemdError> { 272 | if is_associated_with_systemd() { 273 | kill_systemd(1) 274 | } else if let Some(pid) = get_systemd_pid()? { 275 | kill_systemd(pid) 276 | } else { 277 | log::info!("systemd not running"); 278 | Ok(()) 279 | } 280 | } 281 | 282 | fn kill_systemd(pid: libc::pid_t) -> Result<(), SystemdError> { 283 | check_permission()?; 284 | 285 | log::trace!("sending SIGRTMIN + 4 to systemd(PID={})", pid); 286 | unsafe { nix::libc::kill(pid, libc::SIGRTMIN() + 4); } 287 | 288 | if std::fs::metadata(PID_FILE).is_ok() { 289 | log::trace!("removing {}", PID_FILE); 290 | std::fs::remove_file(PID_FILE)?; 291 | } 292 | 293 | log::info!("systemd stopped"); 294 | Ok(()) 295 | } 296 | 297 | pub fn associate_with_systemd() -> Result<(), SystemdError> { 298 | use std::ffi::OsStr; 299 | use nix::fcntl::OFlag; 300 | use nix::sys::stat::Mode; 301 | use nix::sched::CloneFlags; 302 | 303 | check_permission()?; 304 | 305 | match get_systemd_pid() { 306 | Ok(Some(pid)) => { 307 | log::trace!("associating PID namespace"); 308 | { 309 | let path = format!("/proc/{}/ns/pid", pid); 310 | let fd = nix::fcntl::open(OsStr::new(&path), OFlag::O_RDONLY, Mode::empty())?; 311 | nix::sched::setns(fd, CloneFlags::CLONE_NEWPID)?; 312 | nix::unistd::close(fd)?; 313 | } 314 | 315 | log::trace!("associating MNT namespace"); 316 | { 317 | let path = format!("/proc/{}/ns/mnt", pid); 318 | let fd = nix::fcntl::open(OsStr::new(&path), OFlag::O_RDONLY, Mode::empty())?; 319 | nix::sched::setns(fd, CloneFlags::CLONE_NEWNS)?; 320 | nix::unistd::close(fd)?; 321 | } 322 | 323 | log::trace!("switch working directory"); 324 | nix::unistd::chdir("/").unwrap(); 325 | 326 | Ok(()) 327 | }, 328 | Ok(None) => Err(SystemdError::SystemdNotRunning), 329 | Err(e) => Err(e), 330 | } 331 | } 332 | --------------------------------------------------------------------------------