Merge pull request #30837 from poettering/varlink-over-ssh
varlink: add "ssh:" transport for talking to remote servers
This commit is contained in:
@@ -610,3 +610,9 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \
|
||||
latter two via the environment variable unless `systemd-storagetm` is invoked
|
||||
to expose a single device only, since those identifiers better should be kept
|
||||
unique.
|
||||
|
||||
Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as
|
||||
`busctl`):
|
||||
|
||||
* `$SYSTEMD_SSH` – the ssh binary to invoke when the `ssh:` transport is
|
||||
used. May be a filename (which is searched for in `$PATH`) or absolute path.
|
||||
|
||||
+6
-1
@@ -71,11 +71,16 @@
|
||||
|
||||
<itemizedlist>
|
||||
<listitem><para>A Varlink service reference starting with the <literal>unix:</literal> string, followed
|
||||
by an absolute <constant>AF_UNIX</constant> path, or by <literal>@</literal> and an arbitrary string
|
||||
by an absolute <constant>AF_UNIX</constant> socket path, or by <literal>@</literal> and an arbitrary string
|
||||
(the latter for referencing sockets in the abstract namespace).</para></listitem>
|
||||
|
||||
<listitem><para>A Varlink service reference starting with the <literal>exec:</literal> string, followed
|
||||
by an absolute path of a binary to execute.</para></listitem>
|
||||
|
||||
<listitem><para>A Varlink service reference starting with the <literal>ssh:</literal> string, followed
|
||||
by an SSH host specification, followed by <literal>:</literal>, followed by an absolute
|
||||
<constant>AF_UNIX</constant> socket path. (This requires OpenSSH 9.4 or newer on the server side,
|
||||
abstract namespace sockets are not supported.)</para></listitem>
|
||||
</itemizedlist>
|
||||
|
||||
<para>For convenience these two simpler (redundant) service address syntaxes are also supported:</para>
|
||||
|
||||
+26
-5
@@ -1012,8 +1012,8 @@ int putenv_dup(const char *assignment, bool override) {
|
||||
}
|
||||
|
||||
int setenv_systemd_exec_pid(bool update_only) {
|
||||
char str[DECIMAL_STR_MAX(pid_t)];
|
||||
const char *e;
|
||||
int r;
|
||||
|
||||
/* Update $SYSTEMD_EXEC_PID=pid except when '*' is set for the variable. */
|
||||
|
||||
@@ -1024,10 +1024,9 @@ int setenv_systemd_exec_pid(bool update_only) {
|
||||
if (streq_ptr(e, "*"))
|
||||
return 0;
|
||||
|
||||
xsprintf(str, PID_FMT, getpid_cached());
|
||||
|
||||
if (setenv("SYSTEMD_EXEC_PID", str, 1) < 0)
|
||||
return -errno;
|
||||
r = setenvf("SYSTEMD_EXEC_PID", /* overwrite= */ 1, PID_FMT, getpid_cached());
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -1122,3 +1121,25 @@ int set_full_environment(char **env) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int setenvf(const char *name, bool overwrite, const char *valuef, ...) {
|
||||
_cleanup_free_ char *value = NULL;
|
||||
va_list ap;
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
|
||||
if (!valuef)
|
||||
return RET_NERRNO(unsetenv(name));
|
||||
|
||||
va_start(ap, valuef);
|
||||
DISABLE_WARNING_FORMAT_NONLITERAL;
|
||||
r = vasprintf(&value, valuef, ap);
|
||||
REENABLE_WARNING;
|
||||
va_end(ap);
|
||||
|
||||
if (r < 0)
|
||||
return -ENOMEM;
|
||||
|
||||
return RET_NERRNO(setenv(name, value, overwrite));
|
||||
}
|
||||
|
||||
@@ -80,3 +80,5 @@ int getenv_path_list(const char *name, char ***ret_paths);
|
||||
int getenv_steal_erase(const char *name, char **ret);
|
||||
|
||||
int set_full_environment(char **env);
|
||||
|
||||
int setenvf(const char *name, bool overwrite, const char *valuef, ...) _printf_(3,4);
|
||||
|
||||
@@ -170,6 +170,19 @@ int fd_nonblock(int fd, bool nonblock) {
|
||||
return RET_NERRNO(fcntl(fd, F_SETFL, nflags));
|
||||
}
|
||||
|
||||
int stdio_disable_nonblock(void) {
|
||||
int ret = 0;
|
||||
|
||||
/* stdin/stdout/stderr really should have O_NONBLOCK, which would confuse apps if left on, as
|
||||
* write()s might unexpectedly fail with EAGAIN. */
|
||||
|
||||
RET_GATHER(ret, fd_nonblock(STDIN_FILENO, false));
|
||||
RET_GATHER(ret, fd_nonblock(STDOUT_FILENO, false));
|
||||
RET_GATHER(ret, fd_nonblock(STDERR_FILENO, false));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int fd_cloexec(int fd, bool cloexec) {
|
||||
int flags, nflags;
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(DIR*, closedir, NULL);
|
||||
#define _cleanup_close_pair_ _cleanup_(close_pairp)
|
||||
|
||||
int fd_nonblock(int fd, bool nonblock);
|
||||
int stdio_disable_nonblock(void);
|
||||
|
||||
int fd_cloexec(int fd, bool cloexec);
|
||||
int fd_cloexec_many(const int fds[], size_t n_fds, bool cloexec);
|
||||
|
||||
|
||||
@@ -1667,6 +1667,9 @@ int safe_fork_full(
|
||||
log_full_errno(prio, r, "Failed to rearrange stdio fds: %m");
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* Turn off O_NONBLOCK on the fdio fds, in case it was left on */
|
||||
stdio_disable_nonblock();
|
||||
} else {
|
||||
r = make_null_stdio();
|
||||
if (r < 0) {
|
||||
|
||||
+4
-6
@@ -12,6 +12,7 @@
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "build.h"
|
||||
#include "env-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "format-util.h"
|
||||
#include "main-func.h"
|
||||
@@ -157,7 +158,6 @@ static int run(int argc, char *argv[]) {
|
||||
if (argc <= optind)
|
||||
(void) execl("/bin/cat", "/bin/cat", NULL);
|
||||
else {
|
||||
_cleanup_free_ char *s = NULL;
|
||||
struct stat st;
|
||||
|
||||
if (fstat(STDERR_FILENO, &st) < 0)
|
||||
@@ -165,11 +165,9 @@ static int run(int argc, char *argv[]) {
|
||||
"Failed to fstat(%s): %m",
|
||||
FORMAT_PROC_FD_PATH(STDERR_FILENO));
|
||||
|
||||
if (asprintf(&s, DEV_FMT ":" INO_FMT, (dev_t)st.st_dev, st.st_ino) < 0)
|
||||
return log_oom();
|
||||
|
||||
if (setenv("JOURNAL_STREAM", s, /* overwrite = */ true) < 0)
|
||||
return log_error_errno(errno, "Failed to set environment variable JOURNAL_STREAM: %m");
|
||||
r = setenvf("JOURNAL_STREAM", /* overwrite = */ true, DEV_FMT ":" INO_FMT, (dev_t) st.st_dev, st.st_ino);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to set environment variable JOURNAL_STREAM: %m");
|
||||
|
||||
(void) execvp(argv[optind], argv + optind);
|
||||
}
|
||||
|
||||
@@ -1486,9 +1486,15 @@ interpret_port_as_machine_old_syntax:
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
a = strjoin("unixexec:path=ssh,argv1=-xT", p ? ",argv2=-p,argv3=" : "", strempty(p),
|
||||
",argv", p ? "4" : "2", "=--,argv", p ? "5" : "3", "=", e,
|
||||
",argv", p ? "6" : "4", "=systemd-stdio-bridge", c);
|
||||
const char *ssh = secure_getenv("SYSTEMD_SSH") ?: "ssh";
|
||||
_cleanup_free_ char *ssh_escaped = bus_address_escape(ssh);
|
||||
if (!ssh_escaped)
|
||||
return -ENOMEM;
|
||||
|
||||
a = strjoin("unixexec:path=", ssh_escaped, ",argv1=-xT",
|
||||
p ? ",argv2=-p,argv3=" : "", strempty(p),
|
||||
",argv", p ? "4" : "2", "=--,argv", p ? "5" : "3", "=", e,
|
||||
",argv", p ? "6" : "4", "=systemd-stdio-bridge", c);
|
||||
if (!a)
|
||||
return -ENOMEM;
|
||||
|
||||
|
||||
+101
-16
@@ -450,6 +450,10 @@ int varlink_connect_exec(Varlink **ret, const char *_command, char **_argv) {
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0)
|
||||
return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m");
|
||||
|
||||
r = fd_nonblock(pair[1], false);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to disable O_NONBLOCK for varlink socket: %m");
|
||||
|
||||
r = safe_fork_full(
|
||||
"(sd-vlexec)",
|
||||
/* stdio_fds= */ NULL,
|
||||
@@ -507,39 +511,120 @@ int varlink_connect_exec(Varlink **ret, const char *_command, char **_argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int varlink_connect_ssh(Varlink **ret, const char *where) {
|
||||
_cleanup_close_pair_ int pair[2] = EBADF_PAIR;
|
||||
_cleanup_(sigkill_waitp) pid_t pid = 0;
|
||||
int r;
|
||||
|
||||
assert_return(ret, -EINVAL);
|
||||
assert_return(where, -EINVAL);
|
||||
|
||||
/* Connects to an SSH server via OpenSSH 9.4's -W switch to connect to a remote AF_UNIX socket. For
|
||||
* now we do not expose this function directly, but only via varlink_connect_url(). */
|
||||
|
||||
const char *ssh = secure_getenv("SYSTEMD_SSH") ?: "ssh";
|
||||
if (!path_is_valid(ssh))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "SSH path is not valid, refusing: %s", ssh);
|
||||
|
||||
const char *e = strchr(where, ':');
|
||||
if (!e)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "SSH specification lacks a : separator between host and path, refusing: %s", where);
|
||||
|
||||
_cleanup_free_ char *h = strndup(where, e - where);
|
||||
if (!h)
|
||||
return log_oom_debug();
|
||||
|
||||
_cleanup_free_ char *c = strdup(e + 1);
|
||||
if (!c)
|
||||
return log_oom_debug();
|
||||
|
||||
if (!path_is_absolute(c))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Remote AF_UNIX socket path is not absolute, refusing: %s", c);
|
||||
|
||||
_cleanup_free_ char *p = NULL;
|
||||
r = path_simplify_alloc(c, &p);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!path_is_normalized(p))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path is not normalized, refusing: %s", p);
|
||||
|
||||
log_debug("Forking off SSH child process '%s -W %s %s'.", ssh, p, h);
|
||||
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, pair) < 0)
|
||||
return log_debug_errno(errno, "Failed to allocate AF_UNIX socket pair: %m");
|
||||
|
||||
r = safe_fork_full(
|
||||
"(sd-vlssh)",
|
||||
/* stdio_fds= */ (int[]) { pair[1], pair[1], STDERR_FILENO },
|
||||
/* except_fds= */ NULL,
|
||||
/* n_except_fds= */ 0,
|
||||
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REARRANGE_STDIO,
|
||||
&pid);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to spawn process: %m");
|
||||
if (r == 0) {
|
||||
/* Child */
|
||||
|
||||
execlp(ssh, "ssh", "-W", p, h, NULL);
|
||||
log_debug_errno(errno, "Failed to invoke %s: %m", ssh);
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
pair[1] = safe_close(pair[1]);
|
||||
|
||||
Varlink *v;
|
||||
r = varlink_new(&v);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to create varlink object: %m");
|
||||
|
||||
v->fd = TAKE_FD(pair[0]);
|
||||
v->af = AF_UNIX;
|
||||
v->exec_pid = TAKE_PID(pid);
|
||||
varlink_set_state(v, VARLINK_IDLE_CLIENT);
|
||||
|
||||
*ret = v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int varlink_connect_url(Varlink **ret, const char *url) {
|
||||
_cleanup_free_ char *c = NULL;
|
||||
const char *p;
|
||||
bool exec;
|
||||
enum {
|
||||
SCHEME_UNIX,
|
||||
SCHEME_EXEC,
|
||||
SCHEME_SSH,
|
||||
} scheme;
|
||||
int r;
|
||||
|
||||
assert_return(ret, -EINVAL);
|
||||
assert_return(url, -EINVAL);
|
||||
|
||||
// FIXME: Add support for vsock:, ssh-exec:, ssh-unix: URL schemes here. (The latter with OpenSSH
|
||||
// 9.4's -W switch for referencing remote AF_UNIX sockets.)
|
||||
// FIXME: Maybe add support for vsock: and ssh-exec: URL schemes here.
|
||||
|
||||
/* The Varlink URL scheme is a bit underdefined. We support only the unix: transport for now, plus an
|
||||
* exec: transport we made up ourselves. Strictly speaking this shouldn't even be called URL, since
|
||||
* it has nothing to do with Internet URLs by RFC. */
|
||||
/* The Varlink URL scheme is a bit underdefined. We support only the spec-defined unix: transport for
|
||||
* now, plus exec:, ssh: transports we made up ourselves. Strictly speaking this shouldn't even be
|
||||
* called "URL", since it has nothing to do with Internet URLs by RFC. */
|
||||
|
||||
p = startswith(url, "unix:");
|
||||
if (p)
|
||||
exec = false;
|
||||
else {
|
||||
p = startswith(url, "exec:");
|
||||
if (!p)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme not supported.");
|
||||
|
||||
exec = true;
|
||||
}
|
||||
scheme = SCHEME_UNIX;
|
||||
else if ((p = startswith(url, "exec:")))
|
||||
scheme = SCHEME_EXEC;
|
||||
else if ((p = startswith(url, "ssh:")))
|
||||
scheme = SCHEME_SSH;
|
||||
else
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL scheme not supported.");
|
||||
|
||||
/* The varlink.org reference C library supports more than just file system paths. We might want to
|
||||
* support that one day too. For now simply refuse that. */
|
||||
if (p[strcspn(p, ";?#")] != '\0')
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "URL parameterization with ';', '?', '#' not supported.");
|
||||
|
||||
if (exec || p[0] != '@') { /* no validity checks for abstract namespace */
|
||||
if (scheme == SCHEME_SSH)
|
||||
return varlink_connect_ssh(ret, p);
|
||||
|
||||
if (scheme == SCHEME_EXEC || p[0] != '@') { /* no path validity checks for abstract namespace sockets */
|
||||
|
||||
if (!path_is_absolute(p))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path not absolute, refusing.");
|
||||
@@ -552,7 +637,7 @@ int varlink_connect_url(Varlink **ret, const char *url) {
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path is not normalized, refusing.");
|
||||
}
|
||||
|
||||
if (exec)
|
||||
if (scheme == SCHEME_EXEC)
|
||||
return varlink_connect_exec(ret, c, NULL);
|
||||
|
||||
return varlink_connect_address(ret, c ?: p);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "sd-daemon.h"
|
||||
|
||||
#include "common-signal.h"
|
||||
#include "env-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "fs-util.h"
|
||||
#include "mkdir.h"
|
||||
@@ -157,7 +158,6 @@ static int start_one_worker(Manager *m) {
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to fork new worker child: %m");
|
||||
if (r == 0) {
|
||||
char pids[DECIMAL_STR_MAX(pid_t)];
|
||||
/* Child */
|
||||
|
||||
if (m->listen_fd == 3) {
|
||||
@@ -175,9 +175,9 @@ static int start_one_worker(Manager *m) {
|
||||
safe_close(m->listen_fd);
|
||||
}
|
||||
|
||||
xsprintf(pids, PID_FMT, pid);
|
||||
if (setenv("LISTEN_PID", pids, 1) < 0) {
|
||||
log_error_errno(errno, "Failed to set $LISTEN_PID: %m");
|
||||
r = setenvf("LISTEN_PID", /* overwrite= */ true, PID_FMT, pid);
|
||||
if (r < 0) {
|
||||
log_error_errno(r, "Failed to set $LISTEN_PID: %m");
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ test_append_files() {
|
||||
install_mdadm
|
||||
generate_module_dependencies
|
||||
fi
|
||||
|
||||
image_install socat
|
||||
}
|
||||
|
||||
do_test "$@"
|
||||
|
||||
@@ -53,6 +53,32 @@ if [[ -x /usr/lib/systemd/systemd-pcrextend ]]; then
|
||||
varlinkctl introspect /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend
|
||||
fi
|
||||
|
||||
# SSH transport
|
||||
SSHBINDIR="$(mktemp -d)"
|
||||
|
||||
rm_rf_sshbindir() {
|
||||
rm -rf "$SSHBINDIR"
|
||||
}
|
||||
|
||||
trap rm_rf_sshbindir EXIT
|
||||
|
||||
# Create a fake "ssh" binary that validates everything works as expected
|
||||
cat > "$SSHBINDIR"/ssh <<'EOF'
|
||||
#!/bin/sh
|
||||
|
||||
set -xe
|
||||
|
||||
test "$1" = "-W"
|
||||
test "$2" = "/run/systemd/journal/io.systemd.journal"
|
||||
test "$3" = "foobar"
|
||||
|
||||
exec socat - UNIX-CONNECT:/run/systemd/journal/io.systemd.journal
|
||||
EOF
|
||||
|
||||
chmod +x "$SSHBINDIR"/ssh
|
||||
|
||||
SYSTEMD_SSH="$SSHBINDIR/ssh" varlinkctl info ssh:foobar:/run/systemd/journal/io.systemd.journal
|
||||
|
||||
# Go through all varlink sockets we can find under /run/systemd/ for some extra coverage
|
||||
find /run/systemd/ -name "io.systemd*" -type s | while read -r socket; do
|
||||
varlinkctl info "$socket"
|
||||
|
||||
Reference in New Issue
Block a user