varlinkctl: add 'list-sockets' verb

This commit is contained in:
Lennart Poettering
2026-06-23 23:10:01 +02:00
parent daae289f10
commit 52e6c185df
5 changed files with 293 additions and 1 deletions
+29
View File
@@ -81,6 +81,18 @@
<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>
@@ -221,6 +233,23 @@
<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>
+1 -1
View 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'
+1
View 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[@]" )
+186
View 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). */
+76
View File
@@ -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"