Skip to content

Commit 1b63083

Browse files
committed
sd-bus: add API for connecting to a specific user's user bus of a specific container
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
1 parent 1ca3741 commit 1b63083

File tree

8 files changed

+217
-22
lines changed

8 files changed

+217
-22
lines changed

src/busctl/busctl.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ static int acquire_bus(bool set_monitor, sd_bus **ret) {
121121
break;
122122

123123
case BUS_TRANSPORT_MACHINE:
124-
r = bus_set_address_system_machine(bus, arg_host);
124+
r = bus_set_address_machine(bus, arg_user, arg_host);
125125
break;
126126

127127
default:

src/libsystemd/libsystemd.sym

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,8 @@ global:
739739

740740
LIBSYSTEMD_248 {
741741
global:
742+
sd_bus_open_user_machine;
743+
742744
sd_event_source_set_ratelimit;
743745
sd_event_source_get_ratelimit;
744746
sd_event_source_is_ratelimited;

src/libsystemd/sd-bus/bus-internal.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ void bus_close_io_fds(sd_bus *b);
401401
int bus_set_address_system(sd_bus *bus);
402402
int bus_set_address_user(sd_bus *bus);
403403
int bus_set_address_system_remote(sd_bus *b, const char *host);
404-
int bus_set_address_system_machine(sd_bus *b, const char *machine);
404+
int bus_set_address_machine(sd_bus *b, bool user, const char *machine);
405405

406406
int bus_maybe_reply_error(sd_bus_message *m, int r, sd_bus_error *error);
407407

src/libsystemd/sd-bus/sd-bus.c

Lines changed: 199 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include "process-util.h"
4242
#include "string-util.h"
4343
#include "strv.h"
44+
#include "user-util.h"
4445

4546
#define log_debug_bus_message(m) \
4647
do { \
@@ -1514,44 +1515,228 @@ _public_ int sd_bus_open_system_remote(sd_bus **ret, const char *host) {
15141515
return 0;
15151516
}
15161517

1517-
int bus_set_address_system_machine(sd_bus *b, const char *machine) {
1518-
_cleanup_free_ char *e = NULL;
1518+
int bus_set_address_machine(sd_bus *b, bool user, const char *machine) {
1519+
const char *rhs;
15191520
char *a;
15201521

15211522
assert(b);
15221523
assert(machine);
15231524

1524-
e = bus_address_escape(machine);
1525-
if (!e)
1526-
return -ENOMEM;
1525+
rhs = strchr(machine, '@');
1526+
if (rhs || user) {
1527+
_cleanup_free_ char *u = NULL, *eu = NULL, *erhs = NULL;
1528+
1529+
/* If there's an "@" in the container specification, we'll connect as a user specified at its
1530+
* left hand side, which is useful in combination with user=true. This isn't as trivial as it
1531+
* might sound: it's not sufficient to enter the container and connect to some socket there,
1532+
* since the --user socket path depends on $XDG_RUNTIME_DIR which is set via PAM. Thus, to be
1533+
* able to connect, we need to have a PAM session. Our way out? We use systemd-run to get
1534+
* into the container and acquire a PAM session there, and then invoke systemd-stdio-bridge
1535+
* in it, which propagates the bus transport to us.*/
1536+
1537+
if (rhs) {
1538+
if (rhs > machine)
1539+
u = strndup(machine, rhs - machine);
1540+
else
1541+
u = getusername_malloc(); /* Empty user name, let's use the local one */
1542+
if (!u)
1543+
return -ENOMEM;
15271544

1528-
a = strjoin("x-machine-unix:machine=", e);
1529-
if (!a)
1530-
return -ENOMEM;
1545+
eu = bus_address_escape(u);
1546+
if (!eu)
1547+
return -ENOMEM;
1548+
1549+
rhs++;
1550+
} else {
1551+
/* No "@" specified but we shall connect to the user instance? Then assume root (and
1552+
* not a user named identically to the calling one). This means:
1553+
*
1554+
* --machine=foobar --user → connect to user bus of root user in container "foobar"
1555+
* --machine=@foobar --user → connect to user bus of user named like the calling user in container "foobar"
1556+
*
1557+
* Why? so that behaviour for "--machine=foobar --system" is roughly similar to
1558+
* "--machine=foobar --user": both times we unconditionally connect as root user
1559+
* regardless what the calling user is. */
1560+
1561+
rhs = machine;
1562+
}
1563+
1564+
if (!isempty(rhs)) {
1565+
erhs = bus_address_escape(rhs);
1566+
if (!erhs)
1567+
return -ENOMEM;
1568+
}
1569+
1570+
/* systemd-run -M… -PGq --wait -pUser=… -pPAMName=login systemd-stdio-bridge */
1571+
1572+
a = strjoin("unixexec:path=systemd-run,"
1573+
"argv1=-M", erhs ?: ".host", ","
1574+
"argv2=-PGq,"
1575+
"argv3=--wait,"
1576+
"argv4=-pUser%3d", eu ?: "root", ",",
1577+
"argv5=-pPAMName%3dlogin,"
1578+
"argv6=systemd-stdio-bridge");
1579+
if (!a)
1580+
return -ENOMEM;
1581+
1582+
if (user) {
1583+
char *k;
1584+
1585+
/* Ideally we'd use the "--user" switch to systemd-stdio-bridge here, but it's only
1586+
* available in recent systemd versions. Using the "-p" switch with the explicit path
1587+
* is a working alternative, and is compatible with older versions, hence that's what
1588+
* we use here. */
1589+
1590+
k = strjoin(a, ",argv7=-punix:path%3d%24%7bXDG_RUNTIME_DIR%7d/bus");
1591+
if (!k)
1592+
return -ENOMEM;
1593+
1594+
free_and_replace(a, k);
1595+
}
1596+
} else {
1597+
_cleanup_free_ char *e = NULL;
1598+
1599+
/* Just a container name, we can go the simple way, and just join the container, and connect
1600+
* to the well-known path of the system bus there. */
1601+
1602+
e = bus_address_escape(machine);
1603+
if (!e)
1604+
return -ENOMEM;
1605+
1606+
a = strjoin("x-machine-unix:machine=", e);
1607+
if (!a)
1608+
return -ENOMEM;
1609+
}
15311610

15321611
return free_and_replace(b->address, a);
15331612
}
15341613

1535-
_public_ int sd_bus_open_system_machine(sd_bus **ret, const char *machine) {
1614+
static int user_and_machine_valid(const char *user_and_machine) {
1615+
const char *h;
1616+
1617+
/* Checks if a container specification in the form "user@container" or just "container" is valid.
1618+
*
1619+
* If the "@" syntax is used we'll allow either the "user" or the "container" part to be omitted, but
1620+
* not both. */
1621+
1622+
h = strchr(user_and_machine, '@');
1623+
if (!h)
1624+
h = user_and_machine;
1625+
else {
1626+
_cleanup_free_ char *user = NULL;
1627+
1628+
user = strndup(user_and_machine, h - user_and_machine);
1629+
if (!user)
1630+
return -ENOMEM;
1631+
1632+
if (!isempty(user) && !valid_user_group_name(user, VALID_USER_RELAX))
1633+
return false;
1634+
1635+
h++;
1636+
1637+
if (isempty(h))
1638+
return !isempty(user);
1639+
}
1640+
1641+
return hostname_is_valid(h, VALID_HOSTNAME_DOT_HOST);
1642+
}
1643+
1644+
static int user_and_machine_equivalent(const char *user_and_machine) {
1645+
_cleanup_free_ char *un = NULL;
1646+
const char *f;
1647+
1648+
/* Returns true if the specified user+machine name are actually equivalent to our own identity and
1649+
* our own host. If so we can shortcut things. Why bother? Because that way we don't have to fork
1650+
* off short-lived worker processes that are then unavailable for authentication and logging in the
1651+
* peer. Moreover joining a namespace requires privileges. If we are in the right namespace anyway,
1652+
* we can avoid permission problems thus. */
1653+
1654+
assert(user_and_machine);
1655+
1656+
/* Omitting the user name means that we shall use the same user name as we run as locally, which
1657+
* means we'll end up on the same host, let's shortcut */
1658+
if (streq(user_and_machine, "@.host"))
1659+
return true;
1660+
1661+
/* Otherwise, if we are root, then we can also allow the ".host" syntax, as that's the user this
1662+
* would connect to. */
1663+
if (geteuid() == 0 && STR_IN_SET(user_and_machine, ".host", "root@.host"))
1664+
return true;
1665+
1666+
/* Otherwise, we have to figure our user name, and compare things with that. */
1667+
un = getusername_malloc();
1668+
if (!un)
1669+
return -ENOMEM;
1670+
1671+
f = startswith(user_and_machine, un);
1672+
if (!f)
1673+
return false;
1674+
1675+
return STR_IN_SET(f, "@", "@.host");
1676+
}
1677+
1678+
_public_ int sd_bus_open_system_machine(sd_bus **ret, const char *user_and_machine) {
15361679
_cleanup_(bus_freep) sd_bus *b = NULL;
15371680
int r;
15381681

1539-
assert_return(machine, -EINVAL);
1682+
assert_return(user_and_machine, -EINVAL);
15401683
assert_return(ret, -EINVAL);
1541-
assert_return(hostname_is_valid(machine, VALID_HOSTNAME_DOT_HOST), -EINVAL);
1684+
1685+
if (user_and_machine_equivalent(user_and_machine))
1686+
return sd_bus_open_system(ret);
1687+
1688+
r = user_and_machine_valid(user_and_machine);
1689+
if (r < 0)
1690+
return r;
1691+
1692+
assert_return(r > 0, -EINVAL);
15421693

15431694
r = sd_bus_new(&b);
15441695
if (r < 0)
15451696
return r;
15461697

1547-
r = bus_set_address_system_machine(b, machine);
1698+
r = bus_set_address_machine(b, false, user_and_machine);
15481699
if (r < 0)
15491700
return r;
15501701

15511702
b->bus_client = true;
1552-
b->trusted = false;
15531703
b->is_system = true;
1554-
b->is_local = false;
1704+
1705+
r = sd_bus_start(b);
1706+
if (r < 0)
1707+
return r;
1708+
1709+
*ret = TAKE_PTR(b);
1710+
return 0;
1711+
}
1712+
1713+
_public_ int sd_bus_open_user_machine(sd_bus **ret, const char *user_and_machine) {
1714+
_cleanup_(bus_freep) sd_bus *b = NULL;
1715+
int r;
1716+
1717+
assert_return(user_and_machine, -EINVAL);
1718+
assert_return(ret, -EINVAL);
1719+
1720+
/* Shortcut things if we'd end up on this host and as the same user. */
1721+
if (user_and_machine_equivalent(user_and_machine))
1722+
return sd_bus_open_user(ret);
1723+
1724+
r = user_and_machine_valid(user_and_machine);
1725+
if (r < 0)
1726+
return r;
1727+
1728+
assert_return(r > 0, -EINVAL);
1729+
1730+
r = sd_bus_new(&b);
1731+
if (r < 0)
1732+
return r;
1733+
1734+
r = bus_set_address_machine(b, true, user_and_machine);
1735+
if (r < 0)
1736+
return r;
1737+
1738+
b->bus_client = true;
1739+
b->trusted = true;
15551740

15561741
r = sd_bus_start(b);
15571742
if (r < 0)

src/shared/bus-util.c

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,12 @@ int bus_connect_user_systemd(sd_bus **_bus) {
249249
return 0;
250250
}
251251

252-
int bus_connect_transport(BusTransport transport, const char *host, bool user, sd_bus **ret) {
252+
int bus_connect_transport(
253+
BusTransport transport,
254+
const char *host,
255+
bool user,
256+
sd_bus **ret) {
257+
253258
_cleanup_(sd_bus_close_unrefp) sd_bus *bus = NULL;
254259
int r;
255260

@@ -258,7 +263,7 @@ int bus_connect_transport(BusTransport transport, const char *host, bool user, s
258263
assert(ret);
259264

260265
assert_return((transport == BUS_TRANSPORT_LOCAL) == !host, -EINVAL);
261-
assert_return(transport == BUS_TRANSPORT_LOCAL || !user, -EOPNOTSUPP);
266+
assert_return(transport != BUS_TRANSPORT_REMOTE || !user, -EOPNOTSUPP);
262267

263268
switch (transport) {
264269

@@ -279,7 +284,10 @@ int bus_connect_transport(BusTransport transport, const char *host, bool user, s
279284
break;
280285

281286
case BUS_TRANSPORT_MACHINE:
282-
r = sd_bus_open_system_machine(&bus, host);
287+
if (user)
288+
r = sd_bus_open_user_machine(&bus, host);
289+
else
290+
r = sd_bus_open_system_machine(&bus, host);
283291
break;
284292

285293
default:
@@ -293,7 +301,6 @@ int bus_connect_transport(BusTransport transport, const char *host, bool user, s
293301
return r;
294302

295303
*ret = TAKE_PTR(bus);
296-
297304
return 0;
298305
}
299306

src/stdio-bridge/stdio-bridge.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ static int run(int argc, char *argv[]) {
121121
return log_error_errno(r, "Failed to allocate bus: %m");
122122

123123
if (arg_transport == BUS_TRANSPORT_MACHINE)
124-
r = bus_set_address_system_machine(a, arg_bus_path);
124+
r = bus_set_address_machine(a, false, arg_bus_path);
125125
else
126126
r = sd_bus_set_address(a, arg_bus_path);
127127
if (r < 0)

src/systemctl/systemctl.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
877877
assert_not_reached("Unhandled option");
878878
}
879879

880-
if (arg_transport != BUS_TRANSPORT_LOCAL && arg_scope != UNIT_FILE_SYSTEM)
880+
if (arg_transport == BUS_TRANSPORT_REMOTE && arg_scope != UNIT_FILE_SYSTEM)
881881
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
882882
"Cannot access user instance remotely.");
883883

src/systemd/sd-bus.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ int sd_bus_open(sd_bus **ret);
135135
int sd_bus_open_with_description(sd_bus **ret, const char *description);
136136
int sd_bus_open_user(sd_bus **ret);
137137
int sd_bus_open_user_with_description(sd_bus **ret, const char *description);
138+
int sd_bus_open_user_machine(sd_bus **ret, const char *machine);
138139
int sd_bus_open_system(sd_bus **ret);
139140
int sd_bus_open_system_with_description(sd_bus **ret, const char *description);
140141
int sd_bus_open_system_remote(sd_bus **ret, const char *host);

0 commit comments

Comments
 (0)