From: Lennart Poettering Date: Tue, 2 Jun 2026 16:53:07 +0000 (+0200) Subject: varlinkctl: add 'list-sockets' verb X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=52e6c185df3d44cfc4dc62d57d0502a19fb43112;p=thirdparty%2Fsystemd.git varlinkctl: add 'list-sockets' verb --- diff --git a/man/varlinkctl.xml b/man/varlinkctl.xml index 72f1983c9ef..539b0ea33d7 100644 --- a/man/varlinkctl.xml +++ b/man/varlinkctl.xml @@ -81,6 +81,18 @@ CMDLINE + + varlinkctl + OPTIONS + list-registry + + + + varlinkctl + OPTIONS + list-sockets + + varlinkctl OPTIONS @@ -221,6 +233,23 @@ + + list-sockets + + Shows a list of listening AF_UNIX stream sockets on the local + system that are marked as Varlink entrypoints, along with whether the calling user has write (i.e. + connect) access to each of them. A socket is considered a Varlink entrypoint if its inode carries the + user.varlink extended attribute set to entrypoint. + + Sockets are enumerated via the kernel's sock_diag netlink interface, and + hence this is not restricted to sockets in /run/varlink/registry/ (as + list-registry is), but covers all file-system bound listening Varlink sockets, + regardless of their location. This requires kernel support for extended attributes on socket inodes, + which is available since Linux 7.0. + + + + validate-idl [FILE] diff --git a/shell-completion/bash/varlinkctl b/shell-completion/bash/varlinkctl index 53c62dbae51..cfde5568408 100644 --- a/shell-completion/bash/varlinkctl +++ b/shell-completion/bash/varlinkctl @@ -63,7 +63,7 @@ _varlinkctl() { fi local -A VERBS=( - [STANDALONE]='help list-registry' + [STANDALONE]='help list-registry list-sockets' [CALL]='call' [FILE]='info list-interfaces validate-idl' [ADDRESS_INTERFACES]='list-methods introspect' diff --git a/shell-completion/zsh/_varlinkctl b/shell-completion/zsh/_varlinkctl index a071f6ec2ef..aa44beaeb3a 100644 --- a/shell-completion/zsh/_varlinkctl +++ b/shell-completion/zsh/_varlinkctl @@ -34,6 +34,7 @@ _regex_words varlink-commands 'varlink command' \ 'introspect:show an interface definition:$varlink_interface' \ 'call:invoke a method:$varlink_call' \ 'validate-idl:validate an interface description:$varlink_idl' \ + 'list-sockets:List listening Varlink entrypoint sockets' \ 'help:show a help message' local -a varlinkcmd=( /$'[^\0]#\0'/ "$reply[@]" ) diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c index 8f580350a5f..48c500f7c48 100644 --- a/src/varlinkctl/varlinkctl.c +++ b/src/varlinkctl/varlinkctl.c @@ -1,25 +1,33 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include +#include #include #include #include #include "sd-daemon.h" +#include "sd-netlink.h" #include "sd-varlink.h" #include "build.h" #include "bus-util.h" #include "chase.h" +#include "devnum-util.h" #include "env-util.h" +#include "errno-list.h" +#include "errno-util.h" #include "escape.h" #include "fd-util.h" #include "fileio.h" #include "format-table.h" #include "format-util.h" +#include "fs-util.h" #include "help-util.h" #include "log.h" #include "main-func.h" #include "memfd-util.h" +#include "netlink-sock-diag.h" #include "options.h" #include "pager.h" #include "parse-argument.h" @@ -33,6 +41,7 @@ #include "recurse-dir.h" #include "runtime-scope.h" #include "socket-forward.h" +#include "socket-util.h" #include "string-util.h" #include "strv.h" #include "terminal-util.h" @@ -41,6 +50,7 @@ #include "varlink-util.h" #include "verbs.h" #include "version.h" +#include "xattr-util.h" typedef struct PushFds { int *fds; @@ -1153,6 +1163,182 @@ static int verb_list_registry(int argc, char *argv[], uintptr_t _data, void *use return 0; } +VERB_NOARG(verb_list_sockets, "list-sockets", "List listening Varlink entrypoint sockets"); +static int verb_list_sockets(int argc, char *argv[], uintptr_t _data, void *userdata) { + int r; + + assert(argc <= 1); + + /* Enumerates listening, file-system bound AF_UNIX SOCK_STREAM sockets via the sock_diag netlink API, + * and lists those that are marked as Varlink entrypoints (i.e. carry the "user.varlink" xattr set to + * "entrypoint"). */ + + r = socket_xattr_supported(); + if (r < 0) + return log_error_errno(r, "Failed to check if S_IFSOCK inodes support xattrs: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "This kernel does not support extended attributes on socket inodes, cannot enumerate Varlink sockets."); + + _cleanup_(sd_netlink_unrefp) sd_netlink *nl = NULL; + r = sd_sock_diag_socket_open(&nl); + if (r < 0) + return log_error_errno(r, "Failed to open sock_diag netlink socket: %m"); + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL; + r = sd_sock_diag_message_new_unix_dump(nl, &req, 1U << TCP_LISTEN, UDIAG_SHOW_NAME|UDIAG_SHOW_VFS); + if (r < 0) + return log_error_errno(r, "Failed to allocate AF_UNIX socket dump request: %m"); + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *reply = NULL; + r = sd_netlink_call(nl, req, /* timeout= */ 0, &reply); + if (r < 0) + return log_error_errno(r, "Failed to issue AF_UNIX socket dump: %m"); + + _cleanup_(table_unrefp) Table *table = table_new("path", "access"); + if (!table) + return log_oom(); + + (void) table_set_sort(table, (size_t) 0); + + for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) { + + r = sd_netlink_message_get_errno(m); + if (r < 0) { + log_warning_errno(r, "Error in AF_UNIX socket dump entry, ignoring: %m"); + continue; + } + + struct unix_diag_msg udm; + r = sd_sock_diag_message_get_unix(m, &udm); + if (r < 0) { + log_warning_errno(r, "Failed to read AF_UNIX socket dump header, ignoring: %m"); + continue; + } + + /* We only care about listening stream sockets. The kernel already filtered by state, but + * there's no way to filter by type in the request, so we do that here (and double check the + * state for good measure). */ + if (udm.udiag_type != SOCK_STREAM) + continue; + if (udm.udiag_state != TCP_LISTEN) + continue; + + /* Read the bound name. This is not NUL terminated on the wire, hence read it as raw data. */ + _cleanup_free_ void *name = NULL; + size_t name_size = 0; + r = sd_netlink_message_read_data(m, UNIX_DIAG_NAME, &name_size, &name); + if (r == -ENODATA) /* unnamed socket */ + continue; + if (r < 0) + return log_error_errno(r, "Failed to read AF_UNIX socket name: %m"); + + /* Safely turn the raw, not necessarily NUL-terminated, name into a C string. This also + * rejects any name with embedded NUL bytes. */ + _cleanup_free_ char *path = NULL; + r = make_cstring(name, name_size, MAKE_CSTRING_ALLOW_TRAILING_NUL, &path); + if (r < 0) { + log_debug_errno(r, "Failed to convert AF_UNIX socket name to string, skipping: %m"); + continue; + } + if (!path_is_absolute(path)) { + log_debug("Got non-absolute AF_UNIX socket path '%s', skipping.", path); + continue; + } + + /* The kernel also reports the backing VFS inode/device, but only for file-system bound + * sockets. We require it, both as a filter and to validate the path below. */ + _cleanup_free_ void *vfs = NULL; + size_t vfs_size = 0; + r = sd_netlink_message_read_data(m, UNIX_DIAG_VFS, &vfs_size, &vfs); + if (r == -ENODATA) + continue; /* not fs bound */ + if (r < 0) + return log_error_errno(r, "Failed to read AF_UNIX socket VFS data: %m"); + if (vfs_size != sizeof(struct unix_diag_vfs)) { + log_warning("Got AF_UNIX socket VFS data of unexpected size, skipping."); + continue; + } + const struct unix_diag_vfs *uv = vfs; + + /* Validate the path the kernel reported: open it (without following a final-component + * symlink), and verify it really is a socket whose inode and backing device match what + * netlink told us. This guards against the path having been unlinked/replaced in the + * meantime, so that we only ever read xattrs off the right inode. */ + _cleanup_close_ int fd = open(path, O_PATH|O_CLOEXEC|O_NOFOLLOW); + if (fd < 0) { + log_debug_errno(errno, "Failed to open reported AF_UNIX socket path '%s', skipping: %m", path); + continue; + } + + struct stat st; + if (fstat(fd, &st) < 0) { + log_debug_errno(errno, "Failed to stat reported AF_UNIX socket path '%s', skipping: %m", path); + continue; + } + + if (!S_ISSOCK(st.st_mode)) { + log_debug("Reported AF_UNIX socket path '%s' is not a socket, skipping.", path); + continue; + } + + /* the unix_diag_vfs structure only gives us 32bit inode numbers, which it truncates. hence lets truncate the value before comparison */ + if (((st.st_ino ^ uv->udiag_vfs_ino) & UINT32_MAX) != 0) { + log_debug("Inode of reported AF_UNIX socket path '%s' does not match netlink data, skipping.", path); + continue; + } + + /* udiag_vfs_dev carries the kernel-internal dev_t encoding, which differs from the userspace + * dev_t in st_dev — hence translate before comparing. */ + if (STAT_DEV_TO_KERNEL(st.st_dev) != uv->udiag_vfs_dev) { + log_debug("Backing device of reported AF_UNIX socket path '%s' does not match netlink data, skipping.", path); + continue; + } + + /* The path is validated now, hence we may safely read the Varlink role xattr off the fd. We + * only list sockets that are marked as Varlink entrypoints. */ + _cleanup_free_ char *role = NULL; + r = fgetxattr_malloc(fd, "user.varlink", &role, /* ret_size= */ NULL); + if (r < 0) { + if (!ERRNO_IS_NEG_XATTR_ABSENT(r)) + log_debug_errno(r, "Failed to read 'user.varlink' xattr of '%s', skipping: %m", path); + continue; + } + if (!streq(role, "entrypoint")) + continue; + + _cleanup_free_ char *no = NULL; + r = access_fd(fd, W_OK); + if (r < 0) { + no = strjoin("no (", ERRNO_NAME(r), ")"); + if (!no) + return log_oom(); + } + + r = table_add_many( + table, + TABLE_PATH, path, + TABLE_STRING, no ?: "yes", + TABLE_SET_COLOR, ansi_highlight_green_red(!no)); + if (r < 0) + return r; + } + + if (!table_isempty(table) || sd_json_format_enabled(arg_json_format_flags)) { + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + if (arg_legend && !sd_json_format_enabled(arg_json_format_flags)) { + if (table_isempty(table)) + printf("No sockets found.\n"); + else + printf("\n%zu entrypoint sockets listed.\n", table_get_rows(table) - 1); + } + + return 0; +} + /* Build a minimal IDL from a qualified method name so that introspection works. The parsed interface is * returned to the caller who must keep it alive for the lifetime of the server * (sd_varlink_server_add_interface() borrows the pointer). */ diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-list-sockets.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-list-sockets.sh new file mode 100755 index 00000000000..2097d00fb1a --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-list-sockets.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# "varlinkctl list-sockets" relies on extended attributes on socket inodes, which +# require a sufficiently new kernel. Probe for actual support and skip if +# unavailable, since the kernel version alone is not a reliable indicator. +if ! socket_inode_supports_user_xattrs; then + echo "Socket inode extended attributes unsupported on this kernel, skipping." >&2 + exit 0 +fi + +ENTRYPOINT_PATH="/run/test-list-sockets-entrypoint.sock" +PLAIN_PATH="/run/test-list-sockets-plain.sock" + +at_exit() { + set +e + systemctl stop test-list-sockets-entrypoint.socket + systemctl reset-failed test-list-sockets-entrypoint.socket test-list-sockets-entrypoint.service + systemctl stop test-list-sockets-plain.socket + systemctl reset-failed test-list-sockets-plain.socket test-list-sockets-plain.service + rm -f "$ENTRYPOINT_PATH" "$PLAIN_PATH" +} +trap at_exit EXIT + +rm -f "$ENTRYPOINT_PATH" "$PLAIN_PATH" + +# A listening socket tagged as a Varlink entrypoint. +systemd-run \ + --unit=test-list-sockets-entrypoint \ + --service-type=oneshot \ + --remain-after-exit \ + --socket-property=ListenStream="$ENTRYPOINT_PATH" \ + --socket-property=SocketMode=0666 \ + --socket-property=XAttrEntryPoint=user.varlink=entrypoint \ + --socket-property=RemoveOnStop=true \ + true + +# A listening socket *without* the entrypoint marker, which must be ignored. +systemd-run \ + --unit=test-list-sockets-plain \ + --service-type=oneshot \ + --remain-after-exit \ + --socket-property=ListenStream="$PLAIN_PATH" \ + --socket-property=SocketMode=0666 \ + --socket-property=RemoveOnStop=true \ + true + +test -S "$ENTRYPOINT_PATH" +test -S "$PLAIN_PATH" + +# Plain text output should run cleanly and mention the entrypoint socket. +varlinkctl list-sockets +varlinkctl list-sockets | grep "$ENTRYPOINT_PATH" >/dev/null + +# JSON output is an array of {path, access} objects. The entrypoint socket must be +# present... +json="$(varlinkctl --json=short list-sockets)" +echo "$json" | jq -e --arg p "$ENTRYPOINT_PATH" 'any(.[]; .path == $p)' >/dev/null +# ...and carry an "access" field (either "yes" or "No (…)"). +echo "$json" | jq -e --arg p "$ENTRYPOINT_PATH" 'any(.[]; .path == $p and (.access | type == "string"))' >/dev/null + +# The socket without the entrypoint xattr must NOT be listed. +(! echo "$json" | jq -e --arg p "$PLAIN_PATH" 'any(.[]; .path == $p)' >/dev/null) + +# Stopping the socket unit must make the entrypoint disappear from the listing again. +systemctl stop test-list-sockets-entrypoint.socket +test ! -S "$ENTRYPOINT_PATH" +(! varlinkctl --json=short list-sockets | jq -e --arg p "$ENTRYPOINT_PATH" 'any(.[]; .path == $p)' >/dev/null) + +systemctl stop test-list-sockets-plain.socket +test ! -S "$PLAIN_PATH"