Allow disabling boolean flags from the CLI
Pair --hardened, --dry-run, and --unshare-net (renamed from --no-net) with negation counterparts so a CLI invocation can override a truthy config-file or profile value.
This commit is contained in:
@@ -25,7 +25,7 @@ Top-level keys set defaults; `[profile.<name>]` sections define named presets se
|
|||||||
```toml
|
```toml
|
||||||
# Global defaults
|
# Global defaults
|
||||||
whitelist = true
|
whitelist = true
|
||||||
no-net = true
|
unshare-net = true
|
||||||
ro = ["~/.aws"]
|
ro = ["~/.aws"]
|
||||||
|
|
||||||
# Named profile
|
# Named profile
|
||||||
|
|||||||
20
src/cli.rs
20
src/cli.rs
@@ -19,12 +19,20 @@ pub struct Args {
|
|||||||
pub whitelist: bool,
|
pub whitelist: bool,
|
||||||
|
|
||||||
/// Harden: unshare IPC, PID, UTS; private /tmp, /dev, /run
|
/// Harden: unshare IPC, PID, UTS; private /tmp, /dev, /run
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "no_hardened")]
|
||||||
pub hardened: bool,
|
pub hardened: bool,
|
||||||
|
|
||||||
|
/// Disable hardening (overrides config-file `hardened = true`)
|
||||||
|
#[arg(long, overrides_with = "hardened")]
|
||||||
|
pub no_hardened: bool,
|
||||||
|
|
||||||
/// Unshare the network namespace
|
/// Unshare the network namespace
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "share_net")]
|
||||||
pub no_net: bool,
|
pub unshare_net: bool,
|
||||||
|
|
||||||
|
/// Share the host network namespace (overrides config-file `unshare-net = true`)
|
||||||
|
#[arg(long, overrides_with = "unshare_net")]
|
||||||
|
pub share_net: bool,
|
||||||
|
|
||||||
/// Bind an extra path read-write (repeatable)
|
/// Bind an extra path read-write (repeatable)
|
||||||
#[arg(long = "rw", value_name = "PATH", action = clap::ArgAction::Append)]
|
#[arg(long = "rw", value_name = "PATH", action = clap::ArgAction::Append)]
|
||||||
@@ -35,9 +43,13 @@ pub struct Args {
|
|||||||
pub extra_ro: Vec<PathBuf>,
|
pub extra_ro: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Print the bwrap command without executing
|
/// Print the bwrap command without executing
|
||||||
#[arg(long)]
|
#[arg(long, overrides_with = "no_dry_run")]
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
|
||||||
|
/// Disable dry-run (overrides config-file `dry-run = true`)
|
||||||
|
#[arg(long, overrides_with = "dry_run")]
|
||||||
|
pub no_dry_run: bool,
|
||||||
|
|
||||||
/// Working directory inside the sandbox (default: current directory)
|
/// Working directory inside the sandbox (default: current directory)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
pub chdir: Option<PathBuf>,
|
pub chdir: Option<PathBuf>,
|
||||||
|
|||||||
@@ -28,9 +28,21 @@ pub fn build(args: Args, file_config: Option<FileConfig>) -> Result<SandboxConfi
|
|||||||
|
|
||||||
Ok(SandboxConfig {
|
Ok(SandboxConfig {
|
||||||
mode: merge_mode(args.blacklist, args.whitelist, &profile, &globals),
|
mode: merge_mode(args.blacklist, args.whitelist, &profile, &globals),
|
||||||
hardened: merge_flag(args.hardened, profile.hardened, globals.hardened),
|
hardened: merge_flag(
|
||||||
no_net: merge_flag(args.no_net, profile.no_net, globals.no_net),
|
merge_flag_pair(args.hardened, args.no_hardened),
|
||||||
dry_run: merge_flag(args.dry_run, profile.dry_run, globals.dry_run),
|
profile.hardened,
|
||||||
|
globals.hardened,
|
||||||
|
),
|
||||||
|
unshare_net: merge_flag(
|
||||||
|
merge_flag_pair(args.unshare_net, args.share_net),
|
||||||
|
profile.unshare_net,
|
||||||
|
globals.unshare_net,
|
||||||
|
),
|
||||||
|
dry_run: merge_flag(
|
||||||
|
merge_flag_pair(args.dry_run, args.no_dry_run),
|
||||||
|
profile.dry_run,
|
||||||
|
globals.dry_run,
|
||||||
|
),
|
||||||
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
|
chdir: resolve_chdir(args.chdir, profile.chdir, globals.chdir)?,
|
||||||
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
|
extra_rw: merge_paths(args.extra_rw, &profile.rw, &globals.rw)?,
|
||||||
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
|
extra_ro: merge_paths(args.extra_ro, &profile.ro, &globals.ro)?,
|
||||||
@@ -70,11 +82,18 @@ fn resolve_mode(opts: &Options) -> Option<SandboxMode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_flag(cli: bool, profile: Option<bool>, globals: Option<bool>) -> bool {
|
fn merge_flag(cli: Option<bool>, profile: Option<bool>, globals: Option<bool>) -> bool {
|
||||||
if cli {
|
cli.or(profile).or(globals).unwrap_or(false)
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
fn merge_flag_pair(enable: bool, disable: bool) -> Option<bool> {
|
||||||
|
if enable {
|
||||||
|
Some(true)
|
||||||
|
} else if disable {
|
||||||
|
Some(false)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
profile.or(globals).unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_chdir(
|
fn resolve_chdir(
|
||||||
@@ -224,7 +243,7 @@ pub struct Options {
|
|||||||
pub blacklist: Option<bool>,
|
pub blacklist: Option<bool>,
|
||||||
pub whitelist: Option<bool>,
|
pub whitelist: Option<bool>,
|
||||||
pub hardened: Option<bool>,
|
pub hardened: Option<bool>,
|
||||||
pub no_net: Option<bool>,
|
pub unshare_net: Option<bool>,
|
||||||
pub entrypoint: Option<CommandValue>,
|
pub entrypoint: Option<CommandValue>,
|
||||||
pub command: Option<CommandValue>,
|
pub command: Option<CommandValue>,
|
||||||
pub dry_run: Option<bool>,
|
pub dry_run: Option<bool>,
|
||||||
@@ -360,7 +379,7 @@ mod tests {
|
|||||||
|
|
||||||
const FULL_CONFIG_TOML: &str = r#"
|
const FULL_CONFIG_TOML: &str = r#"
|
||||||
hardened = true
|
hardened = true
|
||||||
no-net = true
|
unshare-net = true
|
||||||
rw = ["/tmp/a", "/tmp/b"]
|
rw = ["/tmp/a", "/tmp/b"]
|
||||||
command = "zsh"
|
command = "zsh"
|
||||||
|
|
||||||
@@ -383,7 +402,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn globals_scalars() {
|
fn globals_scalars() {
|
||||||
assert_eq!(CONFIG.options.hardened, Some(true));
|
assert_eq!(CONFIG.options.hardened, Some(true));
|
||||||
assert_eq!(CONFIG.options.no_net, Some(true));
|
assert_eq!(CONFIG.options.unshare_net, Some(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -488,7 +507,7 @@ mod tests {
|
|||||||
profile: HashMap::from([(
|
profile: HashMap::from([(
|
||||||
"nonet".into(),
|
"nonet".into(),
|
||||||
Options {
|
Options {
|
||||||
no_net: Some(false),
|
unshare_net: Some(false),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
@@ -496,11 +515,62 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let args = Args {
|
let args = Args {
|
||||||
profile: Some("nonet".into()),
|
profile: Some("nonet".into()),
|
||||||
no_net: true,
|
unshare_net: true,
|
||||||
..args_with_command()
|
..args_with_command()
|
||||||
};
|
};
|
||||||
let config = build(args, Some(file_config)).unwrap();
|
let config = build(args, Some(file_config)).unwrap();
|
||||||
assert!(config.no_net);
|
assert!(config.unshare_net);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_cli_no_hardened_overrides_profile() {
|
||||||
|
let file_config = FileConfig {
|
||||||
|
options: Options {
|
||||||
|
hardened: Some(true),
|
||||||
|
..Options::default()
|
||||||
|
},
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let args = Args {
|
||||||
|
no_hardened: true,
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
let config = build(args, Some(file_config)).unwrap();
|
||||||
|
assert!(!config.hardened);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_cli_share_net_overrides_profile() {
|
||||||
|
let file_config = FileConfig {
|
||||||
|
options: Options {
|
||||||
|
unshare_net: Some(true),
|
||||||
|
..Options::default()
|
||||||
|
},
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let args = Args {
|
||||||
|
share_net: true,
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
let config = build(args, Some(file_config)).unwrap();
|
||||||
|
assert!(!config.unshare_net);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_cli_no_dry_run_overrides_profile() {
|
||||||
|
let file_config = FileConfig {
|
||||||
|
options: Options {
|
||||||
|
dry_run: Some(true),
|
||||||
|
..Options::default()
|
||||||
|
},
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let args = Args {
|
||||||
|
no_dry_run: true,
|
||||||
|
..args_with_command()
|
||||||
|
};
|
||||||
|
let config = build(args, Some(file_config)).unwrap();
|
||||||
|
assert!(!config.dry_run);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub enum SandboxMode {
|
|||||||
pub struct SandboxConfig {
|
pub struct SandboxConfig {
|
||||||
pub mode: SandboxMode,
|
pub mode: SandboxMode,
|
||||||
pub hardened: bool,
|
pub hardened: bool,
|
||||||
pub no_net: bool,
|
pub unshare_net: bool,
|
||||||
pub extra_rw: Vec<PathBuf>,
|
pub extra_rw: Vec<PathBuf>,
|
||||||
pub extra_ro: Vec<PathBuf>,
|
pub extra_ro: Vec<PathBuf>,
|
||||||
pub mask: Vec<PathBuf>,
|
pub mask: Vec<PathBuf>,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub fn build_command(config: &SandboxConfig) -> Result<Command, SandboxError> {
|
|||||||
cmd.args(["--unshare-ipc", "--unshare-pid", "--unshare-uts"]);
|
cmd.args(["--unshare-ipc", "--unshare-pid", "--unshare-uts"]);
|
||||||
cmd.args(["--hostname", "sandbox"]);
|
cmd.args(["--hostname", "sandbox"]);
|
||||||
}
|
}
|
||||||
if config.no_net {
|
if config.unshare_net {
|
||||||
cmd.arg("--unshare-net");
|
cmd.arg("--unshare-net");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ fn ssh_dir_is_hidden() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_net_blocks_network() {
|
fn unshare_net_blocks_network() {
|
||||||
let output = sandbox(&["--no-net"])
|
let output = sandbox(&["--unshare-net"])
|
||||||
.args([
|
.args([
|
||||||
"--",
|
"--",
|
||||||
"bash",
|
"bash",
|
||||||
|
|||||||
Reference in New Issue
Block a user