]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
varlinkctl: add 'list-sockets' verb
authorLennart Poettering <lennart@amutable.com>
Tue, 2 Jun 2026 16:53:07 +0000 (18:53 +0200)
committerLennart Poettering <lennart@amutable.com>
Tue, 23 Jun 2026 21:10:01 +0000 (23:10 +0200)
man/varlinkctl.xml
shell-completion/bash/varlinkctl
shell-completion/zsh/_varlinkctl
src/varlinkctl/varlinkctl.c
test/units/TEST-74-AUX-UTILS.varlinkctl-list-sockets.sh [new file with mode: 0755]

index 72f1983c9ef29ccfde8df373ca1bf4bf78ee9af1..539b0ea33d76dae8b348b32975d16cf154f040f0 100644 (file)
       <arg choice="req" rep="repeat"><replaceable>CMDLINE</replaceable></arg>
     </cmdsynopsis>
 
+    <cmdsynopsis>
+      <command>varlinkctl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">list-registry</arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>varlinkctl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">list-sockets</arg>
+    </cmdsynopsis>
+
     <cmdsynopsis>
       <command>varlinkctl</command>
       <arg choice="opt" rep="repeat">OPTIONS</arg>
         <xi:include href="version-info.xml" xpointer="v260"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><command>list-sockets</command></term>
+
+        <listitem><para>Shows a list of listening <constant>AF_UNIX</constant> 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
+        <varname>user.varlink</varname> extended attribute set to <literal>entrypoint</literal>.</para>
+
+        <para>Sockets are enumerated via the kernel's <constant>sock_diag</constant> netlink interface, and
+        hence this is not restricted to sockets in <filename>/run/varlink/registry/</filename> (as
+        <command>list-registry</command> 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.</para>
+
+        <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><command>validate-idl</command> [<replaceable>FILE</replaceable>]</term>
 
index 53c62dbae51354558da61bb9eff04948dc8617e2..cfde55684084455a58dc7d4b4e5e6516ef8dd275 100644 (file)
@@ -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'
index a071f6ec2efb85ff517e998d099c126b312f15d5..aa44beaeb3a21e03598a61d50b487ddb4ab3fbdd 100644 (file)
@@ -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[@]" )
index 8f580350a5fd5eb5ab635c2217812293222d8fbe..48c500f7c483bbde2c487fefcf050fc147bd6b00 100644 (file)
@@ -1,25 +1,33 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include <linux/unix_diag.h>
+#include <netinet/tcp.h>
 #include <stdlib.h>
 #include <sys/stat.h>
 #include <unistd.h>
 
 #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 (executable)
index 0000000..2097d00
--- /dev/null
@@ -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"