<arg choice="plain"><replaceable>CMDLINE</replaceable></arg>
</cmdsynopsis>
+ <cmdsynopsis>
+ <command>varlinkctl</command>
+ <arg choice="opt" rep="repeat">OPTIONS</arg>
+ <arg choice="plain">serve</arg>
+ <arg choice="plain"><replaceable>METHOD</replaceable></arg>
+ <arg choice="req" rep="repeat"><replaceable>CMDLINE</replaceable></arg>
+ </cmdsynopsis>
+
<cmdsynopsis>
<command>varlinkctl</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<xi:include href="version-info.xml" xpointer="v255"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><command>serve</command> <replaceable>METHOD</replaceable> <replaceable>CMDLINE…</replaceable></term>
+
+ <listitem><para>Run a Varlink server that accepts protocol upgrade requests for the specified method
+ and connects the upgraded connection to the standard input and output of the specified command. This
+ can act as a server-side counterpart to <command>call</command> <option>--upgrade</option>.</para>
+
+ <para>The listening socket must be passed via socket activation (i.e. the
+ <varname>$LISTEN_FDS</varname> protocol), making this command suitable for use in socket-activated
+ service units. When a client calls the specified method with the upgrade flag, the server sends a
+ reply confirming the upgrade, then forks and executes the given command line with the upgraded
+ connection on its standard input and output.</para>
+
+ <para>This effectively turns any command that speaks a protocol over standard input/output into a
+ Varlink service, discoverable via the service registry and authenticated via socket credentials.
+ Because each connection is handled by a forked child process, the service unit can apply systemd's
+ sandboxing options (such as <varname>ProtectSystem=</varname>, etc.) and does not operate in the
+ caller's environment.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
<varlistentry>
<term><command>list-registry</command></term>
<programlisting># varlinkctl call ssh-exec:somehost:systemd-creds org.varlink.service.GetInfo '{}'</programlisting>
</example>
+ <example>
+ <title>Serving a Sandboxed Decompressor via Protocol Upgrade</title>
+
+ <para>The following socket and service units expose <command>xz</command> decompression as a Varlink
+ service. Clients connect and send compressed data over the upgraded connection, receiving decompressed
+ output in return.</para>
+
+ <programlisting># /etc/systemd/system/varlink-decompress-xz.socket
+[Socket]
+ListenStream=/run/varlink/registry/com.example.Decompress.XZ
+
+[Install]
+WantedBy=sockets.target
+
+# /etc/systemd/system/varlink-decompress-xz.service
+[Service]
+ExecStart=varlinkctl serve com.example.Decompress.XZ xz -d
+DynamicUser=yes
+PrivateNetwork=yes
+ProtectSystem=strict
+ProtectHome=yes
+NoNewPrivileges=yes
+SystemCallFilter=~@privileged @resources
+MemoryMax=256M</programlisting>
+
+ <para>A client can then decompress data through this service:</para>
+
+ <programlisting>$ echo "hello" | xz | varlinkctl call --upgrade \
+ unix:/run/varlink/registry/com.example.Decompress.XZ \
+ com.example.Decompress.XZ '{}'
+hello</programlisting>
+
+ <para>For quick testing without unit files, <command>systemd-socket-activate</command> can be used
+ to provide the listening socket:</para>
+
+ <programlisting>$ systemd-socket-activate -l /tmp/decompress.sock -- varlinkctl serve com.example.Decompress.XZ xz -d &
+$ echo "hello" | xz | varlinkctl call --upgrade unix:/tmp/decompress.sock com.example.Decompress.XZ '{}'
+hello</programlisting>
+ </example>
+
</refsect1>
<refsect1>
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). */
+static int varlink_server_add_interface_from_method(sd_varlink_server *s, const char *method, sd_varlink_interface **ret_interface) {
+ assert(s);
+ assert(method);
+ assert(ret_interface);
+
+ const char *dot = strrchr(method, '.');
+ assert(dot);
+
+ _cleanup_free_ char *interface_name = strndup(method, dot - method);
+ if (!interface_name)
+ return log_oom();
+
+ /* Note that we do not need to put the upgrade flag comment here, it is added automatically
+ * by varlink_idl_format_symbol() because of the SD_VARLINK_REQUIRES_UPGRADE flag. */
+ _cleanup_free_ char *idl_text = strjoin(
+ "interface ", interface_name, "\n"
+ "\n"
+ "method ", dot + 1, " () -> ()\n");
+ if (!idl_text)
+ return log_oom();
+
+ _cleanup_(sd_varlink_interface_freep) sd_varlink_interface *iface = NULL;
+ int r = sd_varlink_idl_parse(idl_text, /* reterr_line= */ NULL, /* reterr_column= */ NULL, &iface);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse IDL for method '%s': %m", method);
+
+ /* Mark the method as requiring the upgrade flag so introspection shows the annotation */
+ assert(iface->symbols[0] && iface->symbols[0]->symbol_type == SD_VARLINK_METHOD);
+ ((sd_varlink_symbol*) iface->symbols[0])->symbol_flags |= SD_VARLINK_REQUIRES_UPGRADE;
+
+ r = sd_varlink_server_add_interface(s, iface);
+ if (r < 0)
+ return r;
+
+ *ret_interface = TAKE_PTR(iface);
+
+ return 0;
+}
+
+static int method_serve_upgrade(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ char **exec_cmdline = ASSERT_PTR(userdata);
+ _cleanup_close_ int input_fd = -EBADF, _output_fd = -EBADF;
+ int output_fd, r;
+
+ if (!FLAGS_SET(flags, SD_VARLINK_METHOD_UPGRADE))
+ return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_UPGRADE, NULL);
+
+ r = sd_varlink_reply_and_upgrade(link, /* parameters= */ NULL, &input_fd, &output_fd);
+ if (r < 0)
+ return log_error_errno(r, "Failed to upgrade connection: %m");
+
+ if (output_fd != input_fd)
+ _output_fd = output_fd;
+
+ /* Copy exec_cmdline before forking: pidref_safe_fork() calls rename_process() which
+ * overwrites the argv area that exec_cmdline points into. */
+ _cleanup_strv_free_ char **cmdline_copy = strv_copy(exec_cmdline);
+ if (!cmdline_copy)
+ return log_oom();
+
+ r = pidref_safe_fork_full(
+ "(serve)",
+ (int[]) { input_fd, output_fd, STDERR_FILENO },
+ /* except_fds= */ NULL, /* n_except_fds= */ 0,
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_REARRANGE_STDIO|FORK_DETACH|FORK_LOG,
+ /* ret= */ NULL);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ execvp(cmdline_copy[0], cmdline_copy);
+ log_error_errno(errno, "Failed to execute '%s': %m", cmdline_copy[0]);
+ _exit(EXIT_FAILURE);
+ }
+
+ return 0;
+}
+
+VERB(verb_serve, "serve", "METHOD CMDLINE…", 3, VERB_ANY, 0, "Serve a command via varlink protocol upgrade");
+static int verb_serve(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ const char *method;
+ char **exec_cmdline;
+ int r, n;
+
+ assert(argc >= 3); /* Guaranteed by verb dispatch table */
+
+ method = argv[1];
+ exec_cmdline = argv + 2;
+
+ r = varlink_idl_qualified_symbol_name_is_valid(method);
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate method name '%s': %m", method);
+ if (r == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Not a valid qualified method name: '%s'", method);
+
+ /* Require socket activation */
+ n = sd_listen_fds(/* unset_environment= */ true);
+ if (n < 0)
+ return log_error_errno(n, "Failed to determine passed file descriptors: %m");
+ if (n == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No file descriptors passed via socket activation.");
+ if (n > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected exactly one socket activation fd, got %d.", n);
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get event loop: %m");
+
+ r = sd_varlink_server_new(&s, SD_VARLINK_SERVER_INHERIT_USERDATA);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate varlink server: %m");
+
+ _cleanup_free_ char *description = strjoin("serve:", method);
+ if (!description)
+ return log_oom();
+
+ r = sd_varlink_server_set_description(s, description);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set server description: %m");
+
+ r = sd_varlink_server_bind_method(s, method, method_serve_upgrade);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind method '%s': %m", method);
+
+ _cleanup_(sd_varlink_interface_freep) sd_varlink_interface *iface = NULL;
+ r = varlink_server_add_interface_from_method(s, method, &iface);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add interface for method '%s': %m", method);
+
+ sd_varlink_server_set_userdata(s, exec_cmdline);
+
+ r = sd_varlink_server_listen_fd(s, SD_LISTEN_FDS_START);
+ if (r < 0)
+ return log_error_errno(r, "Failed to listen on socket activation fd: %m");
+
+ r = sd_varlink_server_attach_event(s, event, SD_EVENT_PRIORITY_NORMAL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach varlink server to event loop: %m");
+
+ (void) sd_notify(/* unset_environment= */ false, "READY=1");
+
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run event loop: %m");
+
+ return 0;
+}
+
static int run(int argc, char *argv[]) {
int r;
varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}'
# test --upgrade (protocol upgrade)
+# The basic --upgrade proxy test is covered by the "varlinkctl serve" tests below (which use
+# serve+rev/gunzip as the server). The tests here exercise features that need the Python
+# server: file-input (defer fallback), ssh-exec transport (pipe pairs) and --exec mode.
UPGRADE_SOCKET="$(mktemp -d)/upgrade.sock"
UPGRADE_SERVER="$(mktemp)"
cat >"$UPGRADE_SERVER" <<'PYEOF'
PYEOF
chmod +x "$UPGRADE_SERVER"
-# Start the server in the background, wait for readiness via sd_notify
-systemd-notify --fork -q -- python3 "$UPGRADE_SERVER" "$UPGRADE_SOCKET"
-
-# Test proxy mode: pipe data through --upgrade, passing parameters and validate
-result="$(echo "hello world" | varlinkctl call --upgrade "unix:$UPGRADE_SOCKET" io.systemd.test.Reverse '{"foo":"bar"}')"
-echo "$result" | grep "<<< UPGRADED >>>" >/dev/null
-echo "$result" | grep '"foo": "bar"' >/dev/null
-echo "$result" | grep "dlrow olleh" >/dev/null
-
# Test --upgrade with stdin redirected from a regular file (epoll can't poll regular files,
# so this exercises the sd_event_add_defer fallback path)
UPGRADE_SOCKET2="$(mktemp -d)/upgrade.sock"
rm -f "$UPGRADE_SOCKET" "$UPGRADE_SOCKET2" "$UPGRADE_SERVER" /tmp/test-upgrade-input
rm -rf "$(dirname "$UPGRADE_SOCKET")" "$(dirname "$UPGRADE_SOCKET2")"
+
+# Test varlinkctl serve: expose a stdio command via varlink protocol upgrade with socket activation.
+# This is the "inetd for varlink" pattern: any stdio tool becomes a varlink service.
+SERVE_SOCKET="$(mktemp -d)/serve.sock"
+
+# Test 1: serve rev: proves bidirectional data flow through the upgrade
+SERVE_PID=$(systemd-notify --fork -- \
+ systemd-socket-activate -l "$SERVE_SOCKET" -- \
+ varlinkctl serve io.systemd.test.Reverse rev)
+
+# Verify introspection works on the serve endpoint and shows the upgrade annotation
+varlinkctl introspect "unix:$SERVE_SOCKET" io.systemd.test | grep "method Reverse" >/dev/null
+varlinkctl introspect "unix:$SERVE_SOCKET" io.systemd.test | grep "Requires 'upgrade' flag" >/dev/null
+
+result="$(echo "hello world" | varlinkctl call --upgrade "unix:$SERVE_SOCKET" io.systemd.test.Reverse '{}')"
+echo "$result" | grep "dlrow olleh" >/dev/null
+kill "$SERVE_PID" 2>/dev/null || true
+wait "$SERVE_PID" 2>/dev/null || true
+rm -f "$SERVE_SOCKET"
+
+# Test 2: decompress via serve: the "sandboxed decompressor" use-case (the real thing would be a proper
+# unit with real sandboxing).
+# Pipe gzip-compressed data through a varlinkctl serve + gunzip endpoint and verify round-trip.
+SERVE_PID=$(systemd-notify --fork -- \
+ systemd-socket-activate -l "$SERVE_SOCKET" -- \
+ varlinkctl serve io.systemd.Compress.Decompress gunzip)
+
+SERVE_TMPDIR="$(mktemp -d)"
+echo "untrusted data decompressed safely via varlink serve" | gzip > "$SERVE_TMPDIR/compressed.gz"
+result="$(varlinkctl call --upgrade "unix:$SERVE_SOCKET" io.systemd.Compress.Decompress '{}' < "$SERVE_TMPDIR/compressed.gz")"
+echo "$result" | grep "untrusted data decompressed safely" >/dev/null
+kill "$SERVE_PID" 2>/dev/null || true
+wait "$SERVE_PID" 2>/dev/null || true
+
+rm -f "$SERVE_SOCKET"
+rm -rf "$(dirname "$SERVE_SOCKET")" "$SERVE_TMPDIR"