varlinkctl: add new serve verb to allow wrapping command in varlink
With the new protocol upgrade support in varlinkctl client we can now do the equivalent for the server side. This commit adds a new `serve` verb that will serve any command that speaks stdin/stdout via varlink and its protocol upgrade feature. This is the "inetd for varlink". This is useful for various reasons: 1. Allows to e.g. provide a heavily sandboxed io.myorg.xz.Decompress varlink endpoint, c.f. xz CVE-2024-3094) 2. Allow sftp over varlink which is quite useful with the varlink-http-bridge (that has more flexible auth mechanism than plain sftp). 3. Makes testing the varlinkctl client protocol upgrade simpler. 4. Because we can.
This commit is contained in:
committed by
Daan De Meyer
parent
2c6f9af8e5
commit
34f29079fd
@@ -73,6 +73,14 @@
|
||||
<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>
|
||||
@@ -181,6 +189,28 @@
|
||||
<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>
|
||||
|
||||
@@ -533,6 +563,46 @@ method Extend(
|
||||
<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>
|
||||
|
||||
@@ -1169,6 +1169,158 @@ static int verb_list_registry(int argc, char *argv[], uintptr_t _data, void *use
|
||||
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;
|
||||
|
||||
|
||||
@@ -257,6 +257,9 @@ systemd-run --wait --pipe --user --machine testuser@ \
|
||||
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'
|
||||
@@ -320,15 +323,6 @@ if sock:
|
||||
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"
|
||||
@@ -370,3 +364,39 @@ rm -f "$EXEC_RESULT"
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user