From 29d095ccccf54aedf6a9f67f5f676ad1770232dd Mon Sep 17 00:00:00 2001 From: Taeyeon Mori Date: Thu, 4 Nov 2021 18:50:28 +0100 Subject: [PATCH] src: Add accumulated native code --- src/.gitignore | 8 + src/README.md | 80 ++++ src/chome.cpp | 141 +++++++ src/fakensudo.cpp | 187 ++++++++++ src/keepassxc-browser.hpp | 429 ++++++++++++++++++++++ src/keepassxc-print.cpp | 68 ++++ src/kofd.hpp | 692 +++++++++++++++++++++++++++++++++++ src/kofd_pipe.hpp | 78 ++++ src/kofs.hpp | 110 ++++++ src/kons.hpp | 241 ++++++++++++ src/kons_clone.hpp | 81 ++++ src/koos.hpp | 68 ++++ src/koproc.hpp | 418 +++++++++++++++++++++ src/koutil.hpp | 90 +++++ src/makefile | 95 +++++ src/overlayns.cpp | 431 ++++++++++++++++++++++ src/ssh-overlay-kiosk.cpp | 192 ++++++++++ src/steamns.cpp | 591 ++++++++++++++++++++++++++++++ src/workspace.code-workspace | 11 + 19 files changed, 4011 insertions(+) create mode 100644 src/.gitignore create mode 100644 src/README.md create mode 100644 src/chome.cpp create mode 100644 src/fakensudo.cpp create mode 100644 src/keepassxc-browser.hpp create mode 100644 src/keepassxc-print.cpp create mode 100644 src/kofd.hpp create mode 100644 src/kofd_pipe.hpp create mode 100644 src/kofs.hpp create mode 100644 src/kons.hpp create mode 100644 src/kons_clone.hpp create mode 100644 src/koos.hpp create mode 100644 src/koproc.hpp create mode 100644 src/koutil.hpp create mode 100644 src/makefile create mode 100644 src/overlayns.cpp create mode 100644 src/ssh-overlay-kiosk.cpp create mode 100644 src/steamns.cpp create mode 100644 src/workspace.code-workspace diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..f16f746 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,8 @@ +/* +!/**/ +!/*.hpp +!/*.cpp +!/makefile +!/.* +!/*.code-workspace +!/README.md diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..e5aa7c9 --- /dev/null +++ b/src/README.md @@ -0,0 +1,80 @@ +Random Sources +============== +Playground for random utilities, mostly unprivileged single-user linux namespaces + +Binaries +-------- + +### chome +Bind mount a different directory on top of $HOME to (partially) isolate a process + +### fakensudo +Pretend to be root (uid 0) by running in a single-user namespace mapping one's own UID to 0 + +### keepassxc-print +Retrieve passwords from KeePassXC on the commandline via the browser interface. + +### overlayns +Run a command in a custom mount namespace. Like `unshare -mUc` with the added possibility of setting up custom mounts in the namespace before running the target application + +### ssh-overlay-kiosk +Create an emphemeral home directory for each invocation. + +### steamns +Isolate steam (and other 32-bit apps) in an unprivileged single-user-namespace "chroot" + +Libraries +--------- + +### keepassxc-browser.hpp +Very simple library for interacting with KeePassXC's browser interface from native code + +Depends on libsodium, jsoncpp, ko::proc + +### ko::fd +Convenient wrapper around Linux APIs with dirfd support + +kofd\_pipe.hpp adds a class for working with pairs of uni-directional pipes + +Depends on ko::fs + +### ko::fs +Misc. filesystem utilities + +- cpath: Type that is trivially convertible to const char\* and from std::string and std::filesystem::path +- dir\_ptr: Convenient iterator-based wrapper around the dirent API + +### ko::ns +Utilities for working with Linux Namespaces (unshare, clone, setns) + +Depends on ko::util, ko::fd, ko::os + +- ko::ns::idmap: Functions for writing /proc/$$/Xidmap +- ko::ns::mount: Functions for setting up mount namespaces +- ko::ns::clone: Helpers for spawning processes in new namespaces (kons\_clone.hpp, requires ko::proc) + +### ko::os +Misc. OS helpers + +Depends on ko:: fs + +- get\_home() +- is\_mountpoint() + +### ko::proc +Utilities for spawning and managing child processes + +Depends on pthread, ko::fd + +- popen[p]: Spawn subprocess and communicate via pipes +- sync::semapair: Synchronization across processes +- child\_ref: Child process reference with cleanup +- [s]vclone: Wrappers around linux clone(CLONE\_VM) +- simple\_spawn: Trivial fork(); execvp() primitive + +### ko::util +Misc. utilities + +- str: Type-safe-ly concatenate all arguments +- cvshort: Short-circuit continuation using C-Style return codes + diff --git a/src/chome.cpp b/src/chome.cpp new file mode 100644 index 0000000..f8fefdc --- /dev/null +++ b/src/chome.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include + +#include "koutil.hpp" +#include "kofd.hpp" +#include "koos.hpp" + +namespace fs = std::filesystem; + +void usage(const char *prog) { + std::cout << "Usage: " << prog << " [option...] [prog] [arg...]" << std::endl + << " (c) 2019 Taeyeon Mori" << std::endl + << std::endl + << " This program allows confining an application to it's own home directory" << std::endl + << " without chainging the literal home directory path." << std::endl + << std::endl + << "Options:" << std::endl + << " -h Display this help text" << std::endl + << " -H HOME Override the home directory path" << std::endl + << " -w Don't make / read-only" << std::endl + << " -W Preserve working directory" << std::endl + //<< " -s Make (the rest of) /home inaccessible" << std::endl + //<< " -S Make /media and /mnt inaccessible as well (implies -s)" << std::endl + //<< " -x PATH Make path inaccessible" << std::endl + << std::endl + << "Parameters:" << std::endl + << " newhome The new home directory path" << std::endl + << " prog The executable to run (defaults to $SHELL)" << std::endl + << " arg... The executable parameters" << std::endl; +} + +struct params { + fs::path home, newhome; + bool rw = false, + nohome = false, + nomnt = false, + pwd = true; + std::unordered_set hide; + const char *const *argv = nullptr; +}; + +int bindfile(const params &p, fs::path path) { + auto opath = p.home / path; + if (fs::exists(opath)) { + auto npath = p.newhome / path; + + if (!fs::exists(npath)) { + if (fs::is_directory(opath)) + fs::create_directories(npath); + else { + fs::create_directories(npath.parent_path()); + auto touch = std::ofstream(npath); + } + } + + if(ko::os::bind(opath, npath, 0)) + return -1; + return ko::os::bind(npath, npath, MS_REMOUNT|MS_RDONLY); + } + return 0; +} + +int pmain(params p) { + auto uid = getuid(), + gid = getgid(); + auto [e, eloc] = ko::util::cvshort() + .then("unshare", ::unshare, CLONE_NEWUSER|CLONE_NEWNS) + .then("bind Xauthority", bindfile, p, ".Xauthority") + .then("bind pulse cookie", bindfile, p, ".config/pulse/cookie") + .then("bind home", ko::os::bind, p.newhome, p.home, MS_REC) + .ifthen("make / ro", !p.rw, ko::os::bind, "/", "/", MS_REMOUNT|MS_RDONLY) + .ifthen("chdir", p.pwd, ::chdir, p.home.c_str()) + .then([uid,gid]() -> ko::util::cvresult { + auto dir = ko::fd::opendir("/proc/self"); + if (!dir) + return {-1, "open /proc/self"}; + if (!ko::fd::dump("deny", "setgroups", 0644, dir)) + return {-1, "write setgroups"}; + if (!ko::fd::dump(ko::util::str(gid, " ", gid, " 1\n"), "gid_map", 0644, dir)) + return {-1, "write gid_map"}; + if (!ko::fd::dump(ko::util::str(uid, " ", uid, " 1\n"), "uid_map", 0644, dir)) + return {-1, "write uid_map"}; + return {0, nullptr}; + }) + .then("setresgid", ::setresgid, gid, gid, gid) + .then("setresuid", ::setresuid, uid, uid, uid) + .then("exec", ::execvp, p.argv[0], const_cast(p.argv)); + perror(eloc); + return e; +} + +int main(int argc, char **argv) { + static const char *exec_argv[] = {getenv("SHELL"), nullptr}; + params p{ + .home = ko::os::get_home(), + .argv = exec_argv + }; + + constexpr auto spec = "+hH:wsSxW"; + + while (true) { + auto opt = getopt(argc, const_cast(argv), spec); + + if (opt == -1) + break; + else if (opt == '?' || opt == 'h') { + usage(argv[0]); + return opt == 'h' ? 0 : 1; + } + else if (opt == 'H') + p.home = ::optarg; + else if (opt == 'w') + p.rw = true; + else if (opt == 'W') + p.pwd = false; + else if (opt == 's' || opt == 'S') { + p.hide.emplace("/home"); + if (opt == 'S') { + p.hide.emplace("/media"); + p.hide.emplace("/mnt"); + } + } + else if (opt == 'x') + p.hide.emplace(::optarg); + } + + if (argc == ::optind) { + std::cout << "Error: missing mandatory newhome argument, see `" << argv[0] << " -h`" << std::endl; + return 2; + } + + p.newhome = argv[::optind++]; + + if (argc > ::optind) + p.argv = &argv[::optind]; + + return pmain(p); +} + diff --git a/src/fakensudo.cpp b/src/fakensudo.cpp new file mode 100644 index 0000000..001fb74 --- /dev/null +++ b/src/fakensudo.cpp @@ -0,0 +1,187 @@ +// Fake sudo using user namespace; Similar to fakeroot +// (c) 2020 Taeyeon Mori + +#include "kons.hpp" + +#include +#include + +#include +#include +#include +#include + + +// Helpers +int xerror(const char *desc) { + perror(desc); + return -errno; +} + +[[noreturn]] void die(int r, const char *msg) { + std::cerr << msg << std::endl; + exit(r); +} + +[[noreturn]] void die_errno(int r, const char *msg) { + perror(msg); + exit(r); +} + +// ======================================================== +// Main +// ======================================================== +void usage(const char *prog) { + std::cout << "Usage:" << std::endl + << " " << prog << " -h | -K | -k | -V" << std::endl + << " " << prog << " -v [-k] [-u user] [-g group]" << std::endl + << " " << prog << " -e [-k] [-u user] [-g group] [--] file" << std::endl + << " " << prog << " [-bEHPk] [-u user] [-g group] [-i|-s] [--] command" << std::endl + << std::endl + << "General Options:" << std::endl + << " -h Display this help text" << std::endl + << std::endl; +} + +struct config { + const char *const *exec_argv = nullptr; + bool background = false, + preserve_env = false, + editor = false, + login = false, + set_home = false, + preserve_groups = false, + run_shell = false; + uid_t uid = 0; + gid_t gid = 0; +}; + +template +R get_pwd(F f, R std::remove_pointer_t>::*fld, T nam) { + auto s = f(nam); + + if (!s) + die(20, "Could not resolve user or group"); + + return s->*fld; +} + +// Parse commandline arguments +// returns -1 on success, exit code otherwise +int parse_cmdline(config &conf, int argc, const char *const *argv) { + constexpr auto spec = "+hbEeg:HiKkPpsu:Vv"; + constexpr option longspec[] = {{"help",0,nullptr,'h'}, + {"background",0,nullptr,'b'}, + {"preserve-env",2,nullptr,'E'}, + {"edit",0,nullptr,'e'}, + {"group",1,nullptr,'g'}, + {"set-home",0,nullptr,'H'}, + {"login",0,nullptr,'i'}, + {"remove-timestamp",0,nullptr,'K'}, + {"reset-timestamp",0,nullptr,'k'}, + {"preserve-groups",0,nullptr,'P'}, + {"prompt",1,nullptr,'p'}, + {"shell",0,nullptr,'s'}, + {"user",1,nullptr,'u'}, + {"version",0,nullptr,'V'}, + {"validate",0,nullptr,'v'}, + {nullptr,0,nullptr,0}}; + + while (true) { + auto opt = getopt_long(argc, const_cast(argv), spec, longspec, nullptr); + + if (opt == -1) + break; + else if (opt == '?' || opt == 'h') { + usage(argv[0]); + return opt == 'h' ? 0 : 1; + } + else if (opt == 'V') { + std::cout << "fakensudo Namespace fake sudo version 0.1" << std::endl + << "(c) 2020 Taeyeon Mori" << std::endl; + return 0; + } + + else if (opt == 'b') conf.background = true; + else if (opt == 'E') conf.preserve_env = true; // XXX: ignores the optinal list + else if (opt == 'e') conf.editor = true; + else if (opt == 'g') conf.gid = get_pwd(getgrnam, &group::gr_gid, optarg); + else if (opt == 'H') conf.set_home = true; + else if (opt == 'i') conf.login = true; + else if (opt == 'K') return 0; // XXX: check for clashes + else if (opt == 'k') /* pass */; + else if (opt == 'P') conf.preserve_groups = true; + else if (opt == 'p') /* pass */; + else if (opt == 's') conf.run_shell = true; + else if (opt == 'u') conf.uid = get_pwd(getpwnam, &passwd::pw_uid, optarg); + else if (opt == 'v') return 0; // XXX: properly check options + else die(10, "Unknown option encountered"); + } + + // Check sanity + bool good = true; + + if (conf.run_shell || conf.login) { + if (conf.run_shell && conf.login) + good = false; + if (conf.editor) + good = false; + } else if (::optind >= argc) + good = false; + + if (!good) { + usage(argv[0]); + return 5; + } + + // Rest is child cmnd + if (argc > ::optind) + conf.exec_argv = &argv[::optind]; + + return -1; +} + + +int main(int argc, char **argv) { + // Set defaults + auto conf = config{}; + + // Parse commandline + auto perr = parse_cmdline(conf, argc, argv); + if (perr != -1) + return perr; + + auto uerr = ko::ns::unshare_single(conf.uid, conf.gid, CLONE_NEWUSER); + if (uerr != 0) + die_errno(31, "unshare"); + + // Drop Permissions + setresgid(conf.gid, conf.gid, conf.gid); + setresuid(conf.uid, conf.uid, conf.uid); + + auto exec_argv = conf.exec_argv; + if (conf.run_shell) { + auto shell = getenv("SHELL"); + if (shell == nullptr) + die(41, "Could not get SHELL from environment"); + if (conf.exec_argv == nullptr || *conf.exec_argv == nullptr) + exec_argv = new char*[]{shell, nullptr}; + else + die(200, "-s not fully implemented"); + } else if (conf.login) { + auto shell = get_pwd(getpwuid, &passwd::pw_shell, conf.uid); + if (shell == nullptr) + die(41, "Could not get SHELL from passwd record"); + if (conf.exec_argv == nullptr || *conf.exec_argv == nullptr) + exec_argv = new char*[]{shell, "-l", nullptr}; + else + die(200, "-i not fully implemented"); + } else if (conf.editor) { + die(200, "-e not implemented"); + } + + // Exec + execvpe(exec_argv[0], const_cast(exec_argv), environ); + die_errno(33, "exec"); +} + diff --git a/src/keepassxc-browser.hpp b/src/keepassxc-browser.hpp new file mode 100644 index 0000000..6548558 --- /dev/null +++ b/src/keepassxc-browser.hpp @@ -0,0 +1,429 @@ +// Simple Client-Library for the KeePassXC-Browser API +// (c) 2019 Taeyeon Mori +// +// Depends on: libsodium, jsoncpp +// +// NOTE: Users must make sure to initialize libsodium! +// WARNING: This currently does nothing to protect the keys in memory. +// Such measures could be added to crypto::, but as the key material +// is stored in a plain file on disk anyway, that seems to be a lot of useless work. +// This applies especially for small, short-lived cli utilities. +// WARNING: With a plain secrets file, the 'Never ask before accessing credentials' option in +// in KeePassXC becomes an even bigger security risk! + +#pragma once + +#include "koproc.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +namespace keepassxc { + using string = std::string; + using data = std::basic_string; + + // Hack, but libsodium insists on unsigned char + // The result of this is cleaner than having individual + // casts all over the place and as a side benefit, it + // tends to prevent toughtlessly trying to put binary + // data into json directly. + const data &data_cast(const string &s) { + return *reinterpret_cast(&s); + } + + const string &nodata_cast(const data &d) { + return *reinterpret_cast(&d); + } + + /** + * Cryptography goes here + */ + namespace crypto { + data generate_nonce() { + auto nonce = data(crypto_box_NONCEBYTES, 0); + ::randombytes(nonce.data(), crypto_box_NONCEBYTES); + return nonce; + } + + /// Return [[public_key, secret_key]] + std::optional> generate_keypair() { + auto seckey = data(crypto_box_SECRETKEYBYTES, 0); + auto pubkey = data(crypto_box_PUBLICKEYBYTES, 0); + if (::crypto_box_keypair(pubkey.data(), seckey.data()) == 0) + return {{pubkey, seckey}}; + return {}; + } + + data encrypt(const data &plain, const data &nonce, const data &pubkey, const data &seckey) { + auto cipher = data(plain.size() + crypto_box_MACBYTES, 0); + const auto ok = crypto_box_easy(cipher.data(), plain.data(), plain.size(), nonce.data(), pubkey.data(), seckey.data()) == 0; + return ok ? cipher : data(); + } + + data decrypt(const data &cipher, const data &nonce, const data &pubkey, const data &seckey) { + auto plain = data(cipher.size() - crypto_box_MACBYTES, 0); + const auto ok = crypto_box_open_easy(plain.data(), cipher.data(), cipher.size(), nonce.data(), pubkey.data(), seckey.data()) == 0; + return ok ? plain : data(); + } + + string b64encode(const data &dec) { + auto enc = string(sodium_base64_ENCODED_LEN(dec.size(), sodium_base64_VARIANT_ORIGINAL), 0); + ::sodium_bin2base64(enc.data(), enc.size(), dec.data(), dec.size(), sodium_base64_VARIANT_ORIGINAL); + enc.resize(enc.find_first_of('\0')); + return enc; + } + + std::optional b64decode(const string &enc) { + auto dec = data(enc.size() * 3 / 4 + 1, 0); + size_t data_len = 0; + if (::sodium_base642bin(dec.data(), dec.size(), enc.data(), enc.size(), + nullptr, &data_len, nullptr, sodium_base64_VARIANT_ORIGINAL) == 0) { + dec.resize(data_len); + return dec; + } + return {}; + } + + void increment(data &n) { + ::sodium_increment(n.data(), n.size()); + } + } + + /** + * The keepassxc client configuration + */ + struct config { + static constexpr auto CONF_PUBKEY = "public_key", + CONF_PRIVKEY = "private_key", + CONF_DATABASES = "databases"; + + data public_key, private_key; + std::unordered_map dbs; + + /** + * Create a new configuration + * @note This creates the persistent key pair + */ + static std::optional create() { + auto keys = crypto::generate_keypair(); + if (!keys) + return {}; + auto [public_key, private_key] = keys.value(); + return config{ + .public_key = public_key, + .private_key = private_key, + .dbs = {}, + }; + } + + /** + * Load configuration from a JSON object + * @param conf The JSON object + */ + static std::optional load(const Json::Value &conf) { + if (!conf.isMember(CONF_PUBKEY) || !conf.isMember(CONF_PRIVKEY)) + return std::nullopt; + + auto public_key = crypto::b64decode(conf[CONF_PUBKEY].asString()); + if (!public_key) + return std::nullopt; + + auto private_key = crypto::b64decode(conf[CONF_PRIVKEY].asString()); + if (!private_key) + return std::nullopt; + + return config{ + .public_key = public_key.value(), + .private_key = private_key.value(), + .dbs = [&conf]() { + auto ids = std::unordered_map{}; + + if (!conf.isMember(CONF_DATABASES)) + return ids; + + for (auto it = conf[CONF_DATABASES].begin(); it != conf[CONF_DATABASES].end(); it++) + ids.emplace(it.name(), it->asString()); + + return ids; + }(), + }; + } + + /** + * Write the configuration into a JSON object + */ + void serialize(Json::Value &conf) const { + conf[CONF_PUBKEY] = crypto::b64encode(this->public_key); + conf[CONF_PRIVKEY] = crypto::b64encode(this->private_key); + conf[CONF_DATABASES] = Json::objectValue; + for (auto [dbhash, id] : this->dbs) + conf[CONF_DATABASES][dbhash] = id; + } + + /** + * Dump the configuration as a JSON object + */ + Json::Value serialize() const { + Json::Value json(Json::objectValue); + serialize(json); + return json; + } + }; + + + /** + * Simple, blocking client for interacting with KeePassXC + */ + class client { + config conf; + + data conn_pubkey = {}, + conn_privkey = {}, + remote_pubkey = {}; + string conn_id = {}, + remote_dbhash = {}; + + pid_t pid = -1; + std::unique_ptr pipe = {}; + + std::array proc_cmd = {"keepassxc-proxy", nullptr}; + + const std::unique_ptr dumper{[](){ + auto builder = Json::StreamWriterBuilder(); + builder["indentation"] = ""; + return builder.newStreamWriter(); + }()}; + const std::unique_ptr loader{Json::CharReaderBuilder().newCharReader()}; + + inline string dumps(const Json::Value &v) { + auto s = std::ostringstream(); + this->dumper->write(v, &s); + return s.str(); + } + + inline std::variant loads(const string &json) { + auto v = Json::Value(); + auto err = std::string(); + if (this->loader->parse(json.data(), json.data() + json.size(), &v, &err)) + return v; + return err; + } + + public: + client(config conf) : + conf(conf) + {} + + const config &get_config() const { + return this->conf; + } + + void set_command(const char *cmd) { + this->proc_cmd[0] = cmd; + } + + bool is_connected() { + return this->pid > 0 && this->pipe; // XXX check pipe is connected + } + + bool is_associated() { + return !this->remote_pubkey.empty() && !this->remote_dbhash.empty(); + } + + /** + * Start the KeePassXC process + * @note This generates necessary ephemeral keys and ids + * XXX Should move the key pair into associate()? + */ + bool connect() { + auto keys_opt = crypto::generate_keypair(); + if (!keys_opt) + return false; + + std::tie(this->conn_pubkey, this->conn_privkey) = keys_opt.value(); + std::tie(this->pid, this->pipe) = ko::proc::popenp(this->proc_cmd.data()); + + this->conn_id = crypto::b64encode(crypto::generate_nonce()); + + return is_connected(); + } + + Json::Value jerror(string reason) { + auto err = Json::Value{Json::objectValue}; + err["action"] = "client-error"; + err["success"] = "false"; + err["errorCode"] = -1; + err["error"] = reason; + return err; + } + + Json::Value send_message(const Json::Value &msg) { + auto &pipe = *(this->pipe); + auto msg_s = this->dumps(msg); + + pipe.write_bin(msg_s.size()); + pipe.write(msg_s); + + auto sz_opt = pipe.read_bin(); + if (!sz_opt) + return jerror(string{"Could not read result size: "} + strerror(errno)); + + auto reply = pipe.read(sz_opt.value()); + if (reply.size() < sz_opt.value()) + return jerror(string{"Could not read result: "} + strerror(errno)); + + auto result_err = this->loads(reply); + if (result_err.index()) + return jerror(string{"Could not parse message: "} + std::get(result_err)); + + //std::cerr << "Conversation: " << msg_s << " -> (" << sz_opt.value() << ") " << reply << std::endl; + + return std::get<0>(result_err); + } + + Json::Value send_message_enc(const Json::Value &msg) { + auto nonce = crypto::generate_nonce(); + auto msg_enc = crypto::encrypt(data_cast(this->dumps(msg)), nonce, this->remote_pubkey, this->conn_privkey); + + auto wrap = Json::Value(Json::objectValue); + wrap["action"] = msg["action"]; + wrap["nonce"] = crypto::b64encode(nonce); + wrap["clientID"] = this->conn_id; + wrap["message"] = crypto::b64encode(msg_enc); + + auto res = this->send_message(wrap); + + if (res.isMember("error")) + return res; + + crypto::increment(nonce); + if (res.get("nonce", "").asString() != crypto::b64encode(nonce)) + return this->jerror("Invalid response nonce"); + + auto cipher_opt = crypto::b64decode(res["message"].asString()); + if (!cipher_opt) + return this->jerror("Malformed ciphertext"); + + auto data = crypto::decrypt(cipher_opt.value(), nonce, this->remote_pubkey, this->conn_privkey); + auto result_err = this->loads(nodata_cast(data)); + if (result_err.index()) + return this->jerror(string{"Could not parse inner message: "} + std::get<1>(result_err)); + + return std::get<0>(result_err); + } + + // ---------------------------------------------------------- + // Message types + inline Json::Value msg_skeleton(const string &action) { + auto msg = Json::Value{Json::objectValue}; + msg["action"] = action; + return msg; + } + + Json::Value send_change_public_keys() { + auto msg = this->msg_skeleton("change-public-keys"); + msg["publicKey"] = crypto::b64encode(this->conn_pubkey); + msg["clientID"] = this->conn_id; + msg["nonce"] = crypto::b64encode(crypto::generate_nonce()); + return this->send_message(msg); + } + + Json::Value send_get_databasehash() { + return this->send_message_enc(this->msg_skeleton("get-databasehash")); + } + + Json::Value send_associate() { + auto msg = this->msg_skeleton("associate"); + msg["key"] = crypto::b64encode(this->conn_pubkey); + msg["idKey"] = crypto::b64encode(this->conf.public_key); + return this->send_message_enc(msg); + } + + Json::Value send_test_associate(const string &id) { + auto msg = this->msg_skeleton("test-associate"); + msg["key"] = crypto::b64encode(this->conf.public_key); + msg["id"] = id; + return this->send_message_enc(msg); + } + + Json::Value send_get_logins(const string &url, const string &submitUrl=string(), bool httpAuth=false) { + auto msg = this->msg_skeleton("get-logins"); + msg["url"] = url; + if (!submitUrl.empty()) + msg["submitUrl"] = submitUrl; + if (httpAuth) + msg["httpAuth"] = httpAuth; + msg["keys"] = Json::Value{Json::arrayValue}; + msg["keys"][0] = Json::Value{Json::objectValue}; + msg["keys"][0]["id"] = crypto::b64encode(this->conf.public_key); + msg["keys"][0]["key"] = crypto::b64encode(this->conn_pubkey); + return this->send_message_enc(msg); + } + + // ---------------------------------------------------------- + // Composite functions + /** + * Try to associate with KeePassXC using existing IDs + * @return A non-empty error message on failure + */ + string try_associate() { + // Exchange pubkeys + auto res = this->send_change_public_keys(); + if (res.isMember("error")) + return res["error"].asString(); + + if (!res.isMember("publicKey")) + return "publicKey not in change-public-keys reply"; + this->remote_pubkey = crypto::b64decode(res["publicKey"].asString()).value(); + + // Get the dbhash + res = this->send_get_databasehash(); + if (res.isMember("error")) + return res["error"].asString(); + this->remote_dbhash = res["hash"].asString(); + + // Look up database + auto f = conf.dbs.find(this->remote_dbhash); + if (f == conf.dbs.end()) + return "Not associated with database"; + + // Verify association + res = this->send_test_associate(f->second); + if (res.get("success", "false") != "true") + return "Key appears to have been revoked"; + return {}; + } + + /** + * Try to associate with KeePassXC using either existing or new IDs + * @return A non-empty error message on failure + */ + string associate() { + auto err = try_associate(); + if (err.empty()) + return {}; + + auto res = this->send_associate(); + if (res.isMember("error")) + return res["error"].asString(); + if (res.get("success", "false") != "true") + return "Unknown error"; + + this->conf.dbs.emplace(this->remote_dbhash, res["id"].asString()); + return {}; + } + }; +} diff --git a/src/keepassxc-print.cpp b/src/keepassxc-print.cpp new file mode 100644 index 0000000..83502f0 --- /dev/null +++ b/src/keepassxc-print.cpp @@ -0,0 +1,68 @@ +#include "keepassxc-browser.hpp" + +#include +#include +#include + + +namespace fs = std::filesystem; + + +template +void die(int code, Args... msg) { + (std::cerr << ... << msg) << std::endl; + exit(code); +} + +int main(int argc, char **argv) { + if (::sodium_init() < 0) + die(-44, "Error: Could not initialize libsodium"); + + if (argc < 2) + die(-1, "Usage: ", argv[0], " "); + + // Try to make the cli emulate pass at some point + auto config_path = fs::path{getenv("HOME")} / ".config/keepassxc-pass.json"; + + auto conf = [&config_path]() { + if (!fs::exists(config_path)) { + auto opt = keepassxc::config::create(); + if (!opt) + die(-6, "Error: Could not initialize secrets"); + return opt.value(); + } else { + auto s = std::ifstream(config_path); + auto v = Json::Value{}; + s >> v; + auto opt = keepassxc::config::load(v); + if (!opt) + die(-5, "Error: Could not load secrets from config"); + return opt.value(); + } + }(); + + auto client = keepassxc::client(conf); + + if (!client.connect()) + die(-2, "Error: Could not popen keepass"); + + // Hide new association behind a flag? + auto err = client.associate(); + if (!err.empty()) + die(-3, "Error: Could not associate with keepass: ", err); + + auto s = std::ofstream(config_path); + s << client.get_config().serialize(); + s.close(); + fs::permissions(config_path, fs::perms::owner_read|fs::perms::owner_write); + + auto res = client.send_get_logins(argv[1]); + if (res["success"] != "true") + die(-4, "Error: Could not get logins: ", res["error"].asString()); + + if (res["count"] == "0") + die(1, "No logins found"); + + std::cout << res["entries"][0]["password"].asString() << std::endl; + return 0; +} diff --git a/src/kofd.hpp b/src/kofd.hpp new file mode 100644 index 0000000..026cd6b --- /dev/null +++ b/src/kofd.hpp @@ -0,0 +1,692 @@ +// ============================================================================ +// kofd.hpp +// ko::fd +// (c) 2019 Taeyeon Mori +// ============================================================================ +// File descriptor functions + +#pragma once + +#include "kofs.hpp" + +#include +#include +#include +#include +#include +#include // linux/fs.h includes linux/mount.h which overrides some of the things from sys/mount.h +#include +#include + +#include +#include +#include +#include + + +// ================================================================== +namespace ko::fd { +// ------------------------------------------------------------------ +// Working with file descriptors + +/** + * Auto-close move-only filedescriptor wrapper + */ +class fd { + int _fd; + +public: + fd() : + _fd(-1) + {} + + fd(int fd) : + _fd(fd) + {} + + fd(fd const &) = delete; + + fd(fd &&o) : + _fd(o.move()) + {} + + fd &operator=(int fd) { + if (_fd >= 0) + ::close(_fd); + _fd = fd; + return *this; + } + + fd &operator=(fd &&o) { + if (_fd >= 0) + ::close(_fd); + _fd = o.move(); + return *this; + } + + ~fd() { + if (_fd >= 0) + ::close(_fd); + } + + /** + * Boolean operator + * @note This differs from a raw int fd + */ + operator bool() const { + return _fd >= 0; + } + + /** + * Negation operator + * @note This differs from a raw int fd + */ + bool operator !() const { + return _fd < 0; + } + + // Comparison + bool operator ==(int i) const { + return _fd == i; + } + + bool operator !=(int i) const{ + return _fd != i; + } + + bool operator <(int i) const { + return _fd < i; + } + + bool operator >(int i) const { + return _fd > i; + } + + bool operator <=(int i) const { + return _fd <= i; + } + + bool operator >=(int i) const { + return _fd >= i; + } + + /** + * Get the raw int fd + * @note This is not allowed on temporaries + * @note Use move() instead to transfer ownership. + * @see move() + */ + operator int() & { + return _fd; + } + + /** + * Disown this object + * @note + */ + int move() { + auto tmp = _fd; + _fd = -1; + return tmp; + } + + /** + * Close the file descriptor early + */ + bool close() { + if (_fd < 0) return false; + if (::close(_fd) && errno != EBADF) return false; + _fd = -1; + return true; + } + + /** + * Copy the file descriptor + */ + fd dup() { + return ::dup(_fd); + } +}; + + +//------------------------------------------------------------------- +// Opening file descriptors +// @{ +/** + * Open a file descriptor + * @param path The path + * @param flags The open(2) flags + * @param dirfd The directory fd \p path may be relative to + * @param cloexec Add O_CLOEXEC to \p flags + * @return A \c fd file descriptor + */ +fd open(const fs::cpath &path, long flags, int dirfd=AT_FDCWD, bool cloexec=true) { + return ::openat(dirfd, path, flags | (cloexec ? O_CLOEXEC : 0)); +} + +/** + * Open a file descriptor, creating the file if it doesn't exist + * @param path The path + * @param flags The open(2) flags + * @param mode The file mode to create with + * @param dirfd The directory fd \p path may be relative to + * @param cloexec Add O_CLOEXEC to \p flags + * @return A \c fd file descriptor + */ +fd open_creat(const fs::cpath &path, long flags, mode_t mode, int dirfd=AT_FDCWD, bool cloexec=true) { + return ::openat(dirfd, path, O_CREAT | flags | (cloexec ? O_CLOEXEC : 0), mode); +} + +/** + * Open a directory file descriptor + * @param path The directory path + * @param dirfd The directory fd \p path may be relative to + * @return A \c fd directory file descriptor + */ +fd opendir(const fs::cpath &path, int dirfd=AT_FDCWD) { + return ::openat(dirfd, path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); +} + +/** + * Open a directory file descriptor with custom flags + * @param path The directory path + * @param flags The flags to pass to open(2) + * @param dirfd The directory fd \p path may be relative to + * @return A directory \c fd + */ +fd opendir2(const fs::cpath &path, long flags, int dirfd=AT_FDCWD) { + return ::openat(dirfd, path, flags|O_DIRECTORY); +} +// @} + + +//------------------------------------------------------------------- +// Checking properties +// @{ +/** + * Check if a path exists + * @param path The path + * @param dirfd The directory fd \p path may be relative to + * @return true if path exists + */ +bool exists(const fs::cpath &path, int dirfd=AT_FDCWD) { + return !::faccessat(dirfd, path, F_OK, 0); +} + +/** + * Check if a path is a directory + * @param path The path + * @param dirfd The directory fd \p path may be relative to + * @return true if path is a directory + */ +bool is_dir(const fs::cpath &path, int dirfd=AT_FDCWD) { + struct stat st; + if (::fstatat(dirfd, path, &st, 0)) + return false; + return S_ISDIR(st.st_mode); +} + +/** + * Read the target of a symbolic link + * @param path The symlink path + * @param dirfd The directory fd \p path may be relative to + * @return A fs::path. It is empty on error + */ +fs::path readlink(const fs::cpath &path, int dirfd=AT_FDCWD) { + constexpr auto static_bufsize = 4096; + char buf[static_bufsize]; + auto sz = ::readlinkat(dirfd, path, buf, static_bufsize); + if (sz < 0) + return {}; + if (sz < static_bufsize) + return {buf, buf + sz}; + + struct stat st; + if (::fstatat(dirfd, path, &st, AT_SYMLINK_NOFOLLOW)) + return {}; + + auto extbuf = std::make_unique(st.st_size); + sz = ::readlinkat(dirfd, path, extbuf.get(), sz); + if (sz < 0) + return {}; + return {&extbuf[0], &extbuf[sz]}; +} + +/** + * Get the target if a file is a symbolic link or return the path as-is if it is something else + * @param path The path + * @param dirfd The directory fd \p path may be relative to + * @param notexist_ok Whether or not to return the path as-is if it doesn't exist (default=true) + * @return A fs::path, possibly relative to dirfd. It may be empty on error + */ +fs::path readlink_or_path(const fs::path &path, int dirfd=AT_FDCWD, bool notexist_ok=true) { + auto target = readlink(path, dirfd); + if (target.empty()) { + if (errno == EINVAL || (errno == ENOENT && notexist_ok)) + return path; + else + return {}; + } + // Make (relative) returned value relative to dirfd + if (target.is_relative()) + return path.parent_path() / target; + return target; +} + +/** + * Check if a directory is empty + */ +bool is_dir_empty(const fs::cpath &path, int dirfd=AT_FDCWD) { + auto fd = opendir(path, dirfd); + if (!fd) + return false; + auto dir = fs::dir_ptr(fd); + if (!dir) + return false; + errno = 0; + while (true) { + auto res = dir.readdir(); + if (res == nullptr) + return errno == 0; + if (strcmp(".", res->d_name) && strcmp("..", res->d_name)) + return false; + } +} +// @} + + +//------------------------------------------------------------------- +// Creating files and directories +// @{ +/** + * Create a symbolic link + * @param target The link target. + * @param path The path of the new symlink + * @param dirfd The directory fd \p path may be relative to + * @return 0 on success + * @note target is relative to the directory containing the link, NOT dirfd + */ +int symlink(const fs::cpath &target, const fs::cpath &path, int dirfd=AT_FDCWD) { + return ::symlinkat(target, dirfd, path); +} + +/** + * Create a directory + * @param path The new directory path + * @param mode The permissions to assign + * @param dirfd The directory fd \p path may be relative to + * @return 0 on success + */ +int mkdir(const fs::cpath &path, mode_t mode=0755, int dirfd=AT_FDCWD) { + return ::mkdirat(dirfd, path, mode); +} + +/** + * Create all parent directories + * @param path The path of the innermost directory to create + * @param mode The permissions to assign + * @param dirfd The directory fd \p path may be relative to + * @return The number of directories created, or -1 on error + */ +int makedirs(const fs::path &path, mode_t mode=0755, int dirfd=AT_FDCWD) { + struct stat st; + // Treat empty path as . + // Check if exists + if (!fstatat(dirfd, path.empty() ? "." : path.c_str(), &st, 0)) { + // If directory, we're fine. + if (S_ISDIR(st.st_mode)) + return 0; + // Else, this is an error + errno = ENOTDIR; + return -1; + } + // Propagate any error other than ENOENT + if (errno != ENOENT || path.empty()) + return -1; + // Ensure parents + auto parents = makedirs(path.parent_path(), mode, dirfd); + // Actually create directory + if (mkdir(path, mode, dirfd)) + return -1; + return parents + 1; +} + +/** + * Create a file if it doesn't exist + * @param path The path of the file + * @param mode The permissions to assign if it has to be created + * @param dirfd The directory fd \p path may be relative to + * @return 0 on success + */ +int touch(const fs::cpath &path, mode_t mode=0755, int dirfd=AT_FDCWD) { + auto fd = open_creat(path, O_WRONLY, mode, dirfd); + return fd ? 0 : -1; +} + +/** + * Remove a file + * @param path The path of the file to remove + * @param dirfd The directory fd \p may be relative to + * @return 0 on success + */ +int unlink(const fs::cpath &path, int dirfd=AT_FDCWD) { + return ::unlinkat(dirfd, path, 0); +} + +/** + * Remove a directory + * @param path The path of the directory to remove + * @param dirfd The directory fd \p may be relative to + * @return 0 on success + */ +int rmdir(const fs::cpath &path, int dirfd=AT_FDCWD) { + return ::unlinkat(dirfd, path, AT_REMOVEDIR); +} + +/** + * Copy a symbolic link + * @param from The source symbolic link path + * @param to The target symbolic link path (must not exist) + * @param from_dirfd The directory fd \p from may be relative to + * @param dirfd The directory fd \p to may be relative to + * @return 0 on success + */ +int copy_symlink(const fs::cpath &from, fs::cpath to, + int from_dirfd=AT_FDCWD, int dirfd=AT_FDCWD) { + auto target = readlink(from, from_dirfd); + return ::symlinkat(target.c_str(), dirfd, to); +} +// @} + + +//------------------------------------------------------------------- +// File descriptor I/O +// @{ +// Read +/** + * Read until \p size bytes have been read or an error has been encoutnered + * @param fd A file descriptor + * @param dest The destination buffer + * @param size The desired number of bytes read + * @return The actual number of bytes read + * @note If returned value != \p size, errno will be set. errno == 0 indicates EOF + */ +size_t read(int fd, char *dest, size_t size) { + size_t have = 0; + + while (have < size) { + auto got = ::read(fd, dest + have, size - have); + + if (got == 0) { + errno = 0; + break; + } else if (got < 0) + break; + + have += got; + } + + return have; +} + +/** + * Read until \p size bytes have been read or an error has been encoutnered + * @param fd A file descriptor + * @param size The desired number of bytes read + * @return The resulting string + * @note If returned string.size() != \p size, errno will be set. errno == 0 indicates EOF + */ +std::string read(int fd, size_t size) { + auto buf = std::string(size, 0); + buf.resize(read(fd, buf.data(), size)); + return buf; +} + +/** + * Read until \p size bytes have been read, an error has been encoutnered, or the timeout is hit + * @param fd A file descriptor + * @param dest The destination buffer + * @param size The desired number of bytes read + * @param timeout The timeout that must not be exceeded between chunk reads + * @return The actual number of bytes read + * @note If returned value != \p size, errno will be set. errno == 0 indicates EOF. + * Timeout is indicated by ETIMEDOUT. + */ +size_t read(int fd, char *dest, size_t size, timeval timeout) { + size_t have = 0; + + auto fds = fd_set(); + FD_ZERO(&fds); + FD_SET(fd, &fds); + + while (have < size) { + auto rv = select(fd + 1, &fds, nullptr, nullptr, &timeout); + + if (rv == 0) { + errno = ETIMEDOUT; + break; + } else if (rv < 0) { + break; + } + + auto got = ::read(fd, dest + have, size - have); + if (got == 0) { + errno = 0; + break; + } else if (got < 0) + break; + + have += got; + } + + return have; +} + +/** + * Read until \p size bytes have been read, an error has been encoutnered, or the timeout is hit + * @param fd A file descriptor + * @param size The desired number of bytes read + * @param timeout The timeout that must not be exceeded between chunk reads + * @return The resulting string + * @note If returned value != \p size, errno will be set. errno == 0 indicates EOF + * Timeout is indicated by ETIMEDOUT. + */ +std::string read(int fd, size_t size, timeval timeout) { + auto buf = std::string(size, 0); + buf.resize(read(fd, buf.data(), size, timeout)); + return buf; +} + +/** + * Read a POD type from a file descriptor + * @tparam T The type + * @param fd The file descriptor + * @return The object on success, std::nullopt on failure + * @note If std::nullopt is returned, errno will be set. + */ +template +std::optional read_bin(int fd) { + char buf[sizeof(T)]; + if (read(fd, buf, sizeof(T)) == sizeof(T)) + return *reinterpret_cast(buf); + else + return std::nullopt; +} + +// Write +/** + * Write all bytes to a file descriptor unless an error occurs (blocking) + * @param fd The file descriptor + * @param buf The source buffer + * @param size The number of bytes to write + * @return The number of bytes written + * @note If returned value != \p size, errno will be set. + */ +size_t write(int fd, const char *buf, size_t size) { + size_t have = 0; + + while (have < size) { + auto got = ::write(fd, buf + have, size - have); + + if (got == 0) { + errno = 0; + break; + } else if (got < 0) + break; + + have += got; + } + + return have; +} + +/** + * Write all bytes to a file descriptor unless an error occurs (blocking) + * @param fd The file descriptor + * @param s A string to write + * @return The number of bytes written + * @note If returned value != \p s.size(), errno will be set. + */ +size_t write(int fd, const std::string &s) { + return write(fd, s.data(), s.size()); +} + +/** + * Write a POD object to a file descriptor + * @tparam T The POD type + * @param fd The file descriptor + * @param v The object + * @return The number of bytes written + * @note If returned value != sizeof(T), errno will be set. + */ +template +size_t write_bin(int fd, const T &v) { + return write(fd, reinterpret_cast(&v), sizeof(v)); +} + +// Shortcuts +/** + * Read a file from disk + * @param path The file path + * @param dirfd The directory fd \p path may be relative to + * @param max The maximum number of bytes to read + * @return A pair of (data read, errno) + * @note If data.size() == max, more data may be available. + */ +std::pair cat(const fs::cpath &path, int dirfd=AT_FDCWD, size_t max=1024) { + auto fd = open(path, O_RDONLY, dirfd); + if (!fd) + return {}; + auto r = read(fd, max); + if (r.size() < max) + return {r, errno}; + return {r, 0}; +} + +/** + * Write a file to disk + * @param s The data to write + * @param path The path to write to + * @param mode The mode to create the file with, if neccessary + * @param dirfd The directory fd \p path may be relative to + */ +bool dump(const std::string &s, const fs::cpath &path, mode_t mode, int dirfd=AT_FDCWD) { + auto fd = open_creat(path, O_WRONLY, mode, dirfd); + if (!fd) + return -1; + return write(fd, s) == s.size(); +} +// @} + +//------------------------------------------------------------------- +// Copying Files +// @{ +/** + * Naively copy data between file descriptors + * @param fs The source file descriptor + * @param fd The destination file descriptor + * @param len The number of bytes to copy + */ +bool fcopy_raw(int fs, int fd, size_t len) { + constexpr size_t bufsz = 8192; + char buf[bufsz]; + do { + auto target = std::min(len, bufsz); + auto nread = read(fs, buf, target); + if (nread < target && errno != 0) + return false; + auto written = write(fd, buf, nread); + if (written < nread) + return false; + if (nread < target) + return true; + len -= nread; + } while (len > 0); + return true; +} + +/** + * Copy data between file descriptors + * @param fs The source file descriptor + * @param fd The destination file descriptor + * @param len The number of bytes to copy + * @return false on failure with errno set + * @note This attempts to use copy_file_range(2) and sendfile(2) + * before falling back to fcopy_raw + */ +bool fcopy(int fs, int fd, size_t len) { + while (len > 0) { + auto r = ::copy_file_range(fs, NULL, fd, NULL, len, 0); + if (r < 0) { + if (errno == ENOSYS || errno == EXDEV || errno == EINVAL) + break; + return fcopy_raw(fs, fd, len); + } + len -= r; + } + + while (len > 0) { + auto r = ::sendfile(fd, fs, NULL, len); + if (r < 0) + return fcopy_raw(fs, fd, len); + len -= r; + } + + return true; +} + +/** + * Copy a file + * @param src The path to copy from + * @param dst The path to copy to + * @param src_dir The directory fd \p src may be relative to + * @param dst_dir The directory fd \p dst may be relative to + * @return false on failure with errno set + * @note This variant will only try to preserve the file mode, no other attributes + * @note Note that this function takes two separate directory fds + * @note This will use reflink/FICLONE if supported. + */ +bool copy0(const fs::cpath &src, const fs::cpath &dst, int src_dir=AT_FDCWD, int dst_dir=AT_FDCWD) { + struct stat st; + if (::fstatat(src_dir, src, &st, 0)) + return false; + + auto fs = open(src, O_RDONLY, src_dir); + if (!fs) + return false; + auto fd = open_creat(dst, O_WRONLY, st.st_mode, dst_dir); + if (!fd) + return false; + + // Try reflink +#ifdef FICLONE + int ret = ::ioctl(fd, FICLONE, (int)fs); + if (ret != -1) + return ret == st.st_size; +#endif + + return fcopy(fs, fd, st.st_size); +} + +// @} +} // namespace ko::fd diff --git a/src/kofd_pipe.hpp b/src/kofd_pipe.hpp new file mode 100644 index 0000000..8009471 --- /dev/null +++ b/src/kofd_pipe.hpp @@ -0,0 +1,78 @@ +// ============================================================================ +// kofd_pipe.hpp +// ko::fd::pipe +// (c) 2019 Taeyeon Mori +// ============================================================================ +// bi-directional pipe implementation + +#pragma once + +#include "kofd.hpp" + +#include + + +namespace ko::fd { +// ------------------------------------------------------------------ +/** + * Represents a bi-directional pair of file descriptors + */ +class pipe { + int rfd, wfd; + +public: + pipe(fd &&fd) : + rfd(fd.move()), wfd(rfd) + {} + + pipe(fd &&rfd, fd &&wfd) : + rfd(rfd.move()), wfd(wfd.move()) + {} + + explicit pipe(int rfd, int wfd) : + rfd(rfd), wfd(wfd) + {} + + ~pipe() { + ::close(this->rfd); + if (this->wfd != this->rfd) + ::close(this->wfd); + } + + // IO Functions, see namespace fd + inline size_t read(char *dest, size_t size) { + return ::ko::fd::read(this->rfd, dest, size); + } + + inline std::string read(size_t size) { + return ::ko::fd::read(this->rfd, size); + } + + inline size_t read(char *dest, size_t size, timeval timeout) { + return ::ko::fd::read(this->rfd, dest, size, timeout); + } + + inline std::string read(size_t size, timeval timeout) { + return ::ko::fd::read(this->rfd, size, timeout); + } + + inline size_t write(const char *buf, size_t size) { + return ::ko::fd::write(this->wfd, buf, size); + } + + inline size_t write(const std::string &s) { + return ::ko::fd::write(this->wfd, s); + } + + template + inline size_t write_bin(const T &v) { + return ::ko::fd::write_bin(this->wfd, v); + } + + template + inline std::optional read_bin() { + return ::ko::fd::read_bin(this->rfd); + } +}; + +} // namespace ko::fd diff --git a/src/kofs.hpp b/src/kofs.hpp new file mode 100644 index 0000000..34a2ca7 --- /dev/null +++ b/src/kofs.hpp @@ -0,0 +1,110 @@ +// ============================================================================ +// kofs.hpp +// ko::fs +// (c) 2019 Taeyeon Mori +// ============================================================================ +// Misc. Filesystem functions + +#pragma once + +#include +#include + +#include +#include + +namespace ko::fs { +using namespace std::filesystem; + +/** + * Helper struct for functions that require a c-string path + */ +struct cpath { + const char *path; + + inline cpath(const char *path) : path(path) {} + inline cpath(const fs::path &path) : path(path.c_str()) {} + inline cpath(const std::string &path) : path(path.c_str()) {} + + inline operator const char *() const { + return path; + } +}; + +class dir_ptr { + DIR *ptr; + +public: + dir_ptr(int fd) { + ptr = ::fdopendir(fd); + } + + dir_ptr(const cpath &path) { + ptr = ::opendir(path); + } + + bool operator !() { + return ptr == nullptr; + } + + ~dir_ptr() { + ::closedir(ptr); + } + + dirent const *readdir() { + return ::readdir(ptr); + } + + // Iterator + class iterator { + dir_ptr &dir; + dirent const *here = nullptr; + bool done = false; + int error = 0; + + friend class dir_ptr; + + iterator(dir_ptr &dir, bool done) : dir(dir), done(done) { + ++(*this); + } + + public: + iterator &operator ++() { + if (!done) { + auto errno_bak = errno; + here = dir.readdir(); + if (here == nullptr) { + done = true; + if (errno != errno_bak) + error = errno; + } + } + return *this; + } + + dirent const &operator *() { + return *here; + } + + operator bool() { + return !done; + } + + bool operator ==(iterator const &other) const { + return dir.ptr == other.dir.ptr && (here == other.here || done == other.done); + } + + int get_errno() { + return error; + } + }; + + iterator begin() { + return iterator(*this, false); + } + + iterator end() { + return iterator(*this, true); + } +}; +} // namespace ko::fs diff --git a/src/kons.hpp b/src/kons.hpp new file mode 100644 index 0000000..71bbe18 --- /dev/null +++ b/src/kons.hpp @@ -0,0 +1,241 @@ +/* + * Small Linux Namespace utility header + * (c) 2019 Taeyeon Mori + */ + +#pragma once + +#include "koutil.hpp" +#include "kofd.hpp" +#include "koos.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + + +namespace ko::ns { + +/** + * idmap namespace + * Contains functions for setting /proc/pid/[ug]id_map + */ +namespace idmap { + /// An entry in [ug]id_map + struct entry { + uid_t start; + uid_t host_start; + unsigned long count; + }; + + /// Write the idmap to + template + inline bool set(fs::path path, Container map) { + auto stream = std::ofstream(path); + for (entry &e : map) + stream << e.start << ' ' << e.host_start << ' ' << e.count << '\n'; + stream.close(); + return stream.good(); + } + + /// Disable setgroups() syscall for process + /// This is required for unprivileged user namespaces + bool disable_setgroups(pid_t pid) { + auto stream = std::ofstream(util::str("/proc/", pid, "/setgroups")); + stream << "deny"; + stream.close(); + return stream.good(); + } + + /// Return an idmap mapping a single ID + inline constexpr std::array single(uid_t id, uid_t host_id) { + return {{ {id, host_id, 1} }}; + } + + /// Get the path to the map for + /// may be "uid" or "pid" + inline fs::path path(pid_t pid, const char *map_type) { + return util::str("/proc/", pid, "/", map_type, "_map"); + } +} + + +/** + * namespace kons::mount + * Stuff related to setting up the mount namespace + */ +namespace mount { + using os::mount; + using os::bind; + + // Mount all the basic filesystems + util::cvresult mount_core(const fs::path &root) { + return util::cvshort() + .ifthen("mount_root", !os::is_mountpoint(root), bind, root, root, 0) + .ifthen("mount_proc", fs::exists(root / "proc"), mount, "proc", root / "proc", "proc", 0, nullptr) + .ifthen("mount_sys", fs::exists(root / "sys"), bind, "/sys", root / "sys", MS_REC) + .ifthen("mount_dev", fs::exists(root / "dev"), bind, "/dev", root / "dev", MS_REC) + .ifthen("mount_tmp", fs::exists(root / "tmp"), mount, "tmp", root / "tmp", "tmpfs", 0, nullptr) + .ifthen("mount_run", fs::exists(root / "run"), mount, "run", root / "run", "tmpfs", 0, nullptr); + } + + /// Write-protect path to mitigate broken filesystem permissions in single-user ns + util::cvresult protect_path(const fs::path &path) { + return util::cvshort() + .then("bind_protect", bind, path, path, MS_REC) + .then("bind_protect_ro", bind, path, path, MS_REC|MS_REMOUNT|MS_RDONLY); + } + + /// Bind in additional locations required by GUI programs + /// Some of these are serious isolation breaches! + /// Note that home and rundir must be relative and will be interpreted against both '/' and $root + util::cvresult mount_gui(const fs::path &root, const fs::path &home, const fs::path &rundir) { + auto path_from_env = [](const char *name, fs::path dflt, const char *prefix=nullptr) -> fs::path { + auto var = getenv(name); + if (var != nullptr) { + if (prefix != nullptr) { + auto plen = strlen(prefix); + if (!strncmp(var, prefix, plen)) + var += plen; + } + if (var[0] == '/') + return var+1; + } + return dflt; + }; + auto path_from_env_rel = [](fs::path to, const char *name, const char *dflt) -> fs::path { + auto var = getenv(name); + if (var != nullptr) + return to / var; + return to / dflt; + }; + // Bind-mount various paths required to get GUI apps to communicate with system services + // X11, DBus (both buses, Steam does use the system bus), PulseAudio + auto frags = std::array{ + "tmp/.X11-unix", + "run/dbus", + "run/udev", // Udev database for correct ENV entries e.g. ID_INPUT_JOYSTICK markers + //"etc/machine-id", // Pulseaudio will only connect with same machine-id by default. See below + path_from_env("XAUTHORITY", home / ".Xauthority"), + home / ".config/pulse/cookie", + path_from_env("DBUS_SESSION_BUS_ADDRESS", rundir / "bus", "unix:path="), + rundir / "pulse", + rundir / "pipewire-0", + path_from_env_rel(rundir, "WAYLAND_DISPLAY", "wayland-0"), + }; + + // /tmp/.X11-unix must be owned by user or root for wlroots xwayland to work (e.g. gamescope) + // behaviour can be overridden by env var KONS_BIND_X11=all + if (![&frags, root]() { + auto x11_mount_mode = getenv("KONS_BIND_X11"); + if (x11_mount_mode && !strcasecmp("all", x11_mount_mode)) + return true; + auto display = getenv("DISPLAY"); + if (!display) + return false; + if (display[0] == ':') + display += 1; + for (char *c = display; *c; c++) + if (!isdigit(*c)) + return false; + auto dirname = root / frags[0]; + fs::create_directories(dirname); + ::chmod(dirname.c_str(), 01777); + auto sockname = frags[0] / util::str("X", display); + fd::touch(root / sockname); + frags[0] = sockname; + return true; + }()) { + std::cerr << "Warn: Invalid $DISPLAY value; falling back to bind-mounting /tmp/.X11-unix whole" << std::endl; + } + + // Pulseaudio will by default only connect to the server published in the X11 root window properties if the machine-ids match. + // Either we bind-mount /etc/machine-id or we need to set PULSE_SERVER in the environment. Both are suboptimal hacks: + // /etc/machine-id shoudn't be the same across two rootfs' but it might be acceptable since we're not running init. + // OTOH, setting PULSE_SERVER can break with nonstandard configurations if they're not manually set in ENV. X11 publish is not enough. + auto pulse = util::str("unix:/", rundir.c_str(), "/pulse/native"); + setenv("PULSE_SERVER", pulse.c_str(), 0); // Don't overwrite, assume that there's a reason it's set. May be TCP. + // If custom unix socket path, it could fail either way as it may not be included above. + // NOTE that exec[vlp]e() must be used to make setenv() work. + + auto sh = util::cvshort(); + auto host_root = fs::path("/"); + for (auto frag : frags) { + auto hpath = host_root / frag; + + if (fs::exists(hpath)) { + auto path = root / frag; + + if (!fs::exists(path)) { + if (fs::is_directory(hpath)) + fs::create_directories(path); + else { + fs::create_directories(path.parent_path()); + auto touch = std::ofstream(path); + } + } + + if (!(sh = sh.then("mount_gui", bind, hpath, path, 0))) + break; + } + } + + return sh; + } + + /// Pivot the root to $new_root, optionally keeping the old one at $old_root. + /// Note that the $old_root directory is required in the process either way. + util::cvresult pivot_root(const fs::path &new_root, const fs::path &old_root, bool keep_old=true) { + auto path = new_root / old_root; + if (!fs::exists(path)) + fs::create_directories(path); + + return util::cvshort() + .then("pivot_root", syscall, SYS_pivot_root, new_root.c_str(), path.c_str()) + .then("chdir_root", chdir, "/") + .ifthen("umount_oldroot", !keep_old, umount2, old_root.c_str(), MNT_DETACH); + } +} // namespace mount + + +/** + * Unshare (at least) new single-user namespace + * @param uid The uid inside the userns + * @param gid The gid inside the userns + * @param flags The unshare(2)/clone(2) flags (CLONE_NEWUSER implied) + * @return Zero on success, -1 + errno on failure + */ +inline int unshare_single(uid_t uid, uid_t gid, long flags) { + auto euid = geteuid(); + auto egid = getegid(); + auto r = ::unshare(flags | CLONE_NEWUSER); + if (r != 0) + return r; + if (!idmap::set("/proc/self/uid_map", idmap::single(uid, euid))) + return -1; + if (!idmap::disable_setgroups(getpid())) + return -1; + if (!idmap::set("/proc/self/gid_map", idmap::single(gid, egid))) + return -1; + return 0; +} + + +inline int setns(const fs::cpath &path, int type, int dirfd=AT_FDCWD) { + auto fd = ::openat(dirfd, path, O_RDONLY); + if (fd < 0) + return errno; + auto res = ::setns(fd, type); + ::close(fd); + return res; +} + +} // namespace ko::ns + diff --git a/src/kons_clone.hpp b/src/kons_clone.hpp new file mode 100644 index 0000000..dfc4e11 --- /dev/null +++ b/src/kons_clone.hpp @@ -0,0 +1,81 @@ +// ============================================================================ +// kons_clone.hpp +// ko::ns::clone namespace +// (c) 2019 Taeyeon Mori +// ============================================================================ +// Small Linux namespace utility header +// Clone-related functions + +#pragma once + +#include "kons.hpp" +#include "koproc.hpp" + +/** + * clone namespace + * Useful wrappers around the clone(2) syscall + */ +namespace ko::ns::clone { +namespace detail { + template + int uvclone_entry(void *arg) { + auto [f, args, sync] = *reinterpret_cast(arg); + sync.yield(); + return std::apply(f, args); + } +} + +/** + * Spawn a process in a new user namespace + * @param uidmap The uid map for the user namespace + * @param gidmap The gid map for the user namespace + * @param fn The function to call in the child process + * @param stacksize The size of the process stack + * @param flags The clone(2) flags (SIGCHLD|CLONE_VM|CLONE_NEWUSER implied) + * @param args The function arguments + */ +template +std::pair uvclone(U uidmap, G gidmap, F fn, size_t stacksize, long flags, Args... args) { + auto [sync, sync_c] = proc::sync::make_semapair(false); + auto data = new std::tuple{fn, std::tuple{std::forward(args)...}, sync_c}; + + auto proc = proc::detail::do_clone(detail::uvclone_entry, stacksize, CLONE_NEWUSER | CLONE_VM | flags, data); + auto res = EINVAL; + + if (proc) { + // Wait for child + sync.wait(); + + // Set maps + auto pid = proc.pid(); + if (idmap::set(idmap::path(pid, "uid"), uidmap)) { + if (idmap::disable_setgroups(pid)) { + if (idmap::set(idmap::path(pid, "gid"), gidmap)) { + res = 0; + } + } + } + + if (res) + res = errno; + + sync.post(); + } + + return {std::move(proc), res}; +} + +/** + * Spawn a process in a new single-user user namespace + * @param uid The uid inside the user namespace + * @param gid The gid inside the user namespace + * @param fn The function to call in the child process + * @param stacksize The size of the process stack + * @param flags The clone(2) flags (SIGCHLD|CLONE_VM|CLONE_NEWUSER implied) + * @param args The function arguments + */ +template +inline std::pair uvclone_single(uid_t uid, gid_t gid, F fn, size_t stacksize, long flags, Args... args) { + return uvclone(idmap::single(uid, getuid()), idmap::single(gid, getgid()), fn, stacksize, flags, args...); +} +} // namespace ko::ns::clone diff --git a/src/koos.hpp b/src/koos.hpp new file mode 100644 index 0000000..3356713 --- /dev/null +++ b/src/koos.hpp @@ -0,0 +1,68 @@ +// ============================================================================ +// koos.hpp +// ko::os +// (c) 2019 Taeyeon Mori +// ============================================================================ +// Misc. OS interfaces + +#pragma once + +#include "kofs.hpp" + +#include +#include +#include + + +namespace ko::os { +/// Try to get the current user home directory +fs::path get_home() { + const char *home_env = getenv("HOME"); + + if (home_env) + return fs::path(home_env); + + auto pwd = getpwuid(getuid()); + + if (pwd) + return fs::path(pwd->pw_dir); + + return fs::path("/"); +} + +// ------------------------------------------------------------------ +// Mounting filesystems +inline int mount(const fs::cpath &src, const fs::cpath &dst, const char *type, long flags=0, void *args=nullptr) { + auto res = ::mount(src, dst, type, flags, args); + if (res) + return errno; + return 0; +} + +inline int bind(const fs::cpath &src, const fs::cpath &dst, long flags=0) { + return mount(src, dst, nullptr, MS_BIND | flags, nullptr); +} + +/// Check if a path is a mount point +bool is_mountpoint(const fs::cpath &path) { + auto fp = setmntent("/proc/self/mounts", "r"); + if (!fp) { + perror("mntent"); + return false; + } + + bool found = false; + + while (auto ent = getmntent(fp)) { + if (!strcmp(path, ent->mnt_dir)) { + found = true; + break; + } + } + + endmntent(fp); + + return found; +} + +} // namespace ko::os diff --git a/src/koproc.hpp b/src/koproc.hpp new file mode 100644 index 0000000..593bcf1 --- /dev/null +++ b/src/koproc.hpp @@ -0,0 +1,418 @@ +// ============================================================================ +// ko::util koutil.hpp +// (c) 2019 Taeyeon Mori +// ============================================================================ +// Managing child processes + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "kofd.hpp" +#include "kofd_pipe.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace ko::proc { +// ------------------------------------------------------------------ +// Simple popen implementaion +using popen_result_t = std::pair>; + +/** + * Spawn a process and connect it's stdin and stdout to a pipe + * @param exec_fn An exec-style function to call in the new process. + * @param args The arguments to exec_fn + * @return The PID and a pipe object + * @warning As this uses vfork(), exec_fn must actually call some kind of exec + * before the parent process can resume. + */ +template +inline popen_result_t popen_impl(F exec_fn, Args... exec_args) { + // Open 2 pipes + auto pipefd = std::array, 2>{}; + + if (::pipe2(pipefd[0].data(), 0)) + return {-1, nullptr}; + if (::pipe2(pipefd[1].data(), 0)) { + ::close(pipefd[0][0]); + ::close(pipefd[0][1]); + return {-1, nullptr}; + } + + // Fork + auto pid = vfork(); + if (pid == 0) { + // Close parent ends + ::close(pipefd[0][1]); + ::close(pipefd[1][0]); + + // Set up stdin and stdout + if (::dup2(pipefd[0][0], 0) != 0 || ::dup2(pipefd[1][1], 1) != 1) + _exit(-1); + + // Close superfluous child ends + ::close(pipefd[0][0]); + ::close(pipefd[1][1]); + + // exec + _exit(exec_fn(exec_args...)); + } + + // Close child ends + ::close(pipefd[0][0]); + ::close(pipefd[1][1]); + + // Abort if fork failed + if (pid < 0) { + ::close(pipefd[0][1]); + ::close(pipefd[1][0]); + return {pid, nullptr}; + } + + // return stuff + return {pid, std::make_unique(pipefd[1][0], pipefd[0][1])}; +} + +/** + * Spawn a process and connect it's stdin and stdout to a pipe + * @param argv The process argv + * @return The PID and a pipe object + * @note argv[0] is the process image path + */ +popen_result_t popen(const char **argv) { + return popen_impl(::execv, const_cast(argv[0]), const_cast(argv)); +} + +/** + * Spawn a process and connect it's stdin and stdout to a pipe + * @param argv The process argv + * @return The PID and a pipe object + * @note argv[0] is the process image name or path + */ +popen_result_t popenp(const char **argv) { + return popen_impl(::execvp, const_cast(argv[0]), const_cast(argv)); +} + + +// ------------------------------------------------------------------ +/// Process Synchronization +namespace sync { +namespace detail { + class semaphore_pair { + std::atomic_int refs; + bool shared; + sem_t sems[2]; + + semaphore_pair(bool shared) : + refs(0), shared(shared) + { + int bshared = shared ? 1 : 0; + sem_init(&sems[0], bshared, 0); + sem_init(&sems[1], bshared, 0); + } + + public: + ~semaphore_pair() { + sem_destroy(&sems[0]); + sem_destroy(&sems[1]); + } + + static semaphore_pair *create(bool shared = false) { + if (shared) { + void *mem = mmap(nullptr, sizeof(semaphore_pair), PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); + if (mem == MAP_FAILED) + return nullptr; + return new (mem) semaphore_pair(true); + } else { + return new semaphore_pair(false); + } + } + + semaphore_pair *retain() { + refs++; + return this; + } + + void release() { + auto v = refs.fetch_sub(1); + if (v == 1) { + if (shared) { + this->~semaphore_pair(); + munmap(this, sizeof(semaphore_pair)); + } else { + delete this; + } + } + } + + sem_t *sem(int n) { + return &sems[n%2]; + } + }; +} + +/** + * Contains a set of semaphores for bidirectional synchronization + */ +class semapair { + detail::semaphore_pair *sems; + int offset; + + semapair(detail::semaphore_pair *sems, int offset) : + sems(sems->retain()), + offset(offset) + {} + + friend std::array make_semapair(bool); + +public: + semapair(const semapair &o) : + sems(o.sems->retain()), + offset(o.offset) + {} + + semapair(semapair &&o) : + sems(o.sems), + offset(o.offset) + { + o.sems = nullptr; + } + + ~semapair() { + if (sems) + sems->release(); + } + + inline void wait() { + sem_wait(sems->sem(offset)); + } + + inline void post() { + sem_post(sems->sem(offset+1)); + } + + inline void yield() { + post(); + wait(); + } +}; + +inline std::array make_semapair(bool shared) { + auto stuff = detail::semaphore_pair::create(shared); + return {{ {stuff, 0}, {stuff, 1} }}; +} +} // namespace sync (ko::proc::sync) + + +// ------------------------------------------------------------------ +// Clone wrappers +using void_callback_t = std::pair; + +/** + * Represents a cloned child process with potential cleanup + */ +class child_ref { + pid_t _pid = -1; + + std::optional cleanup = {}; + + bool _done = false; + int _status = -1; + + inline void _check_clean() { + if (cleanup) + std::cerr << "Warning: ko::proc::child_ref with cleanup destroyed without waiting" << std::endl; + } + +public: + child_ref(pid_t pid) : + _pid(pid) + {} + + child_ref(pid_t pid, void_callback_t cleanup_cb) : + _pid(pid), cleanup(cleanup_cb) + {} + + child_ref(child_ref &&o) : + _pid(o._pid), cleanup(o.cleanup), + _done(o._done), _status(o._status) + { + o._pid = -1; + o.cleanup = {}; + } + + child_ref &operator =(child_ref &&o) { + _check_clean(); + _pid = o._pid; + cleanup = std::move(o.cleanup); + _done = o._done; + _status = o._status; + o._pid = -1; + o.cleanup = {}; + return *this; + } + + + ~child_ref() { + _check_clean(); + } + + operator bool() { + return _pid > 0; + } + + int wait() { + if (!_done) { + waitpid(_pid, &_status, 0); + if (cleanup) { + auto [f, arg] = cleanup.value(); + f(arg); + cleanup = {}; + } + _done = true; + } + return WEXITSTATUS(_status); + } + + std::pair poll() { + if (!_done) { + if(waitpid(_pid, &_status, WNOHANG) == 0) + return {false, 0}; + if (cleanup) { + auto [f, arg] = cleanup.value(); + f(arg); + cleanup = {}; + } + _done = true; + } + return {true, WEXITSTATUS(_status)}; + } + + pid_t pid() { + return _pid; + } + + int status() { + return WEXITSTATUS(_status); + } + + bool waited() { + return _done; + } +}; + +namespace detail { + // Cleanup + struct cleanup_data { + uint8_t *stack = nullptr; + size_t stack_size; + + void *args_copy = nullptr; + }; + + template + void cleanup(void *d) { + auto data = reinterpret_cast(d); + + if (data->args_copy) + delete reinterpret_cast(data->args_copy); + + if (data->stack) + munmap(data->stack, data->stack_size); + + delete data; + } + + template + inline void_callback_t make_cleanup_cb(uint8_t *stack, size_t stack_size, ArgP data) { + return { cleanup, new cleanup_data{stack, stack_size, data} }; + } + + // Entrypoints + template + int vclone_entry(void *arg) { + // XXX Does this work with non-movable types? + auto [f, args] = std::move(*reinterpret_cast(arg)); + return std::apply(f, args); + } + + // Common work function + template + inline child_ref do_clone(int(*entry)(void*), size_t stacksize, int flags, D *data) { + // Allocate stack + auto stack = reinterpret_cast( + mmap(nullptr, stacksize, PROT_WRITE|PROT_READ, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0)); + if (stack == MAP_FAILED) + return {-ENOMEM}; + + // Clone + // SIGCHLD is required for child_ref and cleanup to work. + auto pid = ::clone(entry, stack + stacksize, SIGCHLD | flags, data); + + // Discard everything if failed + if (pid < 0) { + if (data) + delete data; + if (stack) + munmap(stack, stacksize); + return {pid}; + } + + // Return child_ref with cleanup + return {pid, make_cleanup_cb(stack, stacksize, data)}; + } +} + +/** + * Spawn a process sharing the same virtual memory + * @param fn The function to call in the new process + * @param stacksize The size of the new process stack + * @param flags The clone(2) flags (SIGCHLD|CLONE_VM implied) + * @param args The function arguments + */ +template +child_ref vclone(F fn, size_t stacksize, long flags, Args... args) { + auto data = new std::pair{fn, std::tuple{std::forward(args)...}}; + return detail::do_clone(detail::vclone_entry, stacksize, CLONE_VM | flags, data); +} + +/** + * Spawn a process sharing the same virtual memory with synchronization primitives + * @param fn The function to call in the new process [int fn(ko::proc::sync::semapair, args...)] + * @param stacksize The size of the new process stack + * @param flags The clone(2) flags (SIGCHLD|CLONE_VM implied) + * @param args The function arguments + */ +template +std::pair svclone(F fn, size_t stacksize, long flags, Args... args) { + auto [sem_a, sem_b] = sync::make_semapair(false); + auto data = new std::pair{fn, std::tuple{sem_b, std::forward(args)...}}; + return {detail::do_clone(detail::vclone_entry, stacksize, CLONE_VM | flags, data), sem_a}; +} + +/** + * Spawn a child process and immediately execvp() a new image + * @param argv The argument list for the new process. + * @note argv[0] is used as the image name/path + */ +child_ref simple_spawn(const char *const *argv) { + auto pid = ::fork(); + if (pid == 0) + ::_exit(::execvp(argv[0], const_cast(argv))); + return {pid}; +} + +} // namespace ko::proc (ko::proc) diff --git a/src/koutil.hpp b/src/koutil.hpp new file mode 100644 index 0000000..329ef47 --- /dev/null +++ b/src/koutil.hpp @@ -0,0 +1,90 @@ +// ============================================================================ +// ko::util koutil.hpp +// (c) 2019 Taeyeon Mori +// ============================================================================ +// Miscellaneous utilities + +#pragma once + +#include +#include +#include +#include +#include + + +namespace ko::util { +// ------------------------------------------------------------------ +// Misc. +/// Build a string from fragments using ostringstream +template +inline std::string str(Args... args) { + auto sstream = std::ostringstream(); + (sstream <<...<< args); + return sstream.str(); +} + +// ------------------------------------------------------------------ +// Cvresult +/// A more verbose result type with a very terse error location indicator +using cvresult = std::pair; + +/// Allows short-circuiting c-style return values +struct cvshort { + int _state = 0; + const char *_where = nullptr; + + template + inline cvshort &then(F fn, Args... args) { + if (_state == 0) + std::tie(_state, _where) = fn(args...); + return *this; + } + + template + inline cvshort &then(const char *name, F fn, Args... args) { + if (_state == 0) { + _state = fn(args...); + if (_state != 0) + _where = name; + } + + return *this; + } + + template + inline cvshort &ifthen(bool cond, F fn, Args... args) { + if (_state == 0 && cond) + std::tie(_state, _where) = fn(args...); + return *this; + } + + template + inline cvshort &ifthen(const char *name, bool cond, F fn, Args... args) { + if (_state == 0 && cond) { + _state = fn(args...); + if (_state != 0) + _where = name; + } + + return *this; + } + + operator bool() const { + return _state == 0; + } + + int state() const { + return _state; + } + + const char *where() const { + return _where; + } + + operator cvresult() { + return {_state, _where}; + } +}; + +} // namespace ko::util diff --git a/src/makefile b/src/makefile new file mode 100644 index 0000000..298f661 --- /dev/null +++ b/src/makefile @@ -0,0 +1,95 @@ +# =================================================================== +# Taeyeon's miscallaneous applications +# (c) 2019 Taeyeon Mori +# =================================================================== +PROGS = keepassxc-print steamns chome overlayns +PROGS_ALL = fakensudo ssh-overlay-kiosk overlayns-static + +# Compiler config +CXX = clang++ +CXXFLAGS = -std=c++20 -Wall +OPTIMIZE = -O3 -flto +DEBUG = -DDEBUG -g3 + +# Install config +INSTALL_PATH ?= ~/.local/bin + + +# ------------------------------------------------------------------- +# Common targets +.PHONY: all most install clean +most: $(PROGS) + +all: $(PROGS) $(PROGS_ALL) + +install: $(PROGS) + @echo "Installing to INSTALL_PATH = $(INSTALL_PATH)" + @mkdir -p $(INSTALL_PATH) + @bash -c 'for prog in $(PROGS); do test -e $$prog && echo "Install $$prog -> $(INSTALL_PATH)" && install -m755 $$prog $(INSTALL_PATH); done' + +install-fake-sudo: fakensudo + install -m755 $< /usr/local/bin/sudo + +clean: + rm $(PROGS) $(PROGS_ALL) + + +# ------------------------------------------------------------------ +# Dependencies +dep_koutil = koutil.hpp +flg_koutil = + +dep_kofs = kofs.hpp +flg_kofs = + +dep_koos = koos.hpp $(dep_kofs) +flg_koos = $(flg_kofs) + +dep_kofd = kofd.hpp $(dep_kofs) +flg_kofd = $(flg_kofs) + +dep_kofd_pipe = kofd_pipe.hpp $(dep_kofd) +flg_kofd_pipe = $(flg_kofd) + +dep_koproc = koproc.hpp $(dep_kofd_pipe) +flg_koproc = -pthread $(flg_kofd_pipe) + +dep_kons = kons.hpp $(dep_koutil) $(dep_kofd) $(dep_koos) +flg_kons = $(flg_koutil) $(flg_kofd) $(flg_koos) + +dep_kons_clone = kons_clone.hpp $(dep_kons) $(dep_koproc) +flg_kons_clone = $(flg_kons) $(flg_koproc) + +dep_keepassxc = keepassxc-browser.hpp $(dep_koproc) +flg_keepassxc = $(shell pkg-config --cflags --libs libsodium jsoncpp) $(flg_koproc) + + +# ------------------------------------------------------------------- +# Applications +keepassxc-print: keepassxc-print.cpp $(dep_keepassxc) + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_keepassxc) -o $@ $< + +steamns: steamns.cpp $(dep_kons_clone) + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons_clone) -o $@ $< + +MOverlay2-nsexec: MOverlay2-nsexec.cpp $(dep_kons_clone) + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons_clone) -o $@ $< + +chome: chome.cpp $(dep_koutil) $(dep_kofd) $(dep_koos) + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_koutil) $(flg_kofd) $(flg_koos) -o $@ $< + +fakensudo: fakensudo.cpp $(dep_kons) + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons) -o $@ $< + +ssh-overlay-kiosk: ssh-overlay-kiosk.cpp $(dep_koutil) $(dep_kofd) $(dep_koos) + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_koutil) $(flg_kofd) $(flg_koos) -o $@ $< + @echo Setting $@ setuid root + sudo chown root $@ + sudo chmod u+s $@ + +overlayns: overlayns.cpp $(dep_kons) $(dep_koproc) + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons) $(flg_koproc) -o $@ $< + +overlayns-static: overlayns.cpp $(dep_kons) $(dep_koproc) makefile + $(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons) $(flg_koproc) -static -fdata-sections -ffunction-sections -Wl,--gc-sections -o $@ $< + diff --git a/src/overlayns.cpp b/src/overlayns.cpp new file mode 100644 index 0000000..b21d0af --- /dev/null +++ b/src/overlayns.cpp @@ -0,0 +1,431 @@ +// overlayns +// (c) 2021 Taeyeon Mori + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kons_clone.hpp" + +using namespace ko; +using namespace std::literals::string_literals; +using namespace std::literals::string_view_literals; + +static constexpr auto vers = "0.5"sv; + +void usage(char const * prog) { + printf("Synopsis: %s [-h] [-o ovl-spec]... [-m mnt-spec]... \n" + "\n" + "Run a command in it's own mount namespace\n" + "\n" + "Spec options:\n" + " -m mnt-spec Add a mount to the namespace\n" + " -o ovl-spec Add an overlay to the namespace\n" + "\n" + "Mount spec:\n" + " A mount specification takes the following format:\n" + " -m ,,[,