Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F154072623
D50510.id155972.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
12 KB
Referenced Files
None
Subscribers
None
D50510.id155972.diff
View Options
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 <bsd.prog.mk>
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,50 @@
+.\" 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
+status
+.Pp
+.Nm
+enable
+.Pp
+.Nm
+disable
+.Pp
+.Nm
+load
+.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
+(see
+.Xr mdo.conf 5 ) .
+.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,513 @@
+/*
+ * SPDX-License-Identifier: ISC
+ *
+ * Copyright (c) 2025 Lexi Winter.
+ */
+
+/*
+ * mdoctl: manage MAC/do
+ */
+
+#include <sys/types.h>
+#include <sys/sysctl.h>
+
+#include <cctype>
+#include <format>
+#include <functional>
+#include <iostream>
+#include <optional>
+#include <print>
+#include <ranges>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+
+#include <unistd.h>
+#include <pwd.h>
+#include <grp.h>
+
+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<int (int, char **)>;
+auto commands = std::unordered_map<std::string_view, handler_t>{
+ { "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<std::string_view, std::string_view>
+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<typename... Args>
+ parse_error(std::format_string<Args...> fmt, Args &&...args)
+ : std::runtime_error(std::format(fmt,
+ std::forward<Args>(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<matcher>
+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<matcher>
+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<constraint> 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} status\n"
+" {0} enable\n"
+" {0} disable\n",
+" {0} load\n",
+ getprogname());
+}
+
+/*
+ * status: show the current status of MAC/do.
+ */
+int
+c_status(int, char **)
+{
+ 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, char **)
+{
+ int value;
+ std::size_t size;
+
+ 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, char **)
+{
+ int value;
+ std::size_t size;
+
+ 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, char **)
+{
+ std::FILE *fp;
+ std::vector<std::string> lines;
+ std::vector<rule> rules;
+ std::string rules_text;
+ unsigned lineno;
+ bool first;
+ static std::array<char, 2048> buf;
+
+ if ((fp = std::fopen(config_path.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("{}, line {}: {}\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 == 0) {
+ usage();
+ return (1);
+ }
+
+ setprogname(argv[0]);
+
+ ++argv;
+ --argc;
+
+ /*
+ * Dispatch the top-level command.
+ */
+ if (argc == 0) {
+ usage();
+ return (1);
+ }
+
+ 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]);
+ return (1);
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Apr 26, 11:10 PM (12 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
32200304
Default Alt Text
D50510.id155972.diff (12 KB)
Attached To
Mode
D50510: mdoctl(8): a configuration utility for MAC/do
Attached
Detach File
Event Timeline
Log In to Comment