diff --git a/CLAUDE.md b/CLAUDE.md index 4603a28..a39a890 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,10 @@ Source layout (all under `src/`): `run_container()` resolves each spec (erroring out, `ok = false`, same as a failed `--user` resolution — `bwrap` is skipped but unmount/cleanup still runs) into a `ResolvedVolumeMount`, rejecting a duplicate or non-absolute container - path first, and passes the resolved list to `run_bwrap()`. + path first, and passes the resolved list to `run_bwrap()`. `--hostname ` + (long-option only, no short form) is likewise threaded straight through + `run_container()` into `run_bwrap()`/`build_bwrap_args()` (`bwrap.{h,cpp}`) — + see there for how/when it actually takes effect. - `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive + nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory (non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive, @@ -80,6 +83,12 @@ Source layout (all under `src/`): `slirp4netns`), unsharing it just leaves the sandbox with no network at all. Re-add once network isolation is implemented; `detect_bwrap_unshare_args()` itself still probes/reports it (e.g. via `-t/--test`), since that's kernel capability, not policy. + `build_bwrap_args()`/`run_bwrap()` also take an optional `hostname` (from + `--hostname`, long-option only): passed through as bwrap's own `--hostname` only + when `--unshare-uts` is actually among the flags `bwrap` is being given (bwrap + itself refuses `--hostname` without it) — otherwise logs a warning and leaves the + sandbox's hostname alone, since a stock Android kernel in degraded mode may not + support a UTS namespace at all. Never requests `--unshare-user` when running as root: root doesn't need a fresh user namespace for privilege, and bwrap's own single-mapping uid/gid setup for one triggers the kernel's unprivileged-userns setgroups() restriction, which showed up @@ -212,8 +221,8 @@ Build directory is `buildDir/` (already configured). - Run the executable: `./buildDir/slocker-lite -m ` (see `--help` for the full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`, `-l/--list-images`, `-i/--inspect`, `-n/--no-nsenter`, `--user`, `--group`, - `-v/--volume`, `--list-volumes`, `--delete-volume`, `--delete-volume-full`, - `-t/--test`, `--log-level`, `-h/--help`, `-V/--version`) + `--hostname`, `-v/--volume`, `--list-volumes`, `--delete-volume`, + `--delete-volume-full`, `-t/--test`, `--log-level`, `-h/--help`, `-V/--version`) - Run tests: `meson test -C buildDir` ## Code style diff --git a/README.md b/README.md index 59ff4b0..74bfdc8 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ slocker-lite -V|--version | `-n, --no-nsenter` | With `--run`, bind the mount directly instead of `nsenter`-ing into `fuse-overlayfs`'s namespace. Automatic when running as root; use this to force it off otherwise. | | `--user ` | With `--run`, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own `/etc/passwd`. Only takes effect when `--run` executes as root. | | `--group ` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. | +| `--hostname ` | With `--run`, set the sandbox's hostname. Only takes effect if the running kernel supports `--unshare-uts`; ignored with a warning otherwise. | | `-l, --list-images ` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in ``, with their `name:tag`. | | `-i, --inspect ` | Print an image's declared user, exposed ports, env, volumes, and default command, without mounting or running it. | | `-v, --volume ` | Create a named volume mapped to a host directory (created if missing), recorded in the config file's `volumes` section. Fails if the name or directory is already used by an existing volume. Volume names can't contain `/`. With `--run`, instead mounts a volume into the sandbox (repeatable): `` is an existing named volume, or, if it contains `/`, a host directory path (created if missing); `` is the absolute path inside the container to mount it at. If the host directory is empty and the image already has content there, that content is copied in first, preserving numeric ownership/permissions/links and, where the host filesystem supports them, extended attributes/ACLs (skipped with a warning otherwise). | @@ -102,6 +103,9 @@ slocker-lite -V|--version # Run as a specific user (as root only) sudo ./buildDir/slocker-lite -r myimage.tar --user git +# Run with a custom hostname inside the sandbox +./buildDir/slocker-lite -r myimage.tar --hostname mybox + # List every OCI image tar in a directory ./buildDir/slocker-lite -l ./images diff --git a/src/bwrap.cpp b/src/bwrap.cpp index 7d4c81f..792297b 100644 --- a/src/bwrap.cpp +++ b/src/bwrap.cpp @@ -179,7 +179,8 @@ std::vector detect_bwrap_unshare_args() { std::vector build_bwrap_args(const std::string& root, const std::vector& command, const std::vector& volumes, - std::optional user) { + std::optional user, + const std::optional& hostname) { // --new-session detaches from the controlling terminal, which breaks job // control for an interactive foreground shell ("can't access tty"). Re-enable // once background/daemonized runs are implemented, where that's the point. @@ -187,6 +188,7 @@ std::vector build_bwrap_args(const std::string& root, auto unshare_args = detect_bwrap_unshare_args(); bool has_pid_ns = false; + bool has_uts_ns = false; for (const auto& arg : unshare_args) { // Not requested yet: without any network setup (slirp4netns or similar), // unsharing it just leaves the sandbox with no network at all. Re-add once @@ -199,12 +201,26 @@ std::vector build_bwrap_args(const std::string& root, args.push_back(arg); if (arg == "--unshare-pid") { has_pid_ns = true; + } else if (arg == "--unshare-uts") { + has_uts_ns = true; } } if (has_pid_ns) { args.push_back("--as-pid-1"); } + if (hostname) { + // bwrap itself refuses --hostname without --unshare-uts, so this must be + // guarded the same way rather than always passed through -- the kernel may + // not support a UTS namespace at all (degraded-mode Android kernels). + if (has_uts_ns) { + args.insert(args.end(), {"--hostname", *hostname}); + } else { + spdlog::warn("--hostname requires an unshared UTS namespace, which the running " + "kernel doesn't support here; ignoring --hostname"); + } + } + std::vector filesystem_args = { "--bind", root, @@ -288,14 +304,15 @@ std::optional> wrap_for_root_namespace(const std::strin } int run_bwrap(const std::string& root, const std::vector& command, bool use_nsenter, - const std::vector& volumes, std::optional user) { + const std::vector& volumes, std::optional user, + const std::optional& hostname) { if (user && !find_priv_drop_helper()) { spdlog::error("could not find the {} helper next to this binary; --user/--group requires it", kPrivDropHelperName); return -1; } - auto bwrap_args = build_bwrap_args(root, command, volumes, user); + auto bwrap_args = build_bwrap_args(root, command, volumes, user, hostname); auto argv = wrap_for_root_namespace(root, use_nsenter, bwrap_args); if (!argv) { return -1; diff --git a/src/bwrap.h b/src/bwrap.h index 3796d53..fed67ba 100644 --- a/src/bwrap.h +++ b/src/bwrap.h @@ -55,11 +55,14 @@ struct ResolvedUser { // in volume_mount.h). If `user` is set, the command is wrapped so it drops to that // uid/gid before running -- see run_bwrap() for how, since bwrap's own --uid/--gid // require --unshare-user, which isn't requested when running as root (see -// detect_bwrap_unshare_args()). +// detect_bwrap_unshare_args()). If `hostname` is set and the kernel supports +// --unshare-uts (bwrap refuses --hostname without it), passes it as bwrap's own +// --hostname; otherwise logs a warning and leaves the sandbox's hostname alone. std::vector build_bwrap_args(const std::string& root, const std::vector& command, const std::vector& volumes, - std::optional user); + std::optional user, + const std::optional& hostname); // Runs bwrap against `root` (the merged mount path from mount_layer()) in the // foreground and waits for it to exit. If `use_nsenter` is true, first locates the @@ -71,6 +74,9 @@ std::vector build_bwrap_args(const std::string& root, // (observed on kernels older than 4.18, per fuse-overlayfs's own release notes). // If `user` is set, this process's own binary is bind-mounted into the sandbox and // used to drop privileges to that uid/gid before running `command` -- see -// build_bwrap_args(). Returns bwrap's exit code, or -1 on failure to launch. +// build_bwrap_args(). `hostname`, if set, is forwarded to build_bwrap_args() -- +// see there for when it does/doesn't take effect. Returns bwrap's exit code, or -1 +// on failure to launch. int run_bwrap(const std::string& root, const std::vector& command, bool use_nsenter, - const std::vector& volumes, std::optional user); + const std::vector& volumes, std::optional user, + const std::optional& hostname); diff --git a/src/main.cpp b/src/main.cpp index 3ccd369..c9ba5cb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -58,18 +58,19 @@ enum class Mode { kInspect }; -// --log-level/--user/--group/--list-volumes/--delete-volume[-full] have no short -// form (--log-level's was freed up so -l could become --list-images; -u is already -// --umount; the rest have no natural free letter left, or don't need one), so they -// need long-option vals outside the printable-char range short options use. +// --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname have +// no short form (--log-level's was freed up so -l could become --list-images; -u is +// already --umount; the rest have no natural free letter left, or don't need one), +// so they need long-option vals outside the printable-char range short options use. constexpr int kLogLevelOpt = 256; constexpr int kUserOpt = 257; constexpr int kGroupOpt = 258; constexpr int kListVolumesOpt = 259; constexpr int kDeleteVolumeOpt = 260; constexpr int kDeleteVolumeFullOpt = 261; +constexpr int kHostnameOpt = 262; -constexpr std::array kLongOptions = {{ +constexpr std::array kLongOptions = {{ {"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"test", no_argument, nullptr, 't'}, @@ -87,6 +88,7 @@ constexpr std::array kLongOptions = {{ {"delete-volume", required_argument, nullptr, kDeleteVolumeOpt}, {"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt}, {"inspect", required_argument, nullptr, 'i'}, + {"hostname", required_argument, nullptr, kHostnameOpt}, {nullptr, 0, nullptr, 0}, }}; @@ -131,6 +133,10 @@ void print_usage(const char* prog) { " (no user namespace involved)\n" " --group with --user, use this group (name or numeric\n" " gid) instead of the user's own primary group\n" + " --hostname with --run, set the sandbox's hostname (only\n" + " takes effect if the running kernel supports\n" + " --unshare-uts; otherwise ignored with a\n" + " warning)\n" " -l, --list-images list OCI Image Layout tars (*.tar, *.tar.*) found\n" " directly in , with their name:tag\n" " -i, --inspect print an image's declared user, exposed ports,\n" @@ -453,6 +459,7 @@ int delete_volume_command(const std::string& name, const std::filesystem::path& int run_container(const std::filesystem::path& image_tar, const std::vector& requested_command, bool use_nsenter, const std::optional& user, const std::optional& group, + const std::optional& hostname, const std::vector>& volume_specs, const AppConfig& app_config) { auto mounted = mount_image(image_tar); @@ -517,7 +524,7 @@ int run_container(const std::filesystem::path& image_tar, int exit_code = -1; if (ok) { - exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user); + exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname); if (exit_code < 0) { spdlog::error("failed to run bwrap"); } @@ -550,6 +557,7 @@ int main(int argc, char* argv[]) { bool disable_nsenter = false; std::optional user_flag; std::optional group_flag; + std::optional hostname_flag; std::vector> volume_specs; opterr = 0; @@ -645,6 +653,9 @@ int main(int argc, char* argv[]) { case kGroupOpt: group_flag = optarg; break; + case kHostnameOpt: + hostname_flag = optarg; + break; case ':': spdlog::error("option requires an argument: -{}", static_cast(optopt)); print_usage(argv[0]); @@ -720,7 +731,8 @@ int main(int argc, char* argv[]) { if (geteuid() == 0 && !disable_nsenter) { spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)"); } - return run_container(mode_arg, command, use_nsenter, user_flag, group_flag, volume_specs, *config); + return run_container(mode_arg, command, use_nsenter, user_flag, group_flag, hostname_flag, volume_specs, + *config); } auto mounted = mount_image(mode_arg);