]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sd-bus: add API for connecting to a specific user's user bus of a specific container
authorLennart Poettering <lennart@poettering.net>
Mon, 14 Dec 2020 12:21:58 +0000 (13:21 +0100)
committerLennart Poettering <lennart@poettering.net>
Tue, 15 Dec 2020 17:00:15 +0000 (18:00 +0100)
This is unfortunately harder to implement than it sounds. The user's bus
is bound a to the user's lifecycle after all (i.e. only exists as long
as the user has at least one PAM session), and the path dynamically (at
least theoretically, in practice it's going to be the same always)
generated via $XDG_RUNTIME_DIR in /run/.

To fix this properly, we'll thus go through PAM before connecting to a
user bus. Which is hard since we cannot just link against libpam in the
container, since the container might have been compiled entirely
differently. So our way out is to use systemd-run from outside, which
invokes a transient unit that does PAM from outside, doing so via D-Bus.
Inside the transient unit we then invoke systemd-stdio-bridge which
forwards D-Bus from the user bus to us. The systemd-stdio-bridge makes
up the PAM session and thus we can sure tht the bus exists at least as
long as the bus connection is kept.

Or so say this differently: if you use "systemctl -M lennart@foobar"
now, the bus connection works like this:

        1. sd-bus on the host forks off:

                systemd-run -M foobar -PGq --wait -pUser=lennart -pPAMName=login systemd-stdio-bridge

        2. systemd-run gets a connection to the "foobar" container's
           system bus, and invokes the "systemd-stdio-bridge" binary as
           transient service inside a PAM session for the user "lennart"

        3. The systemd-stdio-bridge then proxies our D-Bus traffic to
           the user bus.

sd-bus (on host) → systemd-run (on host) → systemd-stdio-bridge (in container)

Complicated? Well, to some point yes, but otoh it's actually nice in
various other ways, primarily as it makes the -H and -M codepaths more
alike. In the -H case (i.e. connect to remote host via SSH) a very
similar three steps are used. The only difference is that instead of
"systemd-run" the "ssh" binary is used to invoke the stdio bridge in a
PAM session of some other system. Thus we get similar implementation and
isolation for similar operations.

Fixes: #14580
src/busctl/busctl.c
src/libsystemd/libsystemd.sym
src/libsystemd/sd-bus/bus-internal.h
src/libsystemd/sd-bus/sd-bus.c
src/shared/bus-util.c
src/stdio-bridge/stdio-bridge.c
src/systemctl/systemctl.c
src/systemd/sd-bus.h

index 06a15ddd8061823ae1d8cb7e70fb0173aecc4e73..6f805e95a09a5bbdb394454e2863990c31b2c7f5 100644 (file)
@@ -121,7 +121,7 @@ static int acquire_bus(bool set_monitor, sd_bus **ret) {
                         break;
 
                 case BUS_TRANSPORT_MACHINE:
-                        r = bus_set_address_system_machine(bus, arg_host);
+                        r = bus_set_address_machine(bus, arg_user, arg_host);
                         break;
 
                 default:
index b03bcd952f4e4e4d5b761883bddbdd3aad2ab611..9e9e8fd37236451e504e9915baac432917da25d2 100644 (file)
@@ -739,6 +739,8 @@ global:
 
 LIBSYSTEMD_248 {
 global:
+        sd_bus_open_user_machine;
+
         sd_event_source_set_ratelimit;
         sd_event_source_get_ratelimit;
         sd_event_source_is_ratelimited;
index 233a2283150f5589d399d9bffcae63d040c0ac36..82fa97fc5d8241b13214fd6e3dab015005178f93 100644 (file)
@@ -401,7 +401,7 @@ void bus_close_io_fds(sd_bus *b);
 int bus_set_address_system(sd_bus *bus);
 int bus_set_address_user(sd_bus *bus);
 int bus_set_address_system_remote(sd_bus *b, const char *host);
-int bus_set_address_system_machine(sd_bus *b, const char *machine);
+int bus_set_address_machine(sd_bus *b, bool user, const char *machine);
 
 int bus_maybe_reply_error(sd_bus_message *m, int r, sd_bus_error *error);
 
index da7827015a447a4106a92a7c256884f850e1b0cf..e9bc19d96a9696776430d0ba66afbf6cf71d426e 100644 (file)
@@ -41,6 +41,7 @@
 #include "process-util.h"
 #include "string-util.h"
 #include "strv.h"
+#include "user-util.h"
 
 #define log_debug_bus_message(m)                                         \
         do {                                                             \
@@ -1514,44 +1515,228 @@ _public_ int sd_bus_open_system_remote(sd_bus **ret, const char *host) {
         return 0;
 }
 
-int bus_set_address_system_machine(sd_bus *b, const char *machine) {
-        _cleanup_free_ char *e = NULL;
+int bus_set_address_machine(sd_bus *b, bool user, const char *machine) {
+        const char *rhs;
         char *a;
 
         assert(b);
         assert(machine);
 
-        e = bus_address_escape(machine);
-        if (!e)
-                return -ENOMEM;
+        rhs = strchr(machine, '@');
+        if (rhs || user) {
+                _cleanup_free_ char *u = NULL, *eu = NULL, *erhs = NULL;
+
+                /* If there's an "@" in the container specification, we'll connect as a user specified at its
+                 * left hand side, which is useful in combination with user=true. This isn't as trivial as it
+                 * might sound: it's not sufficient to enter the container and connect to some socket there,
+                 * since the --user socket path depends on $XDG_RUNTIME_DIR which is set via PAM. Thus, to be
+                 * able to connect, we need to have a PAM session. Our way out?  We use systemd-run to get
+                 * into the container and acquire a PAM session there, and then invoke systemd-stdio-bridge
+                 * in it, which propagates the bus transport to us.*/
+
+                if (rhs) {
+                        if (rhs > machine)
+                                u = strndup(machine, rhs - machine);
+                        else
+                                u = getusername_malloc(); /* Empty user name, let's use the local one */
+                        if (!u)
+                                return -ENOMEM;
 
-        a = strjoin("x-machine-unix:machine=", e);
-        if (!a)
-                return -ENOMEM;
+                        eu = bus_address_escape(u);
+                        if (!eu)
+                                return -ENOMEM;
+
+                        rhs++;
+                } else {
+                        /* No "@" specified but we shall connect to the user instance? Then assume root (and
+                         * not a user named identically to the calling one). This means:
+                         *
+                         *     --machine=foobar --user    → connect to user bus of root user in container "foobar"
+                         *     --machine=@foobar --user   → connect to user bus of user named like the calling user in container "foobar"
+                         *
+                         * Why? so that behaviour for "--machine=foobar --system" is roughly similar to
+                         * "--machine=foobar --user": both times we unconditionally connect as root user
+                         * regardless what the calling user is. */
+
+                        rhs = machine;
+                }
+
+                if (!isempty(rhs)) {
+                        erhs = bus_address_escape(rhs);
+                        if (!erhs)
+                                return -ENOMEM;
+                }
+
+                /* systemd-run -M… -PGq --wait -pUser=… -pPAMName=login systemd-stdio-bridge */
+
+                a = strjoin("unixexec:path=systemd-run,"
+                            "argv1=-M", erhs ?: ".host", ","
+                            "argv2=-PGq,"
+                            "argv3=--wait,"
+                            "argv4=-pUser%3d", eu ?: "root", ",",
+                            "argv5=-pPAMName%3dlogin,"
+                            "argv6=systemd-stdio-bridge");
+                if (!a)
+                        return -ENOMEM;
+
+                if (user) {
+                        char *k;
+
+                        /* Ideally we'd use the "--user" switch to systemd-stdio-bridge here, but it's only
+                         * available in recent systemd versions. Using the "-p" switch with the explicit path
+                         * is a working alternative, and is compatible with older versions, hence that's what
+                         * we use here. */
+
+                        k = strjoin(a, ",argv7=-punix:path%3d%24%7bXDG_RUNTIME_DIR%7d/bus");
+                        if (!k)
+                                return -ENOMEM;
+
+                        free_and_replace(a, k);
+                }
+        } else {
+                _cleanup_free_ char *e = NULL;
+
+                /* Just a container name, we can go the simple way, and just join the container, and connect
+                 * to the well-known path of the system bus there. */
+
+                e = bus_address_escape(machine);
+                if (!e)
+                        return -ENOMEM;
+
+                a = strjoin("x-machine-unix:machine=", e);
+                if (!a)
+                        return -ENOMEM;
+        }
 
         return free_and_replace(b->address, a);
 }
 
-_public_ int sd_bus_open_system_machine(sd_bus **ret, const char *machine) {
+static int user_and_machine_valid(const char *user_and_machine) {
+        const char *h;
+
+        /* Checks if a container specification in the form "user@container" or just "container" is valid.
+         *
+         * If the "@" syntax is used we'll allow either the "user" or the "container" part to be omitted, but
+         * not both. */
+
+        h = strchr(user_and_machine, '@');
+        if (!h)
+                h = user_and_machine;
+        else {
+                _cleanup_free_ char *user = NULL;
+
+                user = strndup(user_and_machine, h - user_and_machine);
+                if (!user)
+                        return -ENOMEM;
+
+                if (!isempty(user) && !valid_user_group_name(user, VALID_USER_RELAX))
+                        return false;
+
+                h++;
+
+                if (isempty(h))
+                        return !isempty(user);
+        }
+
+        return hostname_is_valid(h, VALID_HOSTNAME_DOT_HOST);
+}
+
+static int user_and_machine_equivalent(const char *user_and_machine) {
+        _cleanup_free_ char *un = NULL;
+        const char *f;
+
+        /* Returns true if the specified user+machine name are actually equivalent to our own identity and
+         * our own host. If so we can shortcut things.  Why bother? Because that way we don't have to fork
+         * off short-lived worker processes that are then unavailable for authentication and logging in the
+         * peer. Moreover joining a namespace requires privileges. If we are in the right namespace anyway,
+         * we can avoid permission problems thus. */
+
+        assert(user_and_machine);
+
+        /* Omitting the user name means that we shall use the same user name as we run as locally, which
+         * means we'll end up on the same host, let's shortcut */
+        if (streq(user_and_machine, "@.host"))
+                return true;
+
+        /* Otherwise, if we are root, then we can also allow the ".host" syntax, as that's the user this
+         * would connect to. */
+        if (geteuid() == 0 && STR_IN_SET(user_and_machine, ".host", "root@.host"))
+                return true;
+
+        /* Otherwise, we have to figure our user name, and compare things with that. */
+        un = getusername_malloc();
+        if (!un)
+                return -ENOMEM;
+
+        f = startswith(user_and_machine, un);
+        if (!f)
+                return false;
+
+        return STR_IN_SET(f, "@", "@.host");
+}
+
+_public_ int sd_bus_open_system_machine(sd_bus **ret, const char *user_and_machine) {
         _cleanup_(bus_freep) sd_bus *b = NULL;
         int r;
 
-        assert_return(machine, -EINVAL);
+        assert_return(user_and_machine, -EINVAL);
         assert_return(ret, -EINVAL);
-        assert_return(hostname_is_valid(machine, VALID_HOSTNAME_DOT_HOST), -EINVAL);
+
+        if (user_and_machine_equivalent(user_and_machine))
+                return sd_bus_open_system(ret);
+
+        r = user_and_machine_valid(user_and_machine);
+        if (r < 0)
+                return r;
+
+        assert_return(r > 0, -EINVAL);
 
         r = sd_bus_new(&b);
         if (r < 0)
                 return r;
 
-        r = bus_set_address_system_machine(b, machine);
+        r = bus_set_address_machine(b, false, user_and_machine);
         if (r < 0)
                 return r;
 
         b->bus_client = true;
-        b->trusted = false;
         b->is_system = true;
-        b->is_local = false;
+
+        r = sd_bus_start(b);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(b);
+        return 0;
+}
+
+_public_ int sd_bus_open_user_machine(sd_bus **ret, const char *user_and_machine) {
+        _cleanup_(bus_freep) sd_bus *b = NULL;
+        int r;
+
+        assert_return(user_and_machine, -EINVAL);
+        assert_return(ret, -EINVAL);
+
+        /* Shortcut things if we'd end up on this host and as the same user.  */
+        if (user_and_machine_equivalent(user_and_machine))
+                return sd_bus_open_user(ret);
+
+        r = user_and_machine_valid(user_and_machine);
+        if (r < 0)
+                return r;
+
+        assert_return(r > 0, -EINVAL);
+
+        r = sd_bus_new(&b);
+        if (r < 0)
+                return r;
+
+        r = bus_set_address_machine(b, true, user_and_machine);
+        if (r < 0)
+                return r;
+
+        b->bus_client = true;
+        b->trusted = true;
 
         r = sd_bus_start(b);
         if (r < 0)
index fbda218b3b335b43d0848a376eab0199a13ac9a9..58211ebd0316e2445e2032f20884ee5bed348d16 100644 (file)
@@ -249,7 +249,12 @@ int bus_connect_user_systemd(sd_bus **_bus) {
         return 0;
 }
 
-int bus_connect_transport(BusTransport transport, const char *host, bool user, sd_bus **ret) {
+int bus_connect_transport(
+                BusTransport transport,
+                const char *host,
+                bool user,
+                sd_bus **ret) {
+
         _cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL;
         int r;
 
@@ -258,7 +263,7 @@ int bus_connect_transport(BusTransport transport, const char *host, bool user, s
         assert(ret);
 
         assert_return((transport == BUS_TRANSPORT_LOCAL) == !host, -EINVAL);
-        assert_return(transport == BUS_TRANSPORT_LOCAL || !user, -EOPNOTSUPP);
+        assert_return(transport != BUS_TRANSPORT_REMOTE || !user, -EOPNOTSUPP);
 
         switch (transport) {
 
@@ -279,7 +284,10 @@ int bus_connect_transport(BusTransport transport, const char *host, bool user, s
                 break;
 
         case BUS_TRANSPORT_MACHINE:
-                r = sd_bus_open_system_machine(&bus, host);
+                if (user)
+                        r = sd_bus_open_user_machine(&bus, host);
+                else
+                        r = sd_bus_open_system_machine(&bus, host);
                 break;
 
         default:
@@ -293,7 +301,6 @@ int bus_connect_transport(BusTransport transport, const char *host, bool user, s
                 return r;
 
         *ret = TAKE_PTR(bus);
-
         return 0;
 }
 
index 81d50717b24ddf17c62fcab4a564607dd88228b4..1b7c3feaeace91f66bded4d7da05e38f454f0b63 100644 (file)
@@ -121,7 +121,7 @@ static int run(int argc, char *argv[]) {
                 return log_error_errno(r, "Failed to allocate bus: %m");
 
         if (arg_transport == BUS_TRANSPORT_MACHINE)
-                r = bus_set_address_system_machine(a, arg_bus_path);
+                r = bus_set_address_machine(a, false, arg_bus_path);
         else
                 r = sd_bus_set_address(a, arg_bus_path);
         if (r < 0)
index d002d933aed9a52269c98a64495e13803c179d1b..9a934badce114aa06ab63217bce8df5ac51efadb 100644 (file)
@@ -877,7 +877,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
                         assert_not_reached("Unhandled option");
                 }
 
-        if (arg_transport != BUS_TRANSPORT_LOCAL && arg_scope != UNIT_FILE_SYSTEM)
+        if (arg_transport == BUS_TRANSPORT_REMOTE && arg_scope != UNIT_FILE_SYSTEM)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                        "Cannot access user instance remotely.");
 
index 87fbcf366e47a77d330aacb45eea0dbdd105fe32..c51df2908d8818e7a12e23e1dad02fa6c9292313 100644 (file)
@@ -135,6 +135,7 @@ int sd_bus_open(sd_bus **ret);
 int sd_bus_open_with_description(sd_bus **ret, const char *description);
 int sd_bus_open_user(sd_bus **ret);
 int sd_bus_open_user_with_description(sd_bus **ret, const char *description);
+int sd_bus_open_user_machine(sd_bus **ret, const char *machine);
 int sd_bus_open_system(sd_bus **ret);
 int sd_bus_open_system_with_description(sd_bus **ret, const char *description);
 int sd_bus_open_system_remote(sd_bus **ret, const char *host);