Download raw body.
Match command / Match sessiontype
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 <djm@mindrot.org>
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 <djm@mindrot.org>
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
Match command / Match sessiontype