From: Damien Miller Subject: Match command / Match sessiontype To: tech@openbsd.org Cc: openssh@openssh.com Date: Fri, 17 Jan 2025 12:27:53 +1100 Hi, These patches add some additional client Match predicates to ssh_config(5): 1. "Match command" This allows matching on the requested remote command, if any. `Match command ""` will match an empty command. It also tweaks the `Match tag` predicate to support similarly matching empty tags, which wasn't possible before. 2. "Match sessiontype" Allows matching on the requested session type: shell, exec, subsystem or none. This solves a few situations that users have requested more hacky solutions to. E.g. a special flag to enable compression only for sftp, which can be done easily using something like Match sessiontype subsystem command sftp Compression yes Also at https://github.com/djmdjm/openssh-wip/pull/49 ok? From f8ac601f0cbf293998a5fbac2d56c9639a2cdd51 Mon Sep 17 00:00:00 2001 From: Damien Miller Date: Sat, 7 Dec 2024 01:04:06 +1100 Subject: [PATCH 1/2] "Match command ..." support for ssh_config Allows matching on the command specified on the commandline Relaxes matching rules for `Match tagged` to allow `Match tagged ""` to match an empty tag value. This also works for command. --- readconf.c | 67 +++++++++++++++++++++++++++++++++------------------ readconf.h | 4 +-- ssh-keysign.c | 2 +- ssh.c | 18 ++++++++------ ssh_config.5 | 13 ++++++++++ 5 files changed, 71 insertions(+), 33 deletions(-) diff --git a/readconf.c b/readconf.c index de577cd..45f506b 100644 --- a/readconf.c +++ b/readconf.c @@ -117,11 +117,11 @@ */ static int read_config_file_depth(const char *filename, struct passwd *pw, - const char *host, const char *original_host, Options *options, - int flags, int *activep, int *want_final_pass, int depth); + const char *host, const char *original_host, const char *remote_command, + Options *options, int flags, int *activep, int *want_final_pass, int depth); static int process_config_line_depth(Options *options, struct passwd *pw, - const char *host, const char *original_host, char *line, - const char *filename, int linenum, int *activep, int flags, + const char *host, const char *original_host, const char *remote_command, + char *line, const char *filename, int linenum, int *activep, int flags, int *want_final_pass, int depth); /* Keyword tokens. */ @@ -687,7 +687,8 @@ expand_match_exec_or_include_path(const char *path, Options *options, static int match_cfg_line(Options *options, const char *full_line, int *acp, char ***avp, struct passwd *pw, const char *host_arg, const char *original_host, - int final_pass, int *want_final_pass, const char *filename, int linenum) + const char *remote_command, int final_pass, int *want_final_pass, + const char *filename, int linenum) { char *arg, *oattrib = NULL, *attrib = NULL, *cmd, *host, *criteria; const char *ruser; @@ -765,6 +766,7 @@ match_cfg_line(Options *options, const char *full_line, int *acp, char ***avp, strprefix(attrib, "localuser=", 1) != NULL || strprefix(attrib, "localnetwork=", 1) != NULL || strprefix(attrib, "tagged=", 1) != NULL || + strprefix(attrib, "command=", 1) != NULL || strprefix(attrib, "exec=", 1) != NULL) { arg = strchr(attrib, '='); *(arg++) = '\0'; @@ -772,8 +774,16 @@ match_cfg_line(Options *options, const char *full_line, int *acp, char ***avp, arg = argv_next(acp, avp); } - /* All other criteria require an argument */ - if (arg == NULL || *arg == '\0' || *arg == '#') { + /* + * All other criteria require an argument, though it may + * be the empty string for the "tagged" and "command" + * options. + */ + if (*arg == '\0' && + strcasecmp(attrib, "tagged") != 0 && + strcasecmp(attrib, "command") != 0) + arg = NULL; + if (arg == NULL || *arg == '#') { error("Missing Match criteria for %s", attrib); result = -1; goto out; @@ -810,7 +820,17 @@ match_cfg_line(Options *options, const char *full_line, int *acp, char ***avp, } else if (strcasecmp(attrib, "tagged") == 0) { criteria = xstrdup(options->tag == NULL ? "" : options->tag); - r = match_pattern_list(criteria, arg, 0) == 1; + /* Special case: empty criteria matches empty arg */ + r = (*criteria == '\0') ? *arg == '\0' : + match_pattern_list(criteria, arg, 0) == 1; + if (r == (negate ? 1 : 0)) + this_result = result = 0; + } else if (strcasecmp(attrib, "command") == 0) { + criteria = xstrdup(remote_command == NULL ? + "" : remote_command); + /* Special case: empty criteria matches empty arg */ + r = (*criteria == '\0') ? *arg == '\0' : + match_pattern_list(criteria, arg, 0) == 1; if (r == (negate ? 1 : 0)) this_result = result = 0; } else if (strcasecmp(attrib, "exec") == 0) { @@ -1057,18 +1077,19 @@ parse_multistate_value(const char *arg, const char *filename, int linenum, */ int process_config_line(Options *options, struct passwd *pw, const char *host, - const char *original_host, char *line, const char *filename, - int linenum, int *activep, int flags) + const char *original_host, const char *remote_command, char *line, + const char *filename, int linenum, int *activep, int flags) { return process_config_line_depth(options, pw, host, original_host, - line, filename, linenum, activep, flags, NULL, 0); + remote_command, line, filename, linenum, activep, flags, NULL, 0); } #define WHITESPACE " \t\r\n" static int process_config_line_depth(Options *options, struct passwd *pw, const char *host, - const char *original_host, char *line, const char *filename, - int linenum, int *activep, int flags, int *want_final_pass, int depth) + const char *original_host, const char *remote_command, char *line, + const char *filename, int linenum, int *activep, int flags, + int *want_final_pass, int depth) { char *str, **charptr, *endofnumber, *keyword, *arg, *arg2, *p; char **cpptr, ***cppptr, fwdarg[256]; @@ -1805,8 +1826,8 @@ parse_pubkey_algos: goto out; } value = match_cfg_line(options, str, &ac, &av, pw, host, - original_host, flags & SSHCONF_FINAL, want_final_pass, - filename, linenum); + original_host, remote_command, flags & SSHCONF_FINAL, + want_final_pass, filename, linenum); if (value < 0) { error("%.200s line %d: Bad Match condition", filename, linenum); @@ -2058,8 +2079,8 @@ parse_pubkey_algos: gl.gl_pathv[i], depth, oactive ? "" : " (parse only)"); r = read_config_file_depth(gl.gl_pathv[i], - pw, host, original_host, options, - flags | SSHCONF_CHECKPERM | + pw, host, original_host, remote_command, + options, flags | SSHCONF_CHECKPERM | (oactive ? 0 : SSHCONF_NEVERMATCH), activep, want_final_pass, depth + 1); if (r != 1 && errno != ENOENT) { @@ -2482,20 +2503,20 @@ parse_pubkey_algos: */ int read_config_file(const char *filename, struct passwd *pw, const char *host, - const char *original_host, Options *options, int flags, + const char *original_host, const char *remote_command, Options *options, int flags, int *want_final_pass) { int active = 1; return read_config_file_depth(filename, pw, host, original_host, - options, flags, &active, want_final_pass, 0); + remote_command, options, flags, &active, want_final_pass, 0); } #define READCONF_MAX_DEPTH 16 static int read_config_file_depth(const char *filename, struct passwd *pw, - const char *host, const char *original_host, Options *options, - int flags, int *activep, int *want_final_pass, int depth) + const char *host, const char *original_host, const char *remote_command, + Options *options, int flags, int *activep, int *want_final_pass, int depth) { FILE *f; char *line = NULL; @@ -2535,8 +2556,8 @@ read_config_file_depth(const char *filename, struct passwd *pw, * line numbers later for error messages. */ if (process_config_line_depth(options, pw, host, original_host, - line, filename, linenum, activep, flags, want_final_pass, - depth) != 0) + remote_command, line, filename, linenum, activep, flags, + want_final_pass, depth) != 0) bad_options++; } free(line); diff --git a/readconf.h b/readconf.h index 2922dcb..65838a3 100644 --- a/readconf.h +++ b/readconf.h @@ -240,9 +240,9 @@ int fill_default_options(Options *); void fill_default_options_for_canonicalization(Options *); void free_options(Options *o); int process_config_line(Options *, struct passwd *, const char *, - const char *, char *, const char *, int, int *, int); + const char *, const char *, char *, const char *, int, int *, int); int read_config_file(const char *, struct passwd *, const char *, - const char *, Options *, int, int *); + const char *, const char *, Options *, int, int *); int parse_forward(struct Forward *, const char *, int, int); int parse_jump(const char *, Options *, int); int parse_ssh_uri(const char *, char **, char **, int *); diff --git a/ssh-keysign.c b/ssh-keysign.c index 434c750..161a02c 100644 --- a/ssh-keysign.c +++ b/ssh-keysign.c @@ -215,7 +215,7 @@ main(int argc, char **argv) /* verify that ssh-keysign is enabled by the admin */ initialize_options(&options); - (void)read_config_file(_PATH_HOST_CONFIG_FILE, pw, "", "", + (void)read_config_file(_PATH_HOST_CONFIG_FILE, pw, "", "", "", &options, 0, NULL); (void)fill_default_options(&options); if (options.enable_ssh_keysign != 1) diff --git a/ssh.c b/ssh.c index 0742ec8..bf2b445 100644 --- a/ssh.c +++ b/ssh.c @@ -542,15 +542,18 @@ check_load(int r, struct sshkey **k, const char *path, const char *message) * file if the user specifies a config file on the command line. */ static void -process_config_files(const char *host_name, struct passwd *pw, int final_pass, - int *want_final_pass) +process_config_files(const char *host_name, struct passwd *pw, + int final_pass, int *want_final_pass) { - char buf[PATH_MAX]; + char *cmd, buf[PATH_MAX]; int r; + if ((cmd = sshbuf_dup_string(command)) == NULL) + fatal_f("sshbuf_dup_string failed"); if (config != NULL) { if (strcasecmp(config, "none") != 0 && - !read_config_file(config, pw, host, host_name, &options, + !read_config_file(config, pw, host, host_name, cmd, + &options, SSHCONF_USERCONF | (final_pass ? SSHCONF_FINAL : 0), want_final_pass)) fatal("Can't open user config file %.100s: " @@ -559,15 +562,16 @@ process_config_files(const char *host_name, struct passwd *pw, int final_pass, r = snprintf(buf, sizeof buf, "%s/%s", pw->pw_dir, _PATH_SSH_USER_CONFFILE); if (r > 0 && (size_t)r < sizeof(buf)) - (void)read_config_file(buf, pw, host, host_name, + (void)read_config_file(buf, pw, host, host_name, cmd, &options, SSHCONF_CHECKPERM | SSHCONF_USERCONF | (final_pass ? SSHCONF_FINAL : 0), want_final_pass); /* Read systemwide configuration file after user config. */ (void)read_config_file(_PATH_HOST_CONFIG_FILE, pw, - host, host_name, &options, + host, host_name, cmd, &options, final_pass ? SSHCONF_FINAL : 0, want_final_pass); } + free(cmd); } /* Rewrite the port number in an addrinfo list of addresses */ @@ -1048,7 +1052,7 @@ main(int ac, char **av) case 'o': line = xstrdup(optarg); if (process_config_line(&options, pw, - host ? host : "", host ? host : "", line, + host ? host : "", host ? host : "", "", line, "command-line", 0, NULL, SSHCONF_USERCONF) != 0) exit(255); free(line); diff --git a/ssh_config.5 b/ssh_config.5 index 5ca1475..519037c 100644 --- a/ssh_config.5 +++ b/ssh_config.5 @@ -145,6 +145,7 @@ The available criteria keywords are: .Cm host , .Cm originalhost , .Cm tagged , +.Cm command , .Cm user , and .Cm localuser . @@ -212,6 +213,7 @@ The other keywords' criteria must be single entries or comma-separated lists and may use the wildcard and negation operators described in the .Sx PATTERNS section. +.Pp The criteria for the .Cm host keyword are matched against the target hostname, after any substitution @@ -223,6 +225,7 @@ options. The .Cm originalhost keyword matches against the hostname as it was specified on the command-line. +.Pp The .Cm tagged keyword matches a tag name specified by a prior @@ -233,6 +236,16 @@ command-line using the .Fl P flag. The +.Cm command +keyword matches the remote command that has been requested, or the subsystem +name that is being invoked (e.g. +.Oq sftp +for an SFTP session). +The empty string will match the case where a command or tag has not been +specified, i.e. +.Sq Match tag \&"\&" +.Pp +The .Cm user keyword matches against the target username on the remote host. The -- 2.47.0 From 3c130e35c9a911adfe161b7069d3e55c209e7482 Mon Sep 17 00:00:00 2001 From: Damien Miller Date: Fri, 17 Jan 2025 10:43:48 +1100 Subject: [PATCH 2/2] match-sessiontype --- readconf.c | 13 +++++++++++++ ssh_config.5 | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/readconf.c b/readconf.c index 45f506b..ed963ee 100644 --- a/readconf.c +++ b/readconf.c @@ -833,6 +833,19 @@ match_cfg_line(Options *options, const char *full_line, int *acp, char ***avp, match_pattern_list(criteria, arg, 0) == 1; if (r == (negate ? 1 : 0)) this_result = result = 0; + } else if (strcasecmp(attrib, "sessiontype") == 0) { + if (options->session_type == SESSION_TYPE_SUBSYSTEM) + criteria = xstrdup("subsystem"); + else if (options->session_type == SESSION_TYPE_NONE) + criteria = xstrdup("none"); + else if (remote_command != NULL && + *remote_command != '\0') + criteria = xstrdup("exec"); + else + criteria = xstrdup("shell"); + r = match_pattern_list(criteria, arg, 0) == 1; + if (r == (negate ? 1 : 0)) + this_result = result = 0; } else if (strcasecmp(attrib, "exec") == 0) { if ((cmd = expand_match_exec_or_include_path(arg, options, pw, host_arg, original_host, diff --git a/ssh_config.5 b/ssh_config.5 index 519037c..23b6e3d 100644 --- a/ssh_config.5 +++ b/ssh_config.5 @@ -255,6 +255,24 @@ keyword matches against the name of the local user running (this keyword may be useful in system-wide .Nm files). +.Pp +Finally, the +.Cm sessiontype +keyword matches the requested session type, which may be one of +.Cm shell +for interactive sessions, +.Cm exec +for command execution sessions, +.Cm subsystem +for subsystem invocations such as +.Xr sftp 1 , +or +.Cm none +for transport-only sessions, such as when +.Xr ssh 1 +is started with the +.Fl N +flag. .It Cm AddKeysToAgent Specifies whether keys should be automatically added to a running .Xr ssh-agent 1 . -- 2.47.0