diff --git a/libexec/rc/rc.conf b/libexec/rc/rc.conf --- a/libexec/rc/rc.conf +++ b/libexec/rc/rc.conf @@ -725,6 +725,9 @@ mixer_enable="YES" # Run the sound mixer. opensm_enable="NO" # Opensm(8) for infiniband devices defaults to off nuageinit_enable="NO" # Run nuageinit at startup +mdoctl_enable="NO" # Load MAC/do at startup +mdoctl_program="/sbin/mdoctl" # mdoctl executable +mdoctl_config="/etc/mdo.conf" # Location of mdo.conf # rctl(8) requires kernel options RACCT and RCTL rctl_enable="YES" # Load rctl(8) rules on boot diff --git a/libexec/rc/rc.d/Makefile b/libexec/rc/rc.d/Makefile --- a/libexec/rc/rc.d/Makefile +++ b/libexec/rc/rc.d/Makefile @@ -46,6 +46,7 @@ mountlate \ mdconfig \ mdconfig2 \ + mdoctl \ msgs \ netif \ netoptions \ diff --git a/libexec/rc/rc.d/mdoctl b/libexec/rc/rc.d/mdoctl new file mode 100755 --- /dev/null +++ b/libexec/rc/rc.d/mdoctl @@ -0,0 +1,25 @@ +#!/bin/sh +# +# PROVIDE: mdoctl +# REQUIRE: FILESYSTEMS +# BEFORE: LOGIN + +. /etc/rc.subr + +name="mdoctl" +desc="MAC/do configuration" +rcvar="mdoctl_enable" +start_cmd="mdoctl_start" + +load_rc_config $name + +# doesn't make sense to run in a svcj: configures kernel +mdoctl_svcj="NO" + +mdoctl_start() { + sysctl -qn security.mac.do >/dev/null || kldload -qn mac_do || return 1 + $mdoctl_program enable + $mdoctl_program load -f "$mdoctl_config" +} + +run_rc_command "$1" diff --git a/sbin/Makefile b/sbin/Makefile --- a/sbin/Makefile +++ b/sbin/Makefile @@ -34,6 +34,7 @@ md5 \ mdconfig \ mdmfs \ + mdoctl \ mknod \ mksnap_ffs \ mount \ diff --git a/sbin/mdoctl/Makefile b/sbin/mdoctl/Makefile new file mode 100644 --- /dev/null +++ b/sbin/mdoctl/Makefile @@ -0,0 +1,7 @@ +PACKAGE= utilities +PROG_CXX= mdoctl +SRCS= mdoctl.cc +CXXSTD= c++23 +MAN= mdoctl.8 + +.include diff --git a/sbin/mdoctl/mdoctl.8 b/sbin/mdoctl/mdoctl.8 new file mode 100644 --- /dev/null +++ b/sbin/mdoctl/mdoctl.8 @@ -0,0 +1,52 @@ +.\" SPDX-License-Identifier: ISC +.\" Copyright (c) 2025 Lexi Winter. +.\" +.Dd May 24, 2025 +.Dt MDOCTL 8 +.Os +.Sh NAME +.Nm mdoctl +.Nd manage MAC/do +.Sh SYNOPSIS +.Nm +.Cm disable +.Nm +.Cm enable +.Nm +.Cm load +.Op Fl f Ar path +.Nm +.Cm status +.Sh DESCRIPTION +The +.Nm +utility is used to manage the MAC/do framework, +.Xr mac_do 4 . +The behaviour of +.Nm +depends on the command: +.Bl -tag -width disable +.It Cm status +Display the current status of MAC/do. +.It Cm enable +Enable MAC/do. +.It Cm disable +Disable MAC/do. +.It Cm load +Load the MAC/do ruleset from +.Pa /etc/mdo.conf , +or from +.Ar file +if specified +(see +.Xr mdo.conf 5 ) . +.El +.Sh SEE ALSO +.Xr mac_do 4 , +.Xr mdo 1 , +.Xr mdo.conf 5 +.Sh HISTORY +The +.Nm +utility appeared in +.Fx 15.0 . diff --git a/sbin/mdoctl/mdoctl.cc b/sbin/mdoctl/mdoctl.cc new file mode 100644 --- /dev/null +++ b/sbin/mdoctl/mdoctl.cc @@ -0,0 +1,547 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (c) 2025 Lexi Winter. + */ + +/* + * mdoctl: manage MAC/do + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace std::literals; + +namespace { + +/* Prototypes */ +void usage(); +int c_status(int argc, char **argv); +int c_enable(int argc, char **argv); +int c_disable(int argc, char **argv); +int c_load(int argc, char **argv); + +/* MAC/do constants */ +const std::string + sysctl_enabled = "security.mac.do.enabled", + sysctl_rules = "security.mac.do.rules", + config_path = "/etc/mdo.conf", + + // These are used in mdo.conf, and refer to nss names. + subject_type_user = "user", + subject_type_group = "group", + subject_type_sgroup = "+group", + subject_type_forcesgroup = "!group", + subject_type_denysgroup = "-group", + + // These are used in the sysctl interface, and refer to ids. + category_uid = "uid", + category_gid = "gid", + category_sgid = "+gid", + category_forcesgid = "!gid", + category_denysgid = "-gid", + subject_any_uid = "*", + subject_any_gid = "*"; + +/* The command table */ +using handler_t = std::function; +auto commands = std::unordered_map{ + { "status"sv, c_status }, + { "enable"sv, c_enable }, + { "disable"sv, c_disable }, + { "load"sv, c_load }, +}; + +/* + * skipws: Return the provided string with leading whitespace removed. + */ +std::string_view +skipws(std::string_view text) +{ + auto n = text.find_first_not_of(" \t"sv); + if (n == std::string_view::npos) + return {}; + return text.substr(n); +} + +/* + * next_word: skip leading whitespace in the given string, then return + * the next whitespace-delimited word and the rest of the string. + * If 'sep' is provided, use this as the separator instead of whitespace. + */ +std::pair +next_word(std::string_view text, std::string_view sep = " \t"sv) +{ + text = skipws(text); + + auto n = text.find_first_of(sep); + if (n == std::string_view::npos) + // The entire string is the next word. + return {text, {}}; + + return {text.substr(0, n), text.substr(n + 1)}; +} + +/* + * uidofname: convert a username into a uid. + */ +std::optional<::uid_t> +uidofname(std::string_view name) +{ + std::string cname(std::from_range, name); + if (auto const *pw = ::getpwnam(cname.c_str()); pw != nullptr) + return {pw->pw_uid}; + return {}; +} + +/* + * gidofname: convert a group name into a gid. + */ +std::optional<::gid_t> +gidofname(std::string_view name) +{ + std::string cname(std::from_range, name); + if (auto const *gr = ::getgrnam(cname.c_str()); gr != nullptr) + return {gr->gr_gid}; + return {}; +} + +/* + * The rule parser + */ + +struct parse_error : std::runtime_error { + template + parse_error(std::format_string fmt, Args &&...args) + : std::runtime_error(std::format(fmt, + std::forward(args)...)) + {} +}; + +/* A matcher (uid or gid) */ +struct matcher final { + std::string category; // The type of thing we match, e.g. "uid" + std::string subject; // The subject we match, e.g. "0". + + // Format this matcher in MAC/do format. + std::string format() const { + return std::format("{}={}", category, subject); + } +}; + +/* + * Parse a user matcher. + */ +std::optional +parse_user_matcher(std::string_view text) +{ + if (auto uid = uidofname(text); uid) + return matcher(category_uid, std::to_string(*uid)); + return {}; +} + +/* + * Parse a group matcher. + */ +std::optional +parse_group_matcher(std::string_view text) +{ + std::string_view p = text; + + // A group name must start with a % symbol. + if (p.empty() || p[0] != '%') + return {}; + p = p.substr(1); + + // See if this group exists. + if (auto gid = gidofname(p); gid) + return matcher(category_gid, std::to_string(*gid)); + return {}; +} + +/* + * Parse a matcher of some type (group or user). + */ +matcher +parse_subject(std::string_view text) +{ + if (auto matcher = parse_group_matcher(text); matcher) + return *matcher; + if (auto matcher = parse_user_matcher(text); matcher) + return *matcher; + // Not a username or a group name. + throw parse_error("invalid subject '{}'"sv, text); +} + +/* A target constraint */ +struct constraint { + std::string category; // The type of thing we match, e.g. "uid" + std::string subject; // The subject we match, e.g. "0". + + // Format this constraint in MAC/do format. + std::string format() const { + return std::format("{}={}", category, subject); + } +}; + +/* + * Parse a user constraint. + */ +constraint +parse_user_constraint(std::string_view text) +{ + if (text == "*"sv) + return {constraint(category_uid, subject_any_uid)}; + if (auto uid = uidofname(text); uid) + return {constraint(category_uid, std::to_string(*uid))}; + throw parse_error("unknown user '{}'"sv, text); +} + +/* + * Parse a group constraint. + */ +constraint +parse_group_constraint(std::string category, std::string_view text) +{ + if (text == "*"sv) + return {constraint(std::move(category), subject_any_gid)}; + if (auto gid = gidofname(text); gid) + return {constraint(std::move(category), std::to_string(*gid))}; + throw parse_error("unknown group '{}'"sv, text); +} + +/* + * Parse a constraint of some sort. + */ +constraint +parse_constraint(std::string_view text) +{ + auto [type, value] = next_word(text, "="); + + if (type == subject_type_user) + return parse_user_constraint(value); + + if (type == subject_type_group) + return parse_group_constraint(category_gid, value); + + if (type == subject_type_sgroup) + return parse_group_constraint(category_sgid, value); + + if (type == subject_type_forcesgroup) + return parse_group_constraint(category_forcesgid, value); + + if (type == subject_type_denysgroup) + return parse_group_constraint(category_denysgid, value); + + throw parse_error("invalid constraint type '{}'"sv, type); +} + +/* A rule, containing a matcher and a list of targets */ +struct rule final { + matcher subject; + std::vector constraints; + + rule(matcher subject_) : subject(std::move(subject_)) {} + + std::string format() const { + std::string rule_text; + bool first; + + rule_text = subject.format(); + rule_text += ">"sv; + + first = true; + for (auto const &cst : this->constraints) { + if (!first) + rule_text += ","sv; + first = false; + rule_text += cst.format(); + } + + return rule_text; + } +}; + +/* + * Parse a single rule and return a rule object. + */ +rule +parse_rule(std::string_view text) +{ + std::string_view rest, subject, constraint_text; + + // The rule should start with a subject matcher. + std::tie(subject, rest) = next_word(text); + if (subject.empty()) + throw parse_error("missing subject"); + + auto subject_matcher = parse_subject(subject); + rule ret(std::move(subject_matcher)); + + // Parse space-separated constraints. + rest = skipws(rest); + while (!rest.empty()) { + std::tie(constraint_text, rest) = next_word(rest); + ret.constraints.push_back(parse_constraint(constraint_text)); + } + + return (ret); +} + +/* + * mdo_enabled: return a constant indicating whether MAC/do is enabled + * mdo_status::enabled -> enabled + * mdo_status::disabled -> disabled + * mdo_status::not_loaded -> the mac_do module isn't loader (or we couldn't + * fetch the sysctl for some reason) + */ +enum struct mdo_status { + enabled, disabled, not_loaded +}; + +mdo_status +mdo_enabled() +{ + int status_value = 0; + std::size_t status_size = sizeof(status_value); + + if (sysctlbyname(sysctl_enabled.c_str(), &status_value, &status_size, + nullptr, 0) != 0) + return mdo_status::not_loaded; + + return status_value ? mdo_status::enabled : mdo_status::disabled; +} + +/* + * usage: print usage + */ +void +usage() +{ + std::print(std::cerr, +"usage: {0} disable\n" +" {0} enable\n" +" {0} load [-f ]\n" +" {0} status\n", + getprogname()); +} + +/* + * status: show the current status of MAC/do. + */ +int +c_status(int argc, char **) +{ + if (argc != 1) { + usage(); + return (1); + } + + switch (mdo_enabled()) { + case mdo_status::enabled: + std::print("enabled\n"); + break; + case mdo_status::disabled: + std::print("disabled\n"); + break; + case mdo_status::not_loaded: + std::print("not loaded\n"); + break; + } + + return (0); +} + +/* + * c_enable: turn MAC/do on + */ +int +c_enable(int argc, char **) +{ + int value; + std::size_t size; + + if (argc != 1) { + usage(); + return (1); + } + + value = 1; + size = sizeof(value); + if (sysctlbyname(sysctl_enabled.c_str(), nullptr, 0, + &value, size) == 0) + return (0); + + if (errno == ENOENT) + std::print(std::cerr, "{}: MAC/do is not loaded\n", + getprogname()); + else + std::print(std::cerr, "{}: {}\n", getprogname(), + strerror(errno)); + return (1); +} + +/* + * c_disable: turn MAC/do off + */ +int +c_disable(int argc, char **) +{ + int value; + std::size_t size; + + if (argc != 1) { + usage(); + return (1); + } + + value = 0; + size = sizeof(value); + if (sysctlbyname(sysctl_enabled.c_str(), nullptr, 0, + &value, size) == 0) + return (0); + + if (errno == ENOENT) + std::print(std::cerr, "{}: MAC/do is not loaded\n", + getprogname()); + else + std::print(std::cerr, "{}: {}\n", getprogname(), + strerror(errno)); + return (1); +} + +/* + * c_load: load MAC/do rules from a file into the kernel + */ +int +c_load(int argc, char **argv) +{ + std::FILE *fp; + std::vector lines; + std::vector rules; + std::string rules_text, cfgpath; + unsigned lineno; + int ch; + bool first; + static std::array buf; + + cfgpath = config_path; + while ((ch = getopt(argc, argv, "f:")) != -1) { + switch (ch) { + case 'f': + cfgpath = optarg; + break; + case '?': + default: + usage(); + return (-1); + } + } + + argc -= optind; + argv += optind; + + if (argc != 0) { + usage(); + return (1); + } + + if ((fp = std::fopen(cfgpath.c_str(), "r")) == NULL) { + std::print(std::cerr, "{}: {}\n", config_path, strerror(errno)); + return (1); + } + + while (fgets(buf.data(), buf.size(), fp) != NULL) { + std::string_view line(buf.data()); + if (!line.empty() && line[line.size() - 1] == '\n') + line = line.substr(0, line.size() - 1); + lines.emplace_back(line); + } + + if (ferror(fp)) { + std::print(std::cerr, "{}: {}\n", config_path, strerror(errno)); + return (1); + } + + lineno = 0; + for (auto line : lines) { + ++lineno; + + // Remove leading whitespace + line = skipws(line); + + // Skip comments and empty line + if (line.empty() || line[0] == '#') + continue; + + try { + rules.push_back(parse_rule(line)); + } catch (parse_error const &exc) { + std::print(std::cerr, "{}:{}: error: {}\n", + config_path, lineno, exc.what()); + return (1); + } + } + + /* We parsed all the rules successfully, so concat them */ + first = true; + for (auto const &rl : rules) { + if (!first) + rules_text += ";"; + first = false; + rules_text += rl.format(); + } + + if (sysctlbyname(sysctl_rules.c_str(), nullptr, 0, + rules_text.data(), rules_text.size()) == -1) { + std::print("{}: failed to configure kernel ruleset: {}\n", + getprogname(), strerror(errno)); + return (1); + } + + return (0); +} + +} // anonymous namespace + +int +main(int argc, char **argv) +{ + if (argc < 2) { + usage(); + return (1); + } + + setprogname(argv[0]); + + ++argv; + --argc; + + /* + * Dispatch the top-level command. + */ + + if (auto c = commands.find(argv[0]); c != commands.end()) + return c->second(argc, argv); + + std::print(std::cerr, "{}: unknown command '{}'\n", + getprogname(), argv[0]); + usage(); + return (1); +} diff --git a/share/man/man5/rc.conf.5 b/share/man/man5/rc.conf.5 --- a/share/man/man5/rc.conf.5 +++ b/share/man/man5/rc.conf.5 @@ -22,7 +22,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd May 21, 2025 +.Dd May 24, 2025 .Dt RC.CONF 5 .Os .Sh NAME @@ -4940,6 +4940,24 @@ .Dq Li YES , these are the flags to pass to the .Xr sendmail 8 +.It Va mdoctl_enable +.Pq Vt bool +If set to +.Dq Li YES , +run +.Xr mdoctl 8 +at startup to configure MAC/do rules. +.It Va mdoctl_program +.Pq Vt str +The path to +.Pa mdoctl . +The default path is +.Pa /sbin/mdoctl . +.It Va mdoctl_config +.Pq Vt str +The MAC/do configuration to load. +The default configuration file is +.Pa /etc/mdo.conf . .It Va precious_machine If set to .Dq Li YES ,