- dtucker@cvs.openbsd.org 2006/07/12 11:34:58
     [sshd.c servconf.h servconf.c sshd_config.5 auth.c]
     Add support for conditional directives to sshd_config via a "Match"
     keyword, which works similarly to the "Host" directive in ssh_config.
     Lines after a Match line override the default set in the main section
     if the condition on the Match line is true, eg
     AllowTcpForwarding yes
     Match User anoncvs
             AllowTcpForwarding no
     will allow port forwarding by all users except "anoncvs".
     Currently only a very small subset of directives are supported.
     ok djm@
diff --git a/servconf.c b/servconf.c
index c5b933a..42ec340 100644
--- a/servconf.c
+++ b/servconf.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: servconf.c,v 1.152 2006/07/08 21:47:12 stevesk Exp $ */
+/* $OpenBSD: servconf.c,v 1.153 2006/07/12 11:34:58 dtucker Exp $ */
 /*
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
  *                    All rights reserved
@@ -25,12 +25,14 @@
 #include "cipher.h"
 #include "kex.h"
 #include "mac.h"
+#include "match.h"
 
 static void add_listen_addr(ServerOptions *, char *, u_short);
 static void add_one_listen_addr(ServerOptions *, char *, u_short);
 
 /* Use of privilege separation or not */
 extern int use_privsep;
+extern Buffer cfg;
 
 /* Initializes the server options to their default values. */
 
@@ -105,9 +107,6 @@
 	options->authorized_keys_file2 = NULL;
 	options->num_accept_env = 0;
 	options->permit_tun = -1;
-
-	/* Needs to be accessable in many places */
-	use_privsep = -1;
 }
 
 void
@@ -277,110 +276,116 @@
 	sHostbasedUsesNameFromPacketOnly, sClientAliveInterval,
 	sClientAliveCountMax, sAuthorizedKeysFile, sAuthorizedKeysFile2,
 	sGssAuthentication, sGssCleanupCreds, sAcceptEnv, sPermitTunnel,
+	sMatch,
 	sUsePrivilegeSeparation,
 	sDeprecated, sUnsupported
 } ServerOpCodes;
 
+#define SSHCFG_GLOBAL	0x01	/* allowed in main section of sshd_config */
+#define SSHCFG_MATCH	0x02	/* allowed inside a Match section */
+#define SSHCFG_ALL	(SSHCFG_GLOBAL|SSHCFG_MATCH)
+
 /* Textual representation of the tokens. */
 static struct {
 	const char *name;
 	ServerOpCodes opcode;
+	u_int flags;
 } keywords[] = {
 	/* Portable-specific options */
 #ifdef USE_PAM
-	{ "usepam", sUsePAM },
+	{ "usepam", sUsePAM, SSHCFG_GLOBAL },
 #else
-	{ "usepam", sUnsupported },
+	{ "usepam", sUnsupported, SSHCFG_GLOBAL },
 #endif
-	{ "pamauthenticationviakbdint", sDeprecated },
+	{ "pamauthenticationviakbdint", sDeprecated, SSHCFG_GLOBAL },
 	/* Standard Options */
-	{ "port", sPort },
-	{ "hostkey", sHostKeyFile },
-	{ "hostdsakey", sHostKeyFile },					/* alias */
-	{ "pidfile", sPidFile },
-	{ "serverkeybits", sServerKeyBits },
-	{ "logingracetime", sLoginGraceTime },
-	{ "keyregenerationinterval", sKeyRegenerationTime },
-	{ "permitrootlogin", sPermitRootLogin },
-	{ "syslogfacility", sLogFacility },
-	{ "loglevel", sLogLevel },
-	{ "rhostsauthentication", sDeprecated },
-	{ "rhostsrsaauthentication", sRhostsRSAAuthentication },
-	{ "hostbasedauthentication", sHostbasedAuthentication },
-	{ "hostbasedusesnamefrompacketonly", sHostbasedUsesNameFromPacketOnly },
-	{ "rsaauthentication", sRSAAuthentication },
-	{ "pubkeyauthentication", sPubkeyAuthentication },
-	{ "dsaauthentication", sPubkeyAuthentication },			/* alias */
+	{ "port", sPort, SSHCFG_GLOBAL },
+	{ "hostkey", sHostKeyFile, SSHCFG_GLOBAL },
+	{ "hostdsakey", sHostKeyFile, SSHCFG_GLOBAL },		/* alias */
+	{ "pidfile", sPidFile, SSHCFG_GLOBAL },
+	{ "serverkeybits", sServerKeyBits, SSHCFG_GLOBAL },
+	{ "logingracetime", sLoginGraceTime, SSHCFG_GLOBAL },
+	{ "keyregenerationinterval", sKeyRegenerationTime, SSHCFG_GLOBAL },
+	{ "permitrootlogin", sPermitRootLogin, SSHCFG_GLOBAL },
+	{ "syslogfacility", sLogFacility, SSHCFG_GLOBAL },
+	{ "loglevel", sLogLevel, SSHCFG_GLOBAL },
+	{ "rhostsauthentication", sDeprecated, SSHCFG_GLOBAL },
+	{ "rhostsrsaauthentication", sRhostsRSAAuthentication, SSHCFG_GLOBAL },
+	{ "hostbasedauthentication", sHostbasedAuthentication, SSHCFG_GLOBAL },
+	{ "hostbasedusesnamefrompacketonly", sHostbasedUsesNameFromPacketOnly, SSHCFG_GLOBAL },
+	{ "rsaauthentication", sRSAAuthentication, SSHCFG_GLOBAL },
+	{ "pubkeyauthentication", sPubkeyAuthentication, SSHCFG_GLOBAL },
+	{ "dsaauthentication", sPubkeyAuthentication, SSHCFG_GLOBAL },	/* alias */
 #ifdef KRB5
-	{ "kerberosauthentication", sKerberosAuthentication },
-	{ "kerberosorlocalpasswd", sKerberosOrLocalPasswd },
-	{ "kerberosticketcleanup", sKerberosTicketCleanup },
+	{ "kerberosauthentication", sKerberosAuthentication, SSHCFG_GLOBAL },
+	{ "kerberosorlocalpasswd", sKerberosOrLocalPasswd, SSHCFG_GLOBAL },
+	{ "kerberosticketcleanup", sKerberosTicketCleanup, SSHCFG_GLOBAL },
 #ifdef USE_AFS
-	{ "kerberosgetafstoken", sKerberosGetAFSToken },
+	{ "kerberosgetafstoken", sKerberosGetAFSToken, SSHCFG_GLOBAL },
 #else
-	{ "kerberosgetafstoken", sUnsupported },
+	{ "kerberosgetafstoken", sUnsupported, SSHCFG_GLOBAL },
 #endif
 #else
-	{ "kerberosauthentication", sUnsupported },
-	{ "kerberosorlocalpasswd", sUnsupported },
-	{ "kerberosticketcleanup", sUnsupported },
-	{ "kerberosgetafstoken", sUnsupported },
+	{ "kerberosauthentication", sUnsupported, SSHCFG_GLOBAL },
+	{ "kerberosorlocalpasswd", sUnsupported, SSHCFG_GLOBAL },
+	{ "kerberosticketcleanup", sUnsupported, SSHCFG_GLOBAL },
+	{ "kerberosgetafstoken", sUnsupported, SSHCFG_GLOBAL },
 #endif
-	{ "kerberostgtpassing", sUnsupported },
-	{ "afstokenpassing", sUnsupported },
+	{ "kerberostgtpassing", sUnsupported, SSHCFG_GLOBAL },
+	{ "afstokenpassing", sUnsupported, SSHCFG_GLOBAL },
 #ifdef GSSAPI
-	{ "gssapiauthentication", sGssAuthentication },
-	{ "gssapicleanupcredentials", sGssCleanupCreds },
+	{ "gssapiauthentication", sGssAuthentication, SSHCFG_GLOBAL },
+	{ "gssapicleanupcredentials", sGssCleanupCreds, SSHCFG_GLOBAL },
 #else
-	{ "gssapiauthentication", sUnsupported },
-	{ "gssapicleanupcredentials", sUnsupported },
+	{ "gssapiauthentication", sUnsupported, SSHCFG_GLOBAL },
+	{ "gssapicleanupcredentials", sUnsupported, SSHCFG_GLOBAL },
 #endif
-	{ "passwordauthentication", sPasswordAuthentication },
-	{ "kbdinteractiveauthentication", sKbdInteractiveAuthentication },
-	{ "challengeresponseauthentication", sChallengeResponseAuthentication },
-	{ "skeyauthentication", sChallengeResponseAuthentication }, /* alias */
-	{ "checkmail", sDeprecated },
-	{ "listenaddress", sListenAddress },
-	{ "addressfamily", sAddressFamily },
-	{ "printmotd", sPrintMotd },
-	{ "printlastlog", sPrintLastLog },
-	{ "ignorerhosts", sIgnoreRhosts },
-	{ "ignoreuserknownhosts", sIgnoreUserKnownHosts },
-	{ "x11forwarding", sX11Forwarding },
-	{ "x11displayoffset", sX11DisplayOffset },
-	{ "x11uselocalhost", sX11UseLocalhost },
-	{ "xauthlocation", sXAuthLocation },
-	{ "strictmodes", sStrictModes },
-	{ "permitemptypasswords", sEmptyPasswd },
-	{ "permituserenvironment", sPermitUserEnvironment },
-	{ "uselogin", sUseLogin },
-	{ "compression", sCompression },
-	{ "tcpkeepalive", sTCPKeepAlive },
-	{ "keepalive", sTCPKeepAlive },				/* obsolete alias */
-	{ "allowtcpforwarding", sAllowTcpForwarding },
-	{ "allowusers", sAllowUsers },
-	{ "denyusers", sDenyUsers },
-	{ "allowgroups", sAllowGroups },
-	{ "denygroups", sDenyGroups },
-	{ "ciphers", sCiphers },
-	{ "macs", sMacs },
-	{ "protocol", sProtocol },
-	{ "gatewayports", sGatewayPorts },
-	{ "subsystem", sSubsystem },
-	{ "maxstartups", sMaxStartups },
-	{ "maxauthtries", sMaxAuthTries },
-	{ "banner", sBanner },
-	{ "usedns", sUseDNS },
-	{ "verifyreversemapping", sDeprecated },
-	{ "reversemappingcheck", sDeprecated },
-	{ "clientaliveinterval", sClientAliveInterval },
-	{ "clientalivecountmax", sClientAliveCountMax },
-	{ "authorizedkeysfile", sAuthorizedKeysFile },
-	{ "authorizedkeysfile2", sAuthorizedKeysFile2 },
-	{ "useprivilegeseparation", sUsePrivilegeSeparation},
-	{ "acceptenv", sAcceptEnv },
-	{ "permittunnel", sPermitTunnel },
-	{ NULL, sBadOption }
+	{ "passwordauthentication", sPasswordAuthentication, SSHCFG_GLOBAL },
+	{ "kbdinteractiveauthentication", sKbdInteractiveAuthentication, SSHCFG_GLOBAL },
+	{ "challengeresponseauthentication", sChallengeResponseAuthentication, SSHCFG_GLOBAL },
+	{ "skeyauthentication", sChallengeResponseAuthentication, SSHCFG_GLOBAL }, /* alias */
+	{ "checkmail", sDeprecated, SSHCFG_GLOBAL },
+	{ "listenaddress", sListenAddress, SSHCFG_GLOBAL },
+	{ "addressfamily", sAddressFamily, SSHCFG_GLOBAL },
+	{ "printmotd", sPrintMotd, SSHCFG_GLOBAL },
+	{ "printlastlog", sPrintLastLog, SSHCFG_GLOBAL },
+	{ "ignorerhosts", sIgnoreRhosts, SSHCFG_GLOBAL },
+	{ "ignoreuserknownhosts", sIgnoreUserKnownHosts, SSHCFG_GLOBAL },
+	{ "x11forwarding", sX11Forwarding, SSHCFG_GLOBAL },
+	{ "x11displayoffset", sX11DisplayOffset, SSHCFG_GLOBAL },
+	{ "x11uselocalhost", sX11UseLocalhost, SSHCFG_GLOBAL },
+	{ "xauthlocation", sXAuthLocation, SSHCFG_GLOBAL },
+	{ "strictmodes", sStrictModes, SSHCFG_GLOBAL },
+	{ "permitemptypasswords", sEmptyPasswd, SSHCFG_GLOBAL },
+	{ "permituserenvironment", sPermitUserEnvironment, SSHCFG_GLOBAL },
+	{ "uselogin", sUseLogin, SSHCFG_GLOBAL },
+	{ "compression", sCompression, SSHCFG_GLOBAL },
+	{ "tcpkeepalive", sTCPKeepAlive, SSHCFG_GLOBAL },
+	{ "keepalive", sTCPKeepAlive, SSHCFG_GLOBAL },	/* obsolete alias */
+	{ "allowtcpforwarding", sAllowTcpForwarding, SSHCFG_ALL },
+	{ "allowusers", sAllowUsers, SSHCFG_GLOBAL },
+	{ "denyusers", sDenyUsers, SSHCFG_GLOBAL },
+	{ "allowgroups", sAllowGroups, SSHCFG_GLOBAL },
+	{ "denygroups", sDenyGroups, SSHCFG_GLOBAL },
+	{ "ciphers", sCiphers, SSHCFG_GLOBAL },
+	{ "macs", sMacs, SSHCFG_GLOBAL },
+	{ "protocol", sProtocol, SSHCFG_GLOBAL },
+	{ "gatewayports", sGatewayPorts, SSHCFG_ALL },
+	{ "subsystem", sSubsystem, SSHCFG_GLOBAL },
+	{ "maxstartups", sMaxStartups, SSHCFG_GLOBAL },
+	{ "maxauthtries", sMaxAuthTries, SSHCFG_GLOBAL },
+	{ "banner", sBanner, SSHCFG_GLOBAL },
+	{ "usedns", sUseDNS, SSHCFG_GLOBAL },
+	{ "verifyreversemapping", sDeprecated, SSHCFG_GLOBAL },
+	{ "reversemappingcheck", sDeprecated, SSHCFG_GLOBAL },
+	{ "clientaliveinterval", sClientAliveInterval, SSHCFG_GLOBAL },
+	{ "clientalivecountmax", sClientAliveCountMax, SSHCFG_GLOBAL },
+	{ "authorizedkeysfile", sAuthorizedKeysFile, SSHCFG_GLOBAL },
+	{ "authorizedkeysfile2", sAuthorizedKeysFile2, SSHCFG_GLOBAL },
+	{ "useprivilegeseparation", sUsePrivilegeSeparation, SSHCFG_GLOBAL },
+	{ "acceptenv", sAcceptEnv, SSHCFG_GLOBAL },
+	{ "permittunnel", sPermitTunnel, SSHCFG_GLOBAL },
+	{ NULL, sBadOption, 0 }
 };
 
 /*
@@ -389,13 +394,15 @@
 
 static ServerOpCodes
 parse_token(const char *cp, const char *filename,
-	    int linenum)
+	    int linenum, u_int *flags)
 {
 	u_int i;
 
 	for (i = 0; keywords[i].name; i++)
-		if (strcasecmp(cp, keywords[i].name) == 0)
+		if (strcasecmp(cp, keywords[i].name) == 0) {
+			*flags = keywords[i].flags;
 			return keywords[i].opcode;
+		}
 
 	error("%s: line %d: Bad configuration option: %s",
 	    filename, linenum, cp);
@@ -440,15 +447,112 @@
 	options->listen_addrs = aitop;
 }
 
+/*
+ * The strategy for the Match blocks is that the config file is parsed twice.
+ *
+ * The first time is at startup.  activep is initialized to 1 and the
+ * directives in the global context are processed and acted on.  Hitting a
+ * Match directive unsets activep and the directives inside the block are
+ * checked for syntax only.
+ *
+ * The second time is after a connection has been established but before
+ * authentication.  activep is initialized to 2 and global config directives
+ * are ignored since they have already been processed.  If the criteria in a
+ * Match block is met, activep is set and the subsequent directives
+ * processed and actioned until EOF or another Match block unsets it.  Any
+ * options set are copied into the main server config.
+ *
+ * Potential additions/improvements:
+ *  - Add Match support for pre-kex directives, eg Protocol, Ciphers.
+ *
+ *  - Add a Tag directive (idea from David Leonard) ala pf, eg:
+ *	Match Address 192.168.0.*
+ *		Tag trusted
+ *	Match Group wheel
+ *		Tag trusted
+ *	Match Tag trusted
+ *		AllowTcpForwarding yes
+ *		GatewayPorts clientspecified
+ *		[...]
+ *
+ *  - Add a PermittedChannelRequests directive
+ *	Match Group shell
+ *		PermittedChannelRequests session,forwarded-tcpip
+ */
+
+static int
+match_cfg_line(char **condition, int line, const char *user, const char *host,
+    const char *address)
+{
+	int result = 1;
+	char *arg, *attrib, *cp = *condition;
+	size_t len;
+
+	if (user == NULL)
+		debug3("checking syntax for 'Match %s'", cp);
+	else
+		debug3("checking match for '%s' user %s host %s addr %s", cp,
+		    user ? user : "(null)", host ? host : "(null)",
+		    address ? address : "(null)");
+
+	while ((attrib = strdelim(&cp)) && *attrib != '\0') {
+		if ((arg = strdelim(&cp)) == NULL || *arg == '\0') {
+			error("Missing Match criteria for %s", attrib);
+			return -1;
+		}
+		len = strlen(arg);
+		if (strcasecmp(attrib, "user") == 0) {
+			if (!user) {
+				result = 0;
+				continue;
+			}
+			if (match_pattern_list(user, arg, len, 0) != 1)
+				result = 0;
+			else
+				debug("user %.100s matched 'User %.100s' at "
+				    "line %d", user, arg, line);
+		} else if (strcasecmp(attrib, "host") == 0) {
+			if (!host) {
+				result = 0;
+				continue;
+			}
+			if (match_hostname(host, arg, len) != 1)
+				result = 0;
+			else
+				debug("connection from %.100s matched 'Host "
+				    "%.100s' at line %d", host, arg, line);
+		} else if (strcasecmp(attrib, "address") == 0) {
+			debug("address '%s' arg '%s'", address, arg);
+			if (!address) {
+				result = 0;
+				continue;
+			}
+			if (match_hostname(address, arg, len) != 1)
+				result = 0;
+			else
+				debug("connection from %.100s matched 'Address "
+				    "%.100s' at line %d", address, arg, line);
+		} else {
+			error("Unsupported Match attribute %s", attrib);
+			return -1;
+		}
+	}
+	if (user != NULL)
+		debug3("match %sfound", result ? "" : "not ");
+	*condition = cp;
+	return result;
+}
+
 int
 process_server_config_line(ServerOptions *options, char *line,
-    const char *filename, int linenum)
+    const char *filename, int linenum, int *activep, const char *user,
+    const char *host, const char *address)
 {
 	char *cp, **charptr, *arg, *p;
-	int *intptr, value, n;
+	int cmdline = 0, *intptr, value, n;
 	ServerOpCodes opcode;
 	u_short port;
-	u_int i;
+	u_int i, flags = 0;
 	size_t len;
 
 	cp = line;
@@ -461,7 +565,25 @@
 		return 0;
 	intptr = NULL;
 	charptr = NULL;
-	opcode = parse_token(arg, filename, linenum);
+	opcode = parse_token(arg, filename, linenum, &flags);
+
+	if (activep == NULL) { /* We are processing a command line directive */
+		cmdline = 1;
+		activep = &cmdline;
+	}
+	if (*activep && opcode != sMatch)
+		debug3("%s:%d setting %s %s", filename, linenum, arg, cp);
+	if (*activep == 0 && !(flags & SSHCFG_MATCH)) {
+		if (user == NULL) {
+			fatal("%s line %d: Directive '%s' is not allowed "
+			    "within a Match block", filename, linenum, arg);
+		} else { /* this is a directive we have already processed */
+			while (arg)
+				arg = strdelim(&cp);
+			return 0;
+		}
+	}
+
 	switch (opcode) {
 	/* Portable-specific options */
 	case sUsePAM:
@@ -499,7 +621,7 @@
 			fatal("%s line %d: missing integer value.",
 			    filename, linenum);
 		value = atoi(arg);
-		if (*intptr == -1)
+		if (*activep && *intptr == -1)
 			*intptr = value;
 		break;
 
@@ -579,7 +701,7 @@
 		if (!arg || *arg == '\0')
 			fatal("%s line %d: missing file name.",
 			    filename, linenum);
-		if (*charptr == NULL) {
+		if (*activep && *charptr == NULL) {
 			*charptr = tilde_expand_filename(arg, getuid());
 			/* increase optional counter */
 			if (intptr != NULL)
@@ -630,7 +752,7 @@
 		else
 			fatal("%s line %d: Bad yes/no argument: %s",
 				filename, linenum, arg);
-		if (*intptr == -1)
+		if (*activep && *intptr == -1)
 			*intptr = value;
 		break;
 
@@ -895,6 +1017,10 @@
 		if (!arg || *arg == '\0')
 			fatal("%s line %d: Missing subsystem name.",
 			    filename, linenum);
+		if (!*activep) {
+			arg = strdelim(&cp);
+			break;
+		}
 		for (i = 0; i < options->num_subsystems; i++)
 			if (strcmp(arg, options->subsystem_name[i]) == 0)
 				fatal("%s line %d: Subsystem '%s' already defined.",
@@ -977,6 +1103,8 @@
 			if (options->num_accept_env >= MAX_ACCEPT_ENV)
 				fatal("%s line %d: too many allow env.",
 				    filename, linenum);
+			if (!*activep)
+				break;
 			options->accept_env[options->num_accept_env++] =
 			    xstrdup(arg);
 		}
@@ -1004,6 +1132,17 @@
 			*intptr = value;
 		break;
 
+	case sMatch:
+		if (cmdline)
+			fatal("Match directive not supported as a command-line "
+			   "option");
+		value = match_cfg_line(&cp, linenum, user, host, address);
+		if (value < 0)
+			fatal("%s line %d: Bad Match condition", filename,
+			    linenum);
+		*activep = value;
+		break;
+
 	case sDeprecated:
 		logit("%s line %d: Deprecated option %s",
 		    filename, linenum, arg);
@@ -1060,18 +1199,41 @@
 }
 
 void
-parse_server_config(ServerOptions *options, const char *filename, Buffer *conf)
+parse_server_match_config(ServerOptions *options, const char *user,
+    const char *host, const char *address)
 {
-	int linenum, bad_options = 0;
+	ServerOptions mo;
+
+	initialize_server_options(&mo);
+	parse_server_config(&mo, "reprocess config", &cfg, user, host, address);
+	copy_set_server_options(options, &mo);
+}
+
+/* Copy any (supported) values that are set */
+void
+copy_set_server_options(ServerOptions *dst, ServerOptions *src)
+{
+	if (src->allow_tcp_forwarding != -1)
+		dst->allow_tcp_forwarding = src->allow_tcp_forwarding;
+	if (src->gateway_ports != -1)
+		dst->gateway_ports = src->gateway_ports;
+}
+
+void
+parse_server_config(ServerOptions *options, const char *filename, Buffer *conf,
+    const char *user, const char *host, const char *address)
+{
+	int active, linenum, bad_options = 0;
 	char *cp, *obuf, *cbuf;
 
 	debug2("%s: config %s len %d", __func__, filename, buffer_len(conf));
 
 	obuf = cbuf = xstrdup(buffer_ptr(conf));
+	active = user ? 0 : 1;
 	linenum = 1;
 	while ((cp = strsep(&cbuf, "\n")) != NULL) {
 		if (process_server_config_line(options, cp, filename,
-		    linenum++) != 0)
+		    linenum++, &active, user, host, address) != 0)
 			bad_options++;
 	}
 	xfree(obuf);