[PATCH] allow relative path in streamlocal forwarding

Daiki Ueno dueno at redhat.com
Sat Jun 17 02:17:41 AEST 2017


When forwarding a Unix-domain socket, the remote socket path must be
absolute (otherwise the forwarding fails later).  However, guessing
absolute path on the remote end is sometimes not straightforward,
because the file system location may vary for many reasons, including
the system installation, the choices of NFS mount points, or the
remote user ID.

To allow ssh clients to request remote socket forwarding without
knowledge of the remote system, this patch enables the use of relative
path in remote socket forwarding.  If a relative path is requested as
remote_socket, it is expanded from StreamLocalBindRootDirectory, a new
option added to sshd_config.

This feature would be particularly useful if the remote system is
capable of user runtime directory, as managed by pam_systemd:
https://www.freedesktop.org/software/systemd/man/pam_systemd.html

The applications locating sockets under the runtime directory could
transparently access the forwarded sockets, with the following setting:

  StreamLocalBindRootDirectory /run/user/%i
---
 channels.c             | 104 +++++++++++++++++++++++++++++++++++++++++++++----
 channels.h             |   2 +-
 misc.h                 |   1 +
 readconf.c             |  16 ++++++++
 regress/Makefile       |   1 +
 regress/streamlocal.sh |  59 ++++++++++++++++++++++++++++
 servconf.c             |  21 ++++++++++
 serverloop.c           |   3 +-
 ssh.1                  |  28 +++++++++++++
 sshd.c                 |  25 ++++++++++++
 sshd_config.5          |  11 +++++-
 11 files changed, 260 insertions(+), 11 deletions(-)
 create mode 100644 regress/streamlocal.sh

diff --git a/channels.c b/channels.c
index 028d5db..97561e0 100644
--- a/channels.c
+++ b/channels.c
@@ -2996,6 +2996,59 @@ channel_setup_fwd_listener_tcpip(int type, struct Forward *fwd,
 	return success;
 }
 
+/* Expands a relative socket path to the absolute path, from the given
+ * root directory. */
+static char *
+expand_relative_socket_path(const char *pathname, const char *root_directory)
+{
+	char *ret, *cp;
+	struct stat st;
+
+	if (*pathname == '/')
+		return xstrdup(pathname);
+
+	if (xasprintf(&ret, "%s/%s", root_directory, pathname) >= PATH_MAX) {
+		error("Socket path too long");
+		free(ret);
+		return NULL;
+	}
+
+	cp = ret + strlen(root_directory) + 1;
+	while (*cp != '\0') {
+		char *cp2 = strchr(cp, '/');
+		if (cp2 != NULL)
+			*cp2 = '\0';
+		if (cp2 == cp || strcmp(cp, ".") == 0 || strcmp(cp, "..") == 0) {
+			error("Invalid socket path");
+			free(ret);
+			return NULL;
+		}
+		if (stat(ret, &st) == -1) {
+			if (cp2 != NULL && errno != ENOENT) {
+				error("Parent directory is not accessible");
+				free(ret);
+				return NULL;
+			}
+		} else {
+			if (cp2 != NULL && !S_ISDIR(st.st_mode)) {
+				error("Parent is not a directory");
+				free(ret);
+				return NULL;
+			}
+			if (cp2 == NULL && S_ISLNK(st.st_mode)) {
+				error("Symbolic link is not allowed");
+				free(ret);
+				return NULL;
+			}
+		}
+		if (cp2 == NULL)
+			break;
+		*cp2 = '/';
+		cp = cp2 + 1;
+	}
+	return ret;
+}
+
 static int
 channel_setup_fwd_listener_streamlocal(int type, struct Forward *fwd,
     struct ForwardOptions *fwd_opts)
@@ -3005,6 +3058,7 @@ channel_setup_fwd_listener_streamlocal(int type, struct Forward *fwd,
 	Channel *c;
 	int port, sock;
 	mode_t omask;
+	char *listen_path;
 
 	switch (type) {
 	case SSH_CHANNEL_UNIX_LISTENER:
@@ -3042,22 +3096,39 @@ channel_setup_fwd_listener_streamlocal(int type, struct Forward *fwd,
 		error("No forward path name.");
 		return 0;
 	}
-	if (strlen(fwd->listen_path) > sizeof(sunaddr.sun_path)) {
-		error("Local listening path too long: %s", fwd->listen_path);
+
+	if (fwd->listen_path[0] != '/') {
+		if (fwd_opts->streamlocal_bind_root_directory == NULL) {
+			error("Relative path is not enabled.");
+			return 0;
+		}
+		listen_path = expand_relative_socket_path(fwd->listen_path,
+		    fwd_opts->streamlocal_bind_root_directory);
+		if (listen_path == NULL)
+			return 0;
+	} else
+		listen_path = xstrdup(fwd->listen_path);
+
+	if (strlen(listen_path) > sizeof(sunaddr.sun_path)) {
+		error("Local listening path too long: %s", listen_path);
+		free(listen_path);
 		return 0;
 	}
 
-	debug3("%s: type %d path %s", __func__, type, fwd->listen_path);
+	debug3("%s: type %d path %s", __func__, type, listen_path);
 
 	/* Start a Unix domain listener. */
 	omask = umask(fwd_opts->streamlocal_bind_mask);
-	sock = unix_listener(fwd->listen_path, SSH_LISTEN_BACKLOG,
+	sock = unix_listener(listen_path, SSH_LISTEN_BACKLOG,
 	    fwd_opts->streamlocal_bind_unlink);
 	umask(omask);
-	if (sock < 0)
+	if (sock < 0) {
+		free(listen_path);
 		return 0;
+	}
 
-	debug("Local forwarding listening on path %s.", fwd->listen_path);
+	debug("Local forwarding listening on path %s.", listen_path);
+	free(listen_path);
 
 	/* Allocate a channel number for the socket. */
 	c = channel_new("unix listener", type, sock, sock, -1,
@@ -3825,9 +3896,12 @@ channel_connect_to_port(const char *host, u_short port, char *ctype,
 
 /* Check if connecting to that path is permitted and connect. */
 Channel *
-channel_connect_to_path(const char *path, char *ctype, char *rname)
+channel_connect_to_path(const char *path, char *ctype, char *rname,
+			struct ForwardOptions *fwd_opts)
 {
 	int i, permit, permit_adm = 1;
+	char *connect_path;
+	Channel *c;
 
 	permit = all_opens_permitted;
 	if (!permit) {
@@ -3852,7 +3926,21 @@ channel_connect_to_path(const char *path, char *ctype, char *rname)
 		    "but the request was denied.", path);
 		return NULL;
 	}
-	return connect_to(path, PORT_STREAMLOCAL, ctype, rname);
+
+	if (path[0] != '/') {
+		if (fwd_opts->streamlocal_bind_root_directory == NULL) {
+			logit("Relative path is not enabled");
+			return NULL;
+		}
+		connect_path = expand_relative_socket_path(path,
+                    fwd_opts->streamlocal_bind_root_directory);
+		if (connect_path == NULL)
+			return NULL;
+	} else
+		connect_path = xstrdup(path);
+	c = connect_to(connect_path, PORT_STREAMLOCAL, ctype, rname);
+	free(connect_path);
+	return c;
 }
 
 void
diff --git a/channels.h b/channels.h
index 36e5363..13b6707 100644
--- a/channels.h
+++ b/channels.h
@@ -274,7 +274,7 @@ void	 channel_clear_adm_permitted_opens(void);
 void 	 channel_print_adm_permitted_opens(void);
 Channel	*channel_connect_to_port(const char *, u_short, char *, char *, int *,
 	     const char **);
-Channel *channel_connect_to_path(const char *, char *, char *);
+Channel *channel_connect_to_path(const char *, char *, char *, struct ForwardOptions *);
 Channel	*channel_connect_stdio_fwd(const char*, u_short, int, int);
 Channel	*channel_connect_by_listen_address(const char *, u_short,
 	     char *, char *);
diff --git a/misc.h b/misc.h
index c242f90..99341f6 100644
--- a/misc.h
+++ b/misc.h
@@ -38,6 +38,7 @@ struct ForwardOptions {
 	int	 gateway_ports; /* Allow remote connects to forwarded ports. */
 	mode_t	 streamlocal_bind_mask; /* umask for streamlocal binds */
 	int	 streamlocal_bind_unlink; /* unlink socket before bind */
+	char    *streamlocal_bind_root_directory;
 };
 
 /* misc.c */
diff --git a/readconf.c b/readconf.c
index b11c628..13ab320 100644
--- a/readconf.c
+++ b/readconf.c
@@ -2234,6 +2234,22 @@ parse_forward(struct Forward *fwd, const char *fwdspec, int dynamicfwd, int remo
 	if (fwd->connect_host != NULL &&
 	    strlen(fwd->connect_host) >= NI_MAXHOST)
 		goto fail_free;
+	/* The path starting with "./" means that it will be resolved
+	 * on the server side */
+	if (!dynamicfwd) {
+		if (!remotefwd && fwd->connect_path != NULL &&
+		    strncmp(fwd->connect_path, "./", 2) == 0) {
+			char *path = xstrdup(fwd->connect_path + 2);
+			free(fwd->connect_path);
+			fwd->connect_path = path;
+		}
+		if (remotefwd && fwd->listen_path != NULL &&
+		    strncmp(fwd->listen_path, "./", 2) == 0) {
+			char *path = xstrdup(fwd->listen_path + 2);
+			free(fwd->listen_path);
+			fwd->listen_path = path;
+		}
+	}
 	/* XXX - if connecting to a remote socket, max sun len may not match this host */
 	if (fwd->connect_path != NULL &&
 	    strlen(fwd->connect_path) >= PATH_MAX_SUN)
diff --git a/regress/Makefile b/regress/Makefile
index f968c41..e6e4f24 100644
--- a/regress/Makefile
+++ b/regress/Makefile
@@ -52,6 +52,7 @@ LTESTS= 	connect \
 		reconfigure \
 		dynamic-forward \
 		forwarding \
+		streamlocal \
 		multiplex \
 		reexec \
 		brokenkeys \
diff --git a/regress/streamlocal.sh b/regress/streamlocal.sh
new file mode 100644
index 0000000..2baa70c
--- /dev/null
+++ b/regress/streamlocal.sh
@@ -0,0 +1,59 @@
+#	$OpenBSD: forwarding.sh,v 1.20 2017/04/30 23:34:55 djm Exp $
+#	Placed in the Public Domain.
+
+tid="streamlocal forwarding"
+
+USER=`id -u`
+NC=$OBJ/netcat
+REMOTE_DIR=$OBJ/remote-$USER
+
+start_sshd
+
+trace "remote forwarding, relative socket path disabled on server"
+rm -f $OBJ/localsock
+$NC -U -l $OBJ/localsock > /dev/null &
+netcat_pid=$!
+${SSH} -F $OBJ/ssh_config -p$PORT -o ExitOnForwardFailure=yes -R ./remotesock:$OBJ/localsock somehost true
+r=$?
+kill $netcat_pid 2>&1 >/dev/null
+if [ $r -eq 0 ]; then
+	fail "should fail if relative socket path is disabled"
+fi
+
+stop_sshd
+
+start_sshd -o StreamLocalBindRootDirectory=$OBJ/remote-%i
+
+trace "remote forwarding, relative socket path enabled on server, but has wrong permission"
+rm -fr $REMOTE_DIR
+mkdir $REMOTE_DIR
+chmod 0777 $REMOTE_DIR
+rm -f $OBJ/localsock
+$NC -U -l $OBJ/localsock > /dev/null &
+netcat_pid=$!
+${SSH} -F $OBJ/ssh_config -p$PORT -o ExitOnForwardFailure=yes -R ./remotesock:$OBJ/localsock somehost true
+r=$?
+kill $netcat_pid 2>/dev/null
+if [ $r -eq 0 ]; then
+	fail "should fail if the socket root directory has wrong permission"
+fi
+
+trace "remote forwarding, relative socket path enabled on server, and has right permission"
+rm -fr $REMOTE_DIR
+mkdir $REMOTE_DIR
+chmod 0700 $REMOTE_DIR
+rm -f $OBJ/localsock
+$NC -U -l $OBJ/localsock > /dev/null &
+netcat_pid=$!
+${SSH} -F $OBJ/ssh_config -p$PORT -o ExitOnForwardFailure=yes -R ./remotesock:$OBJ/localsock somehost true
+r=$?
+kill $netcat_pid 2>/dev/null
+if [ $r -ne 0 ]; then
+	fail "should succeed if the socket root directory has right permission"
+fi
+
+stop_sshd
+
+rm -f $OBJ/localsock
+rm -f $OBJ/remotesock
+rm -fr $REMOTE_DIR
diff --git a/servconf.c b/servconf.c
index a112798..ff23418 100644
--- a/servconf.c
+++ b/servconf.c
@@ -136,6 +136,7 @@ initialize_server_options(ServerOptions *options)
 	options->fwd_opts.gateway_ports = -1;
 	options->fwd_opts.streamlocal_bind_mask = (mode_t)-1;
 	options->fwd_opts.streamlocal_bind_unlink = -1;
+	options->fwd_opts.streamlocal_bind_root_directory = NULL;
 	options->num_subsystems = 0;
 	options->max_startups_begin = -1;
 	options->max_startups_rate = -1;
@@ -355,6 +356,7 @@ fill_default_server_options(ServerOptions *options)
 	CLEAR_ON_NONE(options->authorized_principals_file);
 	CLEAR_ON_NONE(options->adm_forced_command);
 	CLEAR_ON_NONE(options->chroot_directory);
+	CLEAR_ON_NONE(options->fwd_opts.streamlocal_bind_root_directory);
 	for (i = 0; i < options->num_host_key_files; i++)
 		CLEAR_ON_NONE(options->host_key_files[i]);
 	for (i = 0; i < options->num_host_cert_files; i++)
@@ -417,6 +419,7 @@ typedef enum {
 	sAuthorizedKeysCommand, sAuthorizedKeysCommandUser,
 	sAuthenticationMethods, sHostKeyAgent, sPermitUserRC,
 	sStreamLocalBindMask, sStreamLocalBindUnlink,
+	sStreamLocalBindRootDirectory,
 	sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding,
 	sDeprecated, sIgnore, sUnsupported
 } ServerOpCodes;
@@ -558,6 +561,7 @@ static struct {
 	{ "authenticationmethods", sAuthenticationMethods, SSHCFG_ALL },
 	{ "streamlocalbindmask", sStreamLocalBindMask, SSHCFG_ALL },
 	{ "streamlocalbindunlink", sStreamLocalBindUnlink, SSHCFG_ALL },
+	{ "streamlocalbindrootdirectory", sStreamLocalBindRootDirectory, SSHCFG_ALL },
 	{ "allowstreamlocalforwarding", sAllowStreamLocalForwarding, SSHCFG_ALL },
 	{ "fingerprinthash", sFingerprintHash, SSHCFG_GLOBAL },
 	{ "disableforwarding", sDisableForwarding, SSHCFG_ALL },
@@ -1823,6 +1827,17 @@ process_server_config_line(ServerOptions *options, char *line,
 		intptr = &options->fwd_opts.streamlocal_bind_unlink;
 		goto parse_flag;
 
+	case sStreamLocalBindRootDirectory:
+		charptr = &options->fwd_opts.streamlocal_bind_root_directory;
+
+		arg = strdelim(&cp);
+		if (!arg || *arg == '\0')
+			fatal("%s line %d: missing file name.",
+			    filename, linenum);
+		if (*activep && *charptr == NULL)
+			*charptr = xstrdup(arg);
+		break;
+
 	case sFingerprintHash:
 		arg = strdelim(&cp);
 		if (!arg || *arg == '\0')
@@ -2039,6 +2054,11 @@ copy_set_server_options(ServerOptions *dst, ServerOptions *src, int preauth)
 		free(dst->chroot_directory);
 		dst->chroot_directory = NULL;
 	}
+	M_CP_STROPT(fwd_opts.streamlocal_bind_root_directory);
+	if (option_clear_or_none(dst->fwd_opts.streamlocal_bind_root_directory)) {
+		free(dst->fwd_opts.streamlocal_bind_root_directory);
+		dst->fwd_opts.streamlocal_bind_root_directory = NULL;
+	}
 }
 
 #undef M_CP_INTOPT
@@ -2300,6 +2320,7 @@ dump_config(ServerOptions *o)
 	    o->hostkeyalgorithms : KEX_DEFAULT_PK_ALG);
 	dump_cfg_string(sPubkeyAcceptedKeyTypes, o->pubkey_key_types ?
 	    o->pubkey_key_types : KEX_DEFAULT_PK_ALG);
+	dump_cfg_string(sStreamLocalBindRootDirectory, o->fwd_opts.streamlocal_bind_root_directory);
 
 	/* string arguments requiring a lookup */
 	dump_cfg_string(sLogLevel, log_level_name(o->log_level));
diff --git a/serverloop.c b/serverloop.c
index b5eb344..f295a3f 100644
--- a/serverloop.c
+++ b/serverloop.c
@@ -488,7 +488,8 @@ server_request_direct_streamlocal(void)
 	    !no_port_forwarding_flag && !options.disable_forwarding &&
 	    (pw->pw_uid == 0 || use_privsep)) {
 		c = channel_connect_to_path(target,
-		    "direct-streamlocal at openssh.com", "direct-streamlocal");
+		    "direct-streamlocal at openssh.com", "direct-streamlocal",
+		    &options.fwd_opts);
 	} else {
 		logit("refused streamlocal port forward: "
 		    "originator %s port %d, target %s",
diff --git a/ssh.1 b/ssh.1
index 3aacec4..e26e1b5 100644
--- a/ssh.1
+++ b/ssh.1
@@ -352,6 +352,20 @@ or the Unix socket
 .Ar remote_socket ,
 from the remote machine.
 .Pp
+.Ar remote_socket
+can be either an absolute path or a relative path.  If it is a
+relative path, it is resolved on the remote host, based on the
+.Cm StreamLocalBindRootDirectory
+setting.
+In order to avoid confusion between service name and socket path,
+.Sq ./
+can be prefixed to explicitly indicate that the path be relative.
+Note that even if relative paths are allowed, path name traversal is
+restricted to directories excluding
+.Sq ..
+and
+.Sq \&. .
+.Pp
 Port forwardings can also be specified in the configuration file.
 Only the superuser can forward privileged ports.
 IPv6 addresses can be specified by enclosing the address in square brackets.
@@ -608,6 +622,20 @@ or
 .Ar local_socket ,
 from the local machine.
 .Pp
+.Ar remote_socket
+can be either an absolute path or a relative path.  If it is a
+relative path, it is resolved on the remote host, based on the
+.Cm StreamLocalBindRootDirectory
+setting.
+In order to avoid confusion between service name and socket path,
+.Sq ./
+can be prefixed to explicitly indicate that the path be relative.
+Note that even if relative paths are allowed, path name traversal is
+restricted to directories excluding
+.Sq ..
+and
+.Sq \&. .
+.Pp
 Port forwardings can also be specified in the configuration file.
 Privileged ports can be forwarded only when
 logging in as root on the remote machine.
diff --git a/sshd.c b/sshd.c
index 06cb81f..ba0207f 100644
--- a/sshd.c
+++ b/sshd.c
@@ -2077,6 +2077,31 @@ main(int ac, char **av)
 		/* the monitor process [priv] will not return */
 	}
 
+	/* Process per-user options */
+	if (options.fwd_opts.streamlocal_bind_root_directory != NULL) {
+		char uidstr[32], *cp;
+		struct stat st;
+
+		snprintf(uidstr, sizeof(uidstr), "%d", authctxt->pw->pw_uid);
+
+		cp = tilde_expand_filename(
+			options.fwd_opts.streamlocal_bind_root_directory,
+			authctxt->pw->pw_uid);
+		free(options.fwd_opts.streamlocal_bind_root_directory);
+		options.fwd_opts.streamlocal_bind_root_directory =
+		    percent_expand(cp,
+			"u", authctxt->pw->pw_name,
+			"i", uidstr,
+			(char *)NULL);
+		free(cp);
+		if (stat(options.fwd_opts.streamlocal_bind_root_directory, &st) == -1)
+			fatal("%s is not accessible",
+			    options.fwd_opts.streamlocal_bind_root_directory);
+		if (st.st_uid != authctxt->pw->pw_uid || (st.st_mode & 077) != 0)
+			fatal("Bad ownership or modes for directory %s",
+			    options.fwd_opts.streamlocal_bind_root_directory);
+	}
+
 	packet_set_timeout(options.client_alive_interval,
 	    options.client_alive_count_max);
 
diff --git a/sshd_config.5 b/sshd_config.5
index 7b4cb1d..0f1cf3f 100644
--- a/sshd_config.5
+++ b/sshd_config.5
@@ -1368,6 +1368,11 @@ or
 .Cm no .
 The default is
 .Cm no .
+.It Cm StreamLocalBindRootDirectory
+Specifies the root directory where a Unix-domain socket file for
+remote port forwarding is created, if the requested path is relative.
+.Pp
+The directory must be readable and writable only by the owner.
 .It Cm StrictModes
 Specifies whether
 .Xr sshd 8
@@ -1613,7 +1618,8 @@ The fingerprint of the key or certificate.
 .It %h
 The home directory of the user.
 .It %i
-The key ID in the certificate.
+The key ID in the certificate if used in AuthorizedPrincipalsCommand,
+or the user ID otherwise.
 .It %K
 The base64-encoded CA key.
 .It %k
@@ -1642,6 +1648,9 @@ accepts the tokens %%, %h, and %u.
 .Pp
 .Cm ChrootDirectory
 accepts the tokens %%, %h, and %u.
+.Pp
+.Cm StreamLocalBindRootDirectory
+accepts the tokens %%, %i, and %u.
 .Sh FILES
 .Bl -tag -width Ds
 .It Pa /etc/ssh/sshd_config
-- 
2.9.4



More information about the openssh-unix-dev mailing list