diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md index cf83f19f22..c4e187c666 100644 --- a/docs/USER_GROUP_API.md +++ b/docs/USER_GROUP_API.md @@ -327,6 +327,37 @@ Services which are asked for enumeration may return the `EnumerationNotSupported And that's really all there is to it. +## Reducing IPC Requests + +In many cases backends are only interested in responding to certain UID/GID +ranges, or only to user/group names matching a specific pattern. To optimize +look-ups in this case it is possible to indicate on the Varlink entrypoint +socket inode of the service which ranges or patterns these are: if the kernel +supports extended attributes on socket inodes (Linux >= 7.1), the following +extended attributes may be set: + +* `user.userdb.uid` takes a comma separated list of UIDs or dash-separated UID + range pairs. If set, then the service will not be contacted for any UID + look-ups outside of this range. Example: setting the extended attribute to + `61184-65519,524288-1879048191,4711` will have the effect that a lookup for + UID 1051 will not be attempted against this service, but one for 62334 will be. + +* `user.userdb.gid` is similar, but for GIDs and GID ranges. + +* `user.userdb.username` takes a NUL separated list of `fnmatch()` style glob + expressions. If set only look-ups for user names matching at least one of + these expressions will be routed to this service. Example: setting the + extended attribute to `foo-*\0www*h?` (where `\0` shall indicate the NUL + byte) will route only look-ups for `foo-bar` and `wwwooosh7` to this service, + but not `waldo`. + +* `user.userdb.groupname` is similar, but for group names. + +Note that these extended attributes are intended for optimization only: any +backend *must* be prepared to answer look-ups for any UID, GID, user and group +name, and respond with `NoRecordFound` for requests outside of the +ranges/patterns defined this way. + ## Command Line Access For command line access you can use the diff --git a/src/core/varlink.c b/src/core/varlink.c index fa6a5e1565..79a198f14d 100644 --- a/src/core/varlink.c +++ b/src/core/varlink.c @@ -4,12 +4,14 @@ #include "constants.h" #include "errno-util.h" +#include "format-util.h" #include "job.h" #include "json-util.h" #include "manager.h" #include "metrics.h" #include "path-util.h" #include "pidref.h" +#include "stdio-util.h" #include "string-util.h" #include "strv.h" #include "unit.h" @@ -27,6 +29,7 @@ #include "varlink-serialize.h" #include "varlink-unit.h" #include "varlink-util.h" +#include "xattr-util.h" static const char* const managed_oom_mode_properties[] = { "ManagedOOMSwap", @@ -481,10 +484,14 @@ int manager_setup_varlink_metrics_server(Manager *m) { if (MANAGER_IS_SYSTEM(m)) flags |= SD_VARLINK_SERVER_ACCOUNT_UID; - return metrics_setup_varlink_server(&m->metrics_varlink_server, flags, - m->event, EVENT_PRIORITY_IPC, - vl_method_list_metrics, vl_method_describe_metrics, - m); + return metrics_setup_varlink_server( + &m->metrics_varlink_server, + flags, + m->event, + EVENT_PRIORITY_IPC, + vl_method_list_metrics, + vl_method_describe_metrics, + m); } static int varlink_server_listen_many_idempotent_sentinel( @@ -549,6 +556,12 @@ static int manager_varlink_init_system_api(Manager *m) { VARLINK_PATH_MANAGED_OOM_SYSTEM); if (r < 0) return r; + + /* Reduce the noise routed to us: advertise that only look-ups for the dynamic UID range should go to us */ + char text[DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(gid_t) + 1]; + xsprintf(text, UID_FMT "-" UID_FMT, (uid_t) DYNAMIC_UID_MIN, (uid_t) DYNAMIC_UID_MAX); + FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid") + (void) xsetxattr(AT_FDCWD, "/run/systemd/userdb/io.systemd.DynamicUser", /* at_flags= */ 0, xattr, text); } return 0; diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c index b949c4b8fd..a19dc3f6c8 100644 --- a/src/home/homed-manager.c +++ b/src/home/homed-manager.c @@ -65,6 +65,7 @@ #include "varlink-io.systemd.UserDatabase.h" #include "varlink-io.systemd.service.h" #include "varlink-util.h" +#include "xattr-util.h" /* Where to look for private/public keys that are used to sign the user records. We are not using * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that @@ -1102,6 +1103,13 @@ static int manager_bind_varlink(Manager *m) { if (r < 0) return log_error_errno(r, "Failed to bind to varlink socket '%s': %m", socket_path); + /* Reduce the noise routed to us: advertise that only look-ups for non-system users in the 16bit + * range are routed to us (excluding 'nobody', i.e. 0xfffe) */ + char text[DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(gid_t) + 1]; + xsprintf(text, UID_FMT "-" UID_FMT, (uid_t) (SYSTEM_UID_MAX + 1), UID_NOBODY - 1); + FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid") + (void) xsetxattr(AT_FDCWD, socket_path, /* at_flags= */ 0, xattr, text); + r = sd_varlink_server_attach_event(m->varlink_server, m->event, SD_EVENT_PRIORITY_NORMAL); if (r < 0) return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); diff --git a/src/machine/machined-varlink.c b/src/machine/machined-varlink.c index acc2137f83..b136ccf2a2 100644 --- a/src/machine/machined-varlink.c +++ b/src/machine/machined-varlink.c @@ -31,6 +31,7 @@ #include "varlink-io.systemd.Resolve.Hook.h" #include "varlink-io.systemd.service.h" #include "varlink-util.h" +#include "xattr-util.h" typedef struct LookupParameters { const char *user_name; @@ -774,6 +775,14 @@ static int manager_varlink_init_userdb(Manager *m) { if (r < 0) return log_error_errno(r, "Failed to bind to varlink socket %s: %m", VARLINK_PATH_MACHINED_USERDB); + /* Reduce the noise routed to us: advertise that only look-ups for the higher UID range */ + char text[DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(gid_t) + 1]; + xsprintf(text, UID_FMT "-" UID_FMT, (uid_t) 0x10000U, (uid_t) UINT32_C(0xfffffffe)); + FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid") + (void) xsetxattr(AT_FDCWD, VARLINK_PATH_MACHINED_USERDB, /* at_flags= */ 0, xattr, text); + (void) xsetxattr(AT_FDCWD, VARLINK_PATH_MACHINED_USERDB, /* at_flags= */ 0, "user.userdb.username", "vu-*"); + (void) xsetxattr(AT_FDCWD, VARLINK_PATH_MACHINED_USERDB, /* at_flags= */ 0, "user.userdb.groupname", "vg-*"); + r = sd_varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL); if (r < 0) return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); diff --git a/src/nsresourced/nsresourced-manager.c b/src/nsresourced/nsresourced-manager.c index 4e671de833..df12e352b0 100644 --- a/src/nsresourced/nsresourced-manager.c +++ b/src/nsresourced/nsresourced-manager.c @@ -33,10 +33,12 @@ #include "string-util.h" #include "strv.h" #include "time-util.h" +#include "uid-classification.h" #include "umask-util.h" #include "unaligned.h" #include "userns-registry.h" #include "userns-restrict.h" +#include "xattr-util.h" #define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC) @@ -450,6 +452,16 @@ static int manager_make_listen_socket(Manager *m) { if (r < 0) return log_error_errno(r, "Failed to symlink userdb socket: %m"); + /* Reduce the noise routed to us: advertise that only look-ups for the higher UID range */ + char text[2 * (DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(uid_t) + 1)]; + xsprintf(text, UID_FMT "-" UID_FMT "," UID_FMT "-" UID_FMT, + CONTAINER_UID_MIN, CONTAINER_UID_MAX, + (uid_t) DYNAMIC_UID_MIN, (uid_t) DYNAMIC_UID_MAX); + FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid") + (void) xsetxattr(AT_FDCWD, sockaddr.un.sun_path, /* at_flags= */ 0, xattr, text); + (void) xsetxattr(AT_FDCWD, sockaddr.un.sun_path, /* at_flags= */ 0, "user.userdb.username", "ns-*"); + (void) xsetxattr(AT_FDCWD, sockaddr.un.sun_path, /* at_flags= */ 0, "user.userdb.groupname", "ns-*"); + if (listen(m->listen_fd, SOMAXCONN) < 0) return log_error_errno(errno, "Failed to listen on socket: %m"); diff --git a/src/shared/userdb.c b/src/shared/userdb.c index 14848150c5..7dd9ac7496 100644 --- a/src/shared/userdb.c +++ b/src/shared/userdb.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include @@ -18,6 +19,7 @@ #include "log.h" #include "parse-util.h" #include "set.h" +#include "socket-util.h" #include "string-util.h" #include "strv.h" #include "uid-classification.h" @@ -25,6 +27,7 @@ #include "user-util.h" #include "userdb.h" #include "userdb-dropin.h" +#include "xattr-util.h" DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(link_hash_ops, void, trivial_hash_func, trivial_compare_func, sd_varlink, sd_varlink_unref); @@ -35,6 +38,14 @@ typedef enum LookupWhat { _LOOKUP_WHAT_MAX, } LookupWhat; +typedef struct SocketFilter { + /* Restricts the sockets to connect to */ + uid_t uid; + gid_t gid; + const char *username; + const char *groupname; +} SocketFilter; + struct UserDBIterator { LookupWhat what; UserDBFlags flags; @@ -505,11 +516,138 @@ static int userdb_connect( return r; } +static int socket_filter_id_test( + id_t id, + const char *xattr, + int dir_fd, + const char *fname, + const char *full_path /* for logging only */) { + + int r; + + assert(xattr); + assert(dir_fd >= 0); + assert(fname); + assert(full_path); + + /* Generic implementation for both UIDs and GIDs */ + + if (!uid_is_valid(id)) + return true; + + /* Looks for an xattr of the specified name on the referenced socket entrypoint inode. Parses this + * as a pair of start UID/end UID. Matches the specified UID against it. If it matches yay. If it + * doesn't nay. If no UID is specified or no xattr set, also yay. */ + + _cleanup_free_ char *t = NULL; + r = getxattr_at_malloc(dir_fd, fname, xattr, AT_SYMLINK_FOLLOW, &t, /* ret_size= */ NULL); + if (r == -ENODATA) + return true; /* no such xattr? match! */ + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) { + log_debug_errno(r, "Extended attributes on sockets not supported after all, ignoring: %m"); + return true; /* if sockets don't support xattrs here, then this is a match */ + } + if (r < 0) + return log_debug_errno(r, "Unexpected error while reading extended attribute '%s' of '%s', ignoring: %m", xattr, full_path); + + _cleanup_strv_free_ char **l = strv_split(t, ","); + if (!l) + return log_oom_debug(); + + STRV_FOREACH(i, l) { + /* Single UID or UID range */ + uid_t x, y; + r = parse_uid_range(*i, &x, &y); + if (r < 0) + return log_debug_errno(r, "Malformed extended attribute '%s' on '%s', ignoring.", xattr, full_path); + + if (id >= x && id <= y) + return true; + } + + return false; +} + +static int socket_filter_name_test( + const char *name, + const char *xattr, + int dir_fd, + const char *fname, + const char *full_path /* for logging only */) { + + int r; + + assert(xattr); + assert(dir_fd >= 0); + assert(fname); + assert(full_path); + + /* Looks for an xattr of the specified name on the referenced socket entrypoint inode. Parses this + * as a NUL terminated string list. Matches the specified name against it via fnmatch(). If it matches + * yay. If it doesn't nay. If no name is specified or no xattr set, also yay. */ + + if (!name) + return true; + + _cleanup_strv_free_ char **l = NULL; + r = getxattr_at_strv(dir_fd, fname, xattr, AT_SYMLINK_FOLLOW, &l); + if (r == -ENODATA) + return true; /* no such xattr? match! */ + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) { + log_debug_errno(r, "Extended attributes on sockets not supported after all, ignoring: %m"); + return true; /* if sockets don't support xattrs here, then this is a match */ + } + if (r < 0) + return log_debug_errno(r, "Unexpected error while reading extended attribute '%s' of '%s', ignoring: %m", xattr, full_path); + + return strv_fnmatch_full(l, name, FNM_NOESCAPE, /* ret_matched_pos= */ NULL); +} + +static int socket_filter_test( + const SocketFilter *sfilter, + int dir_fd, + const char *fname, + const char *full_path /* for logging only */) { + + assert(dir_fd >= 0); + assert(fname); + assert(full_path); + + /* Before we contact a Varlink service, let's see if there's any chance it might answer our questions + * at all. For such pre-filtering a userdb backend can install xattrs on their entrypoint inode to + * indicate that it's never going to answer outside of a certain UID/GID range or username/groupname + * pattern */ + + /* No filter means all sockets match */ + if (!sfilter) + return 1; + + /* No xattr support? then all sockets match. This check mostly exists to take benefit of the internal caching + * of the test. Either way we'll check below again. */ + if (socket_xattr_supported() == 0) + return 1; + + if (socket_filter_id_test(sfilter->uid, "user.userdb.uid", dir_fd, fname, full_path) == 0) + return 0; + + if (socket_filter_id_test(sfilter->gid, "user.userdb.gid", dir_fd, fname, full_path) == 0) + return 0; + + if (socket_filter_name_test(sfilter->username, "user.userdb.username", dir_fd, fname, full_path) == 0) + return 0; + + if (socket_filter_name_test(sfilter->groupname, "user.userdb.groupname", dir_fd, fname, full_path) == 0) + return 0; + + return 1; +} + static int userdb_start_query( UserDBIterator *iterator, const char *method, /* must be a static string, we are not going to copy this here! */ bool more, sd_json_variant *query, + const SocketFilter *sfilter, UserDBFlags flags) { _cleanup_strv_free_ char **except = NULL, **only = NULL; @@ -601,6 +739,12 @@ static int userdb_start_query( if (!p) return -ENOMEM; + r = socket_filter_test(sfilter, dirfd(d), de->d_name, p); + if (r < 0) + log_debug_errno(r, "Failed to filter sockets, ignoring: %m"); + else if (r == 0) + continue; + r = userdb_connect(iterator, p, method, more, query); if (is_nss && r >= 0) /* Turn off fallback NSS + dropin if we found the NSS/dropin service * and could connect to it */ @@ -928,8 +1072,20 @@ int userdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flags if (!iterator) return -ENOMEM; + SocketFilter sfilter = { + .uid = UID_INVALID, + .gid = GID_INVALID, + .username = name, + }; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; - r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ false, query, flags); + r = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetUserRecord", + /* more= */ false, + query, + &sfilter, + flags); if (r >= 0) { r = userdb_process(iterator, &ur, /* ret_group_record= */ NULL, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); if (r == -ENOEXEC) /* Found a user matching UID or name, but not filter. In this case the @@ -1018,8 +1174,19 @@ int userdb_by_uid(uid_t uid, const UserDBMatch *match, UserDBFlags flags, UserRe if (!iterator) return -ENOMEM; + SocketFilter sfilter = { + .uid = uid, + .gid = GID_INVALID, + }; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; - r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ false, query, flags); + r = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetUserRecord", + /* more= */ false, + query, + &sfilter, + flags); if (r >= 0) { r = userdb_process(iterator, &ur, /* ret_group_record= */ NULL, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); if (r == -ENOEXEC) @@ -1055,7 +1222,13 @@ int userdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **ret if (!iterator) return -ENOMEM; - qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ true, query, flags); + qr = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetUserRecord", + /* more= */ true, + query, + /* sfilter= */ NULL, + flags); if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) { r = userdb_iterator_block_nss_systemd(iterator); @@ -1384,8 +1557,20 @@ int groupdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flag if (!iterator) return -ENOMEM; + SocketFilter sfilter = { + .uid = UID_INVALID, + .gid = GID_INVALID, + .groupname = name, + }; + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; - r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ false, query, flags); + r = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetGroupRecord", + /* more= */ false, + query, + &sfilter, + flags); if (r >= 0) { r = userdb_process(iterator, /* ret_user_record= */ NULL, &gr, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); if (r == -ENOEXEC) @@ -1418,13 +1603,13 @@ static int groupdb_by_gid_fallbacks( assert(iterator); assert(ret); - if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !(iterator && iterator->dropin_covered)) { + if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !iterator->dropin_covered) { r = dropin_group_record_by_gid(gid, NULL, flags, ret); if (r >= 0) return r; } - if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && !(iterator && iterator->nss_covered)) { + if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && !iterator->nss_covered) { r = userdb_iterator_block_nss_systemd(iterator); if (r >= 0) { r = nss_group_record_by_gid(gid, !FLAGS_SET(flags, USERDB_SUPPRESS_SHADOW), ret); @@ -1470,8 +1655,19 @@ int groupdb_by_gid(gid_t gid, const UserDBMatch *match, UserDBFlags flags, Group if (!iterator) return -ENOMEM; + SocketFilter sfilter = { + .uid = UID_INVALID, + .gid = gid, + }; + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; - r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ false, query, flags); + r = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetGroupRecord", + /* more= */ false, + query, + &sfilter, + flags); if (r >= 0) { r = userdb_process(iterator, /* ret_user_record= */ NULL, &gr, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); if (r == -ENOEXEC) @@ -1507,7 +1703,13 @@ int groupdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **re if (!iterator) return -ENOMEM; - qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ true, query, flags); + qr = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetGroupRecord", + /* more= */ true, + query, + /* sfilter= */ NULL, + flags); if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) { r = userdb_iterator_block_nss_systemd(iterator); @@ -1698,7 +1900,13 @@ int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **r if (!iterator->filter_user_name) return -ENOMEM; - qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags); + qr = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetMemberships", + /* more= */ true, + query, + /* sfilter= */ NULL, + flags); if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) { r = userdb_iterator_block_nss_systemd(iterator); @@ -1743,7 +1951,13 @@ int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator ** if (!iterator->filter_group_name) return -ENOMEM; - qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags); + qr = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetMemberships", + /* more= */ true, + query, + /* sfilter= */ NULL, + flags); if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) { _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; @@ -1789,7 +2003,13 @@ int membershipdb_all(UserDBFlags flags, UserDBIterator **ret) { if (!iterator) return -ENOMEM; - qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, NULL, flags); + qr = userdb_start_query( + iterator, + "io.systemd.UserDatabase.GetMemberships", + /* more= */ true, + /* query= */ NULL, + /* sfilter= */ NULL, + flags); if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) { r = userdb_iterator_block_nss_systemd(iterator); diff --git a/units/meson.build b/units/meson.build index ca50475ebe..c07e5712b0 100644 --- a/units/meson.build +++ b/units/meson.build @@ -1040,7 +1040,7 @@ units = [ 'conditions' : ['ENABLE_NSRESOURCED'], }, { - 'file' : 'systemd-nsresourced.socket', + 'file' : 'systemd-nsresourced.socket.in', 'conditions' : ['ENABLE_NSRESOURCED'], }, { diff --git a/units/systemd-nsresourced.socket b/units/systemd-nsresourced.socket.in similarity index 72% rename from units/systemd-nsresourced.socket rename to units/systemd-nsresourced.socket.in index 2840be43e2..3798b9a1e4 100644 --- a/units/systemd-nsresourced.socket +++ b/units/systemd-nsresourced.socket.in @@ -22,6 +22,10 @@ FileDescriptorName=varlink SocketMode=0666 XAttrEntryPoint=user.varlink=entrypoint XAttrListen=user.varlink=listen +XAttrEntryPoint=user.userdb.uid={{DYNAMIC_UID_MIN}}-{{DYNAMIC_UID_MAX}},{{CONTAINER_UID_BASE_MIN}}-{{CONTAINER_UID_BASE_MAX+65535}} +XAttrEntryPoint=user.userdb.gid={{DYNAMIC_UID_MIN}}-{{DYNAMIC_UID_MAX}},{{CONTAINER_UID_BASE_MIN}}-{{CONTAINER_UID_BASE_MAX+65535}} +XAttrEntryPoint=user.userdb.username=ns-* +XAttrEntryPoint=user.userdb.groupname=ns-* [Install] WantedBy=sockets.target