Feature request: FQDN Host match

Damien Miller djm at mindrot.org
Tue Oct 8 10:48:09 EST 2013



On Mon, 7 Oct 2013, Alexander T wrote:

> Hello!
> 
> I'm hoping that Gmail won't HTML format this mail so that I'll get flamed :)
> 
> Anyway, my question relates to ssh_config. The problem I find is that
> the Host pattern is only applied to the argument given on the command
> line, as outlined in the man page:
> 
> "The host is the hostname argument given on the command line (i.e. the
> name is not converted to a canonicalized host name before matching)."
> 
> I find this problematic, since I have resolv.conf-entries listing
> certain search domains, like:
> 
> search my.very.long.subdomain.at.example.com

The problems are:

1) The resolver doesn't offer a way to figure out what the fully-qualified
name is. Some platforms do, via AI_FQDN - but it isn't widely available
(Windows and OpenBSD only AFAIK)

2) Even if we could get the name, then we couldn't trust it for anything
configured via DHCP anyway.

The only solution I've thought of is to add explicit hostname
canonicalisation options that allow the user to define their own optional
DNS search paths in OpenSSH itself. Here's the patch and explanation:

> This implements explicit client-side hostname canonicalisation. The idea
> is to make host certificates usable for common use - the problem at the
> moment is while host certificates work with fully-qualified names, users
> (quite legitimately) like to type things like "ssh cvs" and the client
> has no good way to figure out how to convert "cvs" to the fully-qualified
> name that the server's certificate will offer.
> 
> A similar problem exists with plain host keys, but users just usually
> accept all the synonyms for the hosts that they refer to by unqualified
> names. Some (like me) use HostKeyAlias to manually work around it, but
> it's a kludge.
> 
> This adds a few new options to allow a client to explicitly convert an
> unqualified (or underqualified) hostname into a fully-qualified one:
> 
> CanonicaliseHostname => turns on/off the canonicalisation
> CanonicalDomains => specifies suffixes appended to qualify a bare name
> CanonicaliseMaxDots => specifies how to tell if a name is underqualified
> CanonicaliseFallbackLocal => if 'no', error if canonicalisation failed
> CanonicalisePermittedCNAMEs => specifies rules for when to follow CNAMEs
> 
> We can't use the system resolver for two reasons: first, the facilities
> that we need aren't standard. AI_FQDN offers most of what we need, but
> isn't compatible with AI_CANONNAME. Second, we can't trust the resolver's
> configuration in dhcpful environments - a rogue server could return an
> attacker-controlled search list. 

I wrote it mostly to make host certificates work better, but it works
for regular hostnames too.

Warning: patch is only lightly tested.

Index: canohost.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/canohost.c,v
retrieving revision 1.67
diff -u -p -r1.67 canohost.c
--- canohost.c	17 May 2013 00:13:13 -0000	1.67
+++ canohost.c	12 Sep 2013 11:07:09 -0000
@@ -45,7 +45,6 @@ static char *
 get_remote_hostname(int sock, int use_dns)
 {
 	struct sockaddr_storage from;
-	int i;
 	socklen_t fromlen;
 	struct addrinfo hints, *ai, *aitop;
 	char name[NI_MAXHOST], ntop[NI_MAXHOST], ntop2[NI_MAXHOST];
@@ -91,13 +90,9 @@ get_remote_hostname(int sock, int use_dn
 		return xstrdup(ntop);
 	}
 
-	/*
-	 * Convert it to all lowercase (which is expected by the rest
-	 * of this software).
-	 */
-	for (i = 0; name[i]; i++)
-		if (isupper(name[i]))
-			name[i] = (char)tolower(name[i]);
+	/* Names are stores in lowercase. */
+	lowercase(name);
+
 	/*
 	 * Map it back to an IP address and check that the given
 	 * address actually is an address of this host.  This is
Index: misc.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/misc.c,v
retrieving revision 1.91
diff -u -p -r1.91 misc.c
--- misc.c	12 Jul 2013 00:43:50 -0000	1.91
+++ misc.c	12 Sep 2013 11:07:09 -0000
@@ -35,6 +35,7 @@
 #include <netinet/ip.h>
 #include <netinet/tcp.h>
 
+#include <ctype.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <netdb.h>
@@ -987,3 +988,11 @@ iptos2str(int iptos)
 	snprintf(iptos_str, sizeof iptos_str, "0x%02x", iptos);
 	return iptos_str;
 }
+
+void
+lowercase(char *s)
+{
+	for (; *s; s++)
+		*s = tolower((u_char)*s);
+}
+
Index: misc.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/misc.h,v
retrieving revision 1.49
diff -u -p -r1.49 misc.h
--- misc.h	1 Jun 2013 13:15:52 -0000	1.49
+++ misc.h	12 Sep 2013 11:07:09 -0000
@@ -36,6 +36,7 @@ void	 sanitise_stdfd(void);
 void	 ms_subtract_diff(struct timeval *, int *);
 void	 ms_to_timeval(struct timeval *, int);
 time_t	 monotime(void);
+void	 lowercase(char *s);
 
 struct passwd *pwcopy(struct passwd *);
 const char *ssh_gai_strerror(int);
Index: readconf.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/readconf.c,v
retrieving revision 1.205
diff -u -p -r1.205 readconf.c
--- readconf.c	20 Aug 2013 00:11:37 -0000	1.205
+++ readconf.c	12 Sep 2013 11:07:10 -0000
@@ -133,6 +133,8 @@ typedef enum {
 	oTunnel, oTunnelDevice, oLocalCommand, oPermitLocalCommand,
 	oVisualHostKey, oUseRoaming, oZeroKnowledgePasswordAuthentication,
 	oKexAlgorithms, oIPQoS, oRequestTTY, oIgnoreUnknown, oProxyUseFdpass,
+	oCanonicalDomains, oCanonicaliseHostname, oCanonicaliseMaxDots,
+	oCanonicaliseFallbackLocal, oCanonicalisePermittedCNAMEs,
 	oIgnoredUnknownOption, oDeprecated, oUnsupported
 } OpCodes;
 
@@ -245,6 +247,11 @@ static struct {
 	{ "ipqos", oIPQoS },
 	{ "requesttty", oRequestTTY },
 	{ "proxyusefdpass", oProxyUseFdpass },
+	{ "canonicaldomains", oCanonicalDomains },
+	{ "canonicalisefallbacklocal", oCanonicaliseFallbackLocal },
+	{ "canonicalisehostname", oCanonicaliseHostname },
+	{ "canonicalisemaxdots", oCanonicaliseMaxDots },
+	{ "canonicalisepermittedcnames", oCanonicalisePermittedCNAMEs },
 	{ "ignoreunknown", oIgnoreUnknown },
 
 	{ NULL, oBadOption }
@@ -343,6 +350,34 @@ add_identity_file(Options *options, cons
 	options->identity_files[options->num_identity_files++] = path;
 }
 
+/* Check and prepare a domain name: removes trailing '.' and lowercases */
+static void
+valid_domain(char *name, const char *filename, int linenum)
+{
+	size_t i, l = strlen(name);
+	u_char c, last = '\0';
+
+	if (l == 0)
+		fatal("%s line %d: empty hostname suffix", filename, linenum);
+	if (!isalpha((u_char)name[0]) && !isdigit((u_char)name[0]))
+		fatal("%s line %d: hostname suffix \"%.100s\" "
+		    "starts with invalid character", filename, linenum, name);
+	for (i = 0; i < l; i++) {
+		c = tolower((u_char)name[i]);
+		name[i] = (char)c;
+		if (last == '.' && c == '.')
+			fatal("%s line %d: hostname suffix \"%.100s\" contains "
+			    "consecutive separators", filename, linenum, name);
+		if (c != '.' && c != '-' && !isalnum(c) &&
+		    c != '_') /* technically invalid, but common */
+			fatal("%s line %d: hostname suffix \"%.100s\" contains "
+			    "invalid characters", filename, linenum, name);
+		last = c;
+	}
+	if (name[l - 1] == '.')
+		name[l - 1] = '\0';
+}
+
 /*
  * Returns the number of the token pointed to by cp or oBadOption.
  */
@@ -364,6 +399,69 @@ parse_token(const char *cp, const char *
 	return oBadOption;
 }
 
+/* Multistate option parsing */
+struct multistate {
+	char *key;
+	int value;
+};
+static const struct multistate multistate_flag[] = {
+	{ "true",			1 },
+	{ "false",			0 },
+	{ "yes",			1 },
+	{ "no",				0 },
+	{ NULL, -1 }
+};
+static const struct multistate multistate_yesnoask[] = {
+	{ "true",			1 },
+	{ "false",			0 },
+	{ "yes",			1 },
+	{ "no",				0 },
+	{ "ask",			2 },
+	{ NULL, -1 }
+};
+static const struct multistate multistate_addressfamily[] = {
+	{ "inet",			AF_INET },
+	{ "inet6",			AF_INET6 },
+	{ "any",			AF_UNSPEC },
+	{ NULL, -1 }
+};
+static const struct multistate multistate_controlmaster[] = {
+	{ "true",			SSHCTL_MASTER_YES },
+	{ "yes",			SSHCTL_MASTER_YES },
+	{ "false",			SSHCTL_MASTER_NO },
+	{ "no",				SSHCTL_MASTER_NO },
+	{ "auto",			SSHCTL_MASTER_AUTO },
+	{ "ask",			SSHCTL_MASTER_ASK },
+	{ "autoask",			SSHCTL_MASTER_AUTO_ASK },
+	{ NULL, -1 }
+};
+static const struct multistate multistate_tunnel[] = {
+	{ "ethernet",			SSH_TUNMODE_ETHERNET },
+	{ "point-to-point",		SSH_TUNMODE_POINTOPOINT },
+	{ "true",			SSH_TUNMODE_DEFAULT },
+	{ "yes",			SSH_TUNMODE_DEFAULT },
+	{ "false",			SSH_TUNMODE_NO },
+	{ "no",				SSH_TUNMODE_NO },
+	{ NULL, -1 }
+};
+static const struct multistate multistate_requesttty[] = {
+	{ "true",			REQUEST_TTY_YES },
+	{ "yes",			REQUEST_TTY_YES },
+	{ "false",			REQUEST_TTY_NO },
+	{ "no",				REQUEST_TTY_NO },
+	{ "force",			REQUEST_TTY_FORCE },
+	{ "auto",			REQUEST_TTY_AUTO },
+	{ NULL, -1 }
+};
+static const struct multistate multistate_canonicalisehostname[] = {
+	{ "true",			SSH_CANONICALISE_YES },
+	{ "false",			SSH_CANONICALISE_NO },
+	{ "yes",			SSH_CANONICALISE_YES },
+	{ "no",				SSH_CANONICALISE_NO },
+	{ "always",			SSH_CANONICALISE_ALWAYS },
+	{ NULL, -1 }
+};
+
 /*
  * Processes a single option line as used in the configuration files. This
  * only sets those values that have not already been set.
@@ -383,6 +481,8 @@ process_config_line(Options *options, co
 	long long val64;
 	size_t len;
 	Forward fwd;
+	const struct multistate *multistate_ptr;
+	struct allowed_cname *cname;
 
 	/* Strip trailing whitespace */
 	for (len = strlen(line) - 1; len > 0; len--) {
@@ -432,17 +532,23 @@ parse_time:
 
 	case oForwardAgent:
 		intptr = &options->forward_agent;
-parse_flag:
+ parse_flag:
+		multistate_ptr = multistate_flag;
+ parse_multistate:
 		arg = strdelim(&s);
 		if (!arg || *arg == '\0')
-			fatal("%.200s line %d: Missing yes/no argument.", filename, linenum);
-		value = 0;	/* To avoid compiler warning... */
-		if (strcmp(arg, "yes") == 0 || strcmp(arg, "true") == 0)
-			value = 1;
-		else if (strcmp(arg, "no") == 0 || strcmp(arg, "false") == 0)
-			value = 0;
-		else
-			fatal("%.200s line %d: Bad yes/no argument.", filename, linenum);
+			fatal("%s line %d: missing argument.",
+			    filename, linenum);
+		value = -1;
+		for (i = 0; multistate_ptr[i].key != NULL; i++) {
+			if (strcasecmp(arg, multistate_ptr[i].key) == 0) {
+				value = multistate_ptr[i].value;
+				break;
+			}
+		}
+		if (value == -1)
+			fatal("%s line %d: unsupported option \"%s\".",
+			    filename, linenum, arg);
 		if (*activep && *intptr == -1)
 			*intptr = value;
 		break;
@@ -525,27 +631,13 @@ parse_flag:
 
 	case oVerifyHostKeyDNS:
 		intptr = &options->verify_host_key_dns;
-		goto parse_yesnoask;
+		multistate_ptr = multistate_yesnoask;
+		goto parse_multistate;
 
 	case oStrictHostKeyChecking:
 		intptr = &options->strict_host_key_checking;
-parse_yesnoask:
-		arg = strdelim(&s);
-		if (!arg || *arg == '\0')
-			fatal("%.200s line %d: Missing yes/no/ask argument.",
-			    filename, linenum);
-		value = 0;	/* To avoid compiler warning... */
-		if (strcmp(arg, "yes") == 0 || strcmp(arg, "true") == 0)
-			value = 1;
-		else if (strcmp(arg, "no") == 0 || strcmp(arg, "false") == 0)
-			value = 0;
-		else if (strcmp(arg, "ask") == 0)
-			value = 2;
-		else
-			fatal("%.200s line %d: Bad yes/no/ask argument.", filename, linenum);
-		if (*activep && *intptr == -1)
-			*intptr = value;
-		break;
+		multistate_ptr = multistate_yesnoask;
+		goto parse_multistate;
 
 	case oCompression:
 		intptr = &options->compression;
@@ -871,22 +963,9 @@ parse_int:
 		break;
 
 	case oAddressFamily:
-		arg = strdelim(&s);
-		if (!arg || *arg == '\0')
-			fatal("%s line %d: missing address family.",
-			    filename, linenum);
 		intptr = &options->address_family;
-		if (strcasecmp(arg, "inet") == 0)
-			value = AF_INET;
-		else if (strcasecmp(arg, "inet6") == 0)
-			value = AF_INET6;
-		else if (strcasecmp(arg, "any") == 0)
-			value = AF_UNSPEC;
-		else
-			fatal("Unsupported AddressFamily \"%s\"", arg);
-		if (*activep && *intptr == -1)
-			*intptr = value;
-		break;
+		multistate_ptr = multistate_addressfamily;
+		goto parse_multistate;
 
 	case oEnableSSHKeysign:
 		intptr = &options->enable_ssh_keysign;
@@ -925,27 +1004,8 @@ parse_int:
 
 	case oControlMaster:
 		intptr = &options->control_master;
-		arg = strdelim(&s);
-		if (!arg || *arg == '\0')
-			fatal("%.200s line %d: Missing ControlMaster argument.",
-			    filename, linenum);
-		value = 0;	/* To avoid compiler warning... */
-		if (strcmp(arg, "yes") == 0 || strcmp(arg, "true") == 0)
-			value = SSHCTL_MASTER_YES;
-		else if (strcmp(arg, "no") == 0 || strcmp(arg, "false") == 0)
-			value = SSHCTL_MASTER_NO;
-		else if (strcmp(arg, "auto") == 0)
-			value = SSHCTL_MASTER_AUTO;
-		else if (strcmp(arg, "ask") == 0)
-			value = SSHCTL_MASTER_ASK;
-		else if (strcmp(arg, "autoask") == 0)
-			value = SSHCTL_MASTER_AUTO_ASK;
-		else
-			fatal("%.200s line %d: Bad ControlMaster argument.",
-			    filename, linenum);
-		if (*activep && *intptr == -1)
-			*intptr = value;
-		break;
+		multistate_ptr = multistate_controlmaster;
+		goto parse_multistate;
 
 	case oControlPersist:
 		/* no/false/yes/true, or a time spec */
@@ -977,25 +1037,8 @@ parse_int:
 
 	case oTunnel:
 		intptr = &options->tun_open;
-		arg = strdelim(&s);
-		if (!arg || *arg == '\0')
-			fatal("%s line %d: Missing yes/point-to-point/"
-			    "ethernet/no argument.", filename, linenum);
-		value = 0;	/* silence compiler */
-		if (strcasecmp(arg, "ethernet") == 0)
-			value = SSH_TUNMODE_ETHERNET;
-		else if (strcasecmp(arg, "point-to-point") == 0)
-			value = SSH_TUNMODE_POINTOPOINT;
-		else if (strcasecmp(arg, "yes") == 0)
-			value = SSH_TUNMODE_DEFAULT;
-		else if (strcasecmp(arg, "no") == 0)
-			value = SSH_TUNMODE_NO;
-		else
-			fatal("%s line %d: Bad yes/point-to-point/ethernet/"
-			    "no argument: %s", filename, linenum, arg);
-		if (*activep)
-			*intptr = value;
-		break;
+		multistate_ptr = multistate_tunnel;
+		goto parse_multistate;
 
 	case oTunnelDevice:
 		arg = strdelim(&s);
@@ -1044,24 +1087,9 @@ parse_int:
 		goto parse_flag;
 
 	case oRequestTTY:
-		arg = strdelim(&s);
-		if (!arg || *arg == '\0')
-			fatal("%s line %d: missing argument.",
-			    filename, linenum);
 		intptr = &options->request_tty;
-		if (strcasecmp(arg, "yes") == 0)
-			value = REQUEST_TTY_YES;
-		else if (strcasecmp(arg, "no") == 0)
-			value = REQUEST_TTY_NO;
-		else if (strcasecmp(arg, "force") == 0)
-			value = REQUEST_TTY_FORCE;
-		else if (strcasecmp(arg, "auto") == 0)
-			value = REQUEST_TTY_AUTO;
-		else
-			fatal("Unsupported RequestTTY \"%s\"", arg);
-		if (*activep && *intptr == -1)
-			*intptr = value;
-		break;
+		multistate_ptr = multistate_requesttty;
+		goto parse_multistate;
 
 	case oIgnoreUnknown:
 		charptr = &options->ignored_unknown;
@@ -1071,6 +1099,62 @@ parse_int:
 		intptr = &options->proxy_use_fdpass;
 		goto parse_flag;
 
+	case oCanonicalDomains:
+		value = options->num_canonical_domains != 0;
+		while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+			valid_domain(arg, filename, linenum);
+			if (!*activep || value)
+				continue;
+			if (options->num_canonical_domains >= MAX_CANON_DOMAINS)
+				fatal("%s line %d: too many hostname suffixes.",
+				    filename, linenum);
+			options->canonical_domains[
+			    options->num_canonical_domains++] = xstrdup(arg);
+		}
+		break;
+
+	case oCanonicalisePermittedCNAMEs:
+		value = options->num_permitted_cnames != 0;
+		while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+			/* Either '*' for everything or 'list:list' */
+			if (strcmp(arg, "*") == 0)
+				arg2 = arg;
+			else {
+				lowercase(arg);
+				if ((arg2 = strchr(arg, ':')) == NULL ||
+				    arg2[1] == '\0') {
+					fatal("%s line %d: "
+					    "Invalid permitted CNAME \"%s\"",
+					    filename, linenum, arg);
+				}
+				*arg2 = '\0';
+				arg2++;
+			}
+			if (!*activep || value)
+				continue;
+			if (options->num_permitted_cnames >= MAX_CANON_DOMAINS)
+				fatal("%s line %d: too many permitted CNAMEs.",
+				    filename, linenum);
+			cname = options->permitted_cnames +
+			    options->num_permitted_cnames++;
+			cname->source_list = xstrdup(arg);
+			cname->target_list = xstrdup(arg2);
+		}
+		break;
+
+	case oCanonicaliseHostname:
+		intptr = &options->canonicalise_hostname;
+		multistate_ptr = multistate_canonicalisehostname;
+		goto parse_multistate;
+
+	case oCanonicaliseMaxDots:
+		intptr = &options->canonicalise_max_dots;
+		goto parse_int;
+
+	case oCanonicaliseFallbackLocal:
+		intptr = &options->canonicalise_fallback_local;
+		goto parse_flag;
+
 	case oDeprecated:
 		debug("%s line %d: Deprecated option \"%s\"",
 		    filename, linenum, keyword);
@@ -1234,6 +1318,11 @@ initialize_options(Options * options)
 	options->request_tty = -1;
 	options->proxy_use_fdpass = -1;
 	options->ignored_unknown = NULL;
+	options->num_canonical_domains = 0;
+	options->num_permitted_cnames = 0;
+	options->canonicalise_max_dots = -1;
+	options->canonicalise_fallback_local = -1;
+	options->canonicalise_hostname = -1;
 }
 
 /*
@@ -1385,8 +1474,22 @@ fill_default_options(Options * options)
 		options->request_tty = REQUEST_TTY_AUTO;
 	if (options->proxy_use_fdpass == -1)
 		options->proxy_use_fdpass = 0;
-	/* options->local_command should not be set by default */
-	/* options->proxy_command should not be set by default */
+	if (options->canonicalise_max_dots == -1)
+		options->canonicalise_max_dots = 1;
+	if (options->canonicalise_fallback_local == -1)
+		options->canonicalise_fallback_local = 1;
+	if (options->canonicalise_hostname == -1)
+		options->canonicalise_hostname = SSH_CANONICALISE_NO;
+#define CLEAR_ON_NONE(v) \
+	do { \
+		if (v != NULL && strcasecmp(v, "none") == 0) { \
+			free(v); \
+			v = NULL; \
+		} \
+	} while(0)
+	CLEAR_ON_NONE(options->local_command);
+	CLEAR_ON_NONE(options->proxy_command);
+	CLEAR_ON_NONE(options->control_path);
 	/* options->user will be set in the main program if appropriate */
 	/* options->hostname will be set in the main program if appropriate */
 	/* options->host_key_alias should not be set by default */
Index: readconf.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/readconf.h,v
retrieving revision 1.96
diff -u -p -r1.96 readconf.h
--- readconf.h	20 Aug 2013 00:11:38 -0000	1.96
+++ readconf.h	12 Sep 2013 11:07:10 -0000
@@ -29,7 +29,13 @@ typedef struct {
 /* Data structure for representing option data. */
 
 #define MAX_SEND_ENV		256
-#define SSH_MAX_HOSTS_FILES	256
+#define SSH_MAX_HOSTS_FILES	32
+#define MAX_CANON_DOMAINS	32
+
+struct allowed_cname {
+	char *source_list;
+	char *target_list;
+};
 
 typedef struct {
 	int     forward_agent;	/* Forward authentication agent. */
@@ -140,8 +146,20 @@ typedef struct {
 
 	int	proxy_use_fdpass;
 
+	int	num_canonical_domains;
+	char	*canonical_domains[MAX_CANON_DOMAINS];
+	int	canonicalise_hostname;
+	int	canonicalise_max_dots;
+	int	canonicalise_fallback_local;
+	int	num_permitted_cnames;
+	struct allowed_cname permitted_cnames[MAX_CANON_DOMAINS];
+
 	char	*ignored_unknown; /* Pattern list of unknown tokens to ignore */
 }       Options;
+
+#define SSH_CANONICALISE_NO	0
+#define SSH_CANONICALISE_YES	1
+#define SSH_CANONICALISE_ALWAYS	2
 
 #define SSHCTL_MASTER_NO	0
 #define SSHCTL_MASTER_YES	1
Index: roaming_client.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/roaming_client.c,v
retrieving revision 1.5
diff -u -p -r1.5 roaming_client.c
--- roaming_client.c	17 May 2013 00:13:14 -0000	1.5
+++ roaming_client.c	12 Sep 2013 11:07:10 -0000
@@ -255,10 +255,10 @@ wait_for_roaming_reconnect(void)
 		if (c != '\n' && c != '\r')
 			continue;
 
-		if (ssh_connect(host, &hostaddr, options.port,
+		if (ssh_connect(host, NULL, &hostaddr, options.port,
 		    options.address_family, 1, &timeout_ms,
-		    options.tcp_keep_alive, options.use_privileged_port,
-		    options.proxy_command) == 0 && roaming_resume() == 0) {
+		    options.tcp_keep_alive, options.use_privileged_port) == 0 &&
+		    roaming_resume() == 0) {
 			packet_restore_state();
 			reenter_guard = 0;
 			fprintf(stderr, "[connection resumed]\n");
Index: ssh.1
===================================================================
RCS file: /cvs/src/usr.bin/ssh/ssh.1,v
retrieving revision 1.336
diff -u -p -r1.336 ssh.1
--- ssh.1	20 Aug 2013 06:56:07 -0000	1.336
+++ ssh.1	12 Sep 2013 11:07:10 -0000
@@ -417,6 +417,11 @@ For full details of the options listed b
 .It AddressFamily
 .It BatchMode
 .It BindAddress
+.It CanonicalDomains
+.It CanonicaliseFallbackLocal
+.It CanonicaliseHostname
+.It CanonicaliseMaxDots
+.It CanonicalisePermittedCNAMEs
 .It ChallengeResponseAuthentication
 .It CheckHostIP
 .It Cipher
Index: ssh.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/ssh.c,v
retrieving revision 1.381
diff -u -p -r1.381 ssh.c
--- ssh.c	25 Jul 2013 00:29:10 -0000	1.381
+++ ssh.c	12 Sep 2013 11:07:10 -0000
@@ -218,6 +218,134 @@ tilde_expand_paths(char **paths, u_int n
 	}
 }
 
+static struct addrinfo *
+resolve_host(const char *name, u_int port, int logerr, char *cname, size_t clen)
+{
+	char strport[NI_MAXSERV];
+	struct addrinfo hints, *res;
+	int gaierr, loglevel = SYSLOG_LEVEL_DEBUG1;
+
+	snprintf(strport, sizeof strport, "%u", port);
+	bzero(&hints, sizeof(hints));
+	hints.ai_family = options.address_family;
+	hints.ai_socktype = SOCK_STREAM;
+	if (cname != NULL)
+		hints.ai_flags = AI_CANONNAME;
+	if ((gaierr = getaddrinfo(name, strport, &hints, &res)) != 0) {
+		if (logerr || (gaierr != EAI_NONAME && gaierr != EAI_NODATA))
+			loglevel = SYSLOG_LEVEL_ERROR;
+		do_log2(loglevel, "%s: Could not resolve hostname %.100s: %s",
+		    __progname, name, ssh_gai_strerror(gaierr));
+		return NULL;
+	}
+	if (cname != NULL && res->ai_canonname != NULL) {
+		if (strlcpy(cname, res->ai_canonname, clen) >= clen) {
+			error("%s: host \"%s\" cname \"%s\" too long (max %lu)",
+			    __func__, name,  res->ai_canonname, (u_long)clen);
+			if (clen > 0)
+				*cname = '\0';
+		}
+	}
+	return res;
+}
+
+/*
+ * Check whether the cname is a permitted replacement for the hostname
+ * and perform the replacement if it is.
+ */
+static int
+check_follow_cname(char **namep, const char *cname)
+{
+	int i;
+	struct allowed_cname *rule;
+
+	if (*cname == '\0' || options.num_permitted_cnames == 0 ||
+	    strcmp(*namep, cname) == 0)
+		return 0;
+	if (options.canonicalise_hostname == SSH_CANONICALISE_NO)
+		return 0;
+	/*
+	 * Don't attempt to canonicalise names that will be interpreted by
+	 * a proxy unless the user specifically requests so.
+	 */
+	if (options.proxy_command != NULL &&
+	    options.canonicalise_hostname != SSH_CANONICALISE_ALWAYS)
+		return 0;
+	debug3("%s: check \"%s\" CNAME \"%s\"", __func__, *namep, cname);
+	for (i = 0; i < options.num_permitted_cnames; i++) {
+		rule = options.permitted_cnames + i;
+		if (match_pattern_list(*namep, rule->source_list,
+		    strlen(rule->source_list), 1) != 1 ||
+		    match_pattern_list(cname, rule->target_list,
+		    strlen(rule->target_list), 1) != 1)
+			continue;
+		verbose("Canonicalised DNS aliased hostname "
+		    "\"%s\" => \"%s\"", *namep, cname);
+		free(*namep);
+		*namep = xstrdup(cname);
+		return 1;
+	}
+	return 0;
+}
+
+/*
+ * Attempt to resolve the supplied hostname after applying the user's
+ * canonicalisation rules. Returns the address list for the host or NULL
+ * if no name was found after canonicalisation.
+ */
+static struct addrinfo *
+resolve_canonicalise(char **hostp, u_int port)
+{
+	int i, ndots;
+	char *cp, *fullhost, cname_target[NI_MAXHOST];
+	struct addrinfo *addrs;
+
+	if (options.canonicalise_hostname == SSH_CANONICALISE_NO)
+		return NULL;
+	/*
+	 * Don't attempt to canonicalise names that will be interpreted by
+	 * a proxy unless the user specifically requests so.
+	 */
+	if (options.proxy_command != NULL &&
+	    options.canonicalise_hostname != SSH_CANONICALISE_ALWAYS)
+		return NULL;
+	/* Don't apply canonicalisation to sufficiently-qualified hostnames */
+	ndots = 0;
+	for (cp = *hostp; *cp != '\0'; cp++) {
+		if (*cp == '.')
+			ndots++;
+	}
+	if (ndots > options.canonicalise_max_dots) {
+		debug3("%s: not canonicalising hostname \"%s\" (max dots %d)",
+		    __func__, *hostp, options.canonicalise_max_dots);
+		return NULL;
+	}
+	/* Attempt each supplied suffix */
+	for (i = 0; i < options.num_canonical_domains; i++) {
+		*cname_target = '\0';
+		xasprintf(&fullhost, "%s.%s.", *hostp,
+		    options.canonical_domains[i]);
+		if ((addrs = resolve_host(fullhost, options.port, 0,
+		    cname_target, sizeof(cname_target))) == NULL) {
+			free(fullhost);
+			continue;
+		}
+		/* Remove trailing '.' */
+		fullhost[strlen(fullhost) - 1] = '\0';
+		/* Follow CNAME if requested */
+		if (!check_follow_cname(&fullhost, cname_target)) {
+			debug("Canonicalised hostname \"%s\" => \"%s\"",
+			    *hostp, fullhost);
+		}
+		free(*hostp);
+		*hostp = fullhost;
+		return addrs;
+	}
+	if (!options.canonicalise_fallback_local)
+		fatal("%s: Could not resolve host \"%s\"", __progname, host);
+	return NULL;
+}
+
 /*
  * Main program for the ssh client.
  */
@@ -227,6 +355,7 @@ main(int ac, char **av)
 	int i, r, opt, exit_status, use_syslog;
 	char *p, *cp, *line, *argv0, buf[MAXPATHLEN], *host_arg, *logfile;
 	char thishost[NI_MAXHOST], shorthost[NI_MAXHOST], portstr[NI_MAXSERV];
+	char cname[NI_MAXHOST];
 	struct stat st;
 	struct passwd *pw;
 	int dummy, timeout_ms;
@@ -234,6 +363,7 @@ main(int ac, char **av)
 	extern char *optarg;
 	struct servent *sp;
 	Forward fwd;
+	struct addrinfo *addrs = NULL;
 
 	/* Ensure that fds 0, 1 and 2 are open or directed to /dev/null */
 	sanitise_stdfd();
@@ -604,9 +734,9 @@ main(int ac, char **av)
 				usage();
 			options.user = p;
 			*cp = '\0';
-			host = ++cp;
+			host = xstrdup(++cp);
 		} else
-			host = *av;
+			host = xstrdup(*av);
 		if (ac > 1) {
 			optind = optreset = 1;
 			goto again;
@@ -618,6 +748,9 @@ main(int ac, char **av)
 	if (!host)
 		usage();
 
+	lowercase(host);
+	host_arg = xstrdup(host);
+
 	OpenSSL_add_all_algorithms();
 	ERR_load_crypto_strings();
 
@@ -694,6 +827,14 @@ main(int ac, char **av)
 
 	channel_set_af(options.address_family);
 
+	/* Tidy and check options */
+	if (options.host_key_alias != NULL)
+		lowercase(options.host_key_alias);
+	if (options.proxy_command != NULL &&
+	    strcmp(options.proxy_command, "-") == 0 &&
+	    options.proxy_use_fdpass)
+		fatal("ProxyCommand=- and ProxyUseFDPass are incompatible");
+
 	/* reinit */
 	log_init(argv0, options.log_level, SYSLOG_FACILITY_USER, !use_syslog);
 
@@ -727,10 +868,26 @@ main(int ac, char **av)
 	}
 
 	/* preserve host name given on command line for %n expansion */
-	host_arg = host;
 	if (options.hostname != NULL) {
-		host = percent_expand(options.hostname,
+		cp = percent_expand(options.hostname,
 		    "h", host, (char *)NULL);
+		free(host);
+		host = cp;
+	}
+
+	/* If canonicalisation requested then try to apply it */
+	if (options.canonicalise_hostname != SSH_CANONICALISE_NO)
+		addrs = resolve_canonicalise(&host, options.port);
+	/*
+	 * If canonicalisation not requested, or if it failed then try to
+	 * resolve the bare hostname name using the system resolver's usual
+	 * search rules.
+	 */
+	if (addrs == NULL) {
+		if ((addrs = resolve_host(host, options.port, 1,
+		    cname, sizeof(cname))) == NULL)
+			cleanup_exit(255); /* resolve_host logs the error */
+		check_follow_cname(&host, cname);
 	}
 
 	if (gethostname(thishost, sizeof(thishost)) == -1)
@@ -750,24 +907,6 @@ main(int ac, char **av)
 		free(cp);
 	}
 
-	/* force lowercase for hostkey matching */
-	if (options.host_key_alias != NULL) {
-		for (p = options.host_key_alias; *p; p++)
-			if (isupper(*p))
-				*p = (char)tolower(*p);
-	}
-
-	if (options.proxy_command != NULL &&
-	    strcmp(options.proxy_command, "none") == 0) {
-		free(options.proxy_command);
-		options.proxy_command = NULL;
-	}
-	if (options.control_path != NULL &&
-	    strcmp(options.control_path, "none") == 0) {
-		free(options.control_path);
-		options.control_path = NULL;
-	}
-
 	if (options.control_path != NULL) {
 		cp = tilde_expand_filename(options.control_path,
 		    original_real_uid);
@@ -786,13 +925,16 @@ main(int ac, char **av)
 	timeout_ms = options.connection_timeout * 1000;
 
 	/* Open a connection to the remote host. */
-	if (ssh_connect(host, &hostaddr, options.port,
-	    options.address_family, options.connection_attempts, &timeout_ms,
-	    options.tcp_keep_alive, 
-	    original_effective_uid == 0 && options.use_privileged_port,
-	    options.proxy_command) != 0)
+	if (ssh_connect(host, addrs, &hostaddr, options.port,
+	    options.address_family, options.connection_attempts,
+	    &timeout_ms, options.tcp_keep_alive, 
+	    original_effective_uid == 0 && options.use_privileged_port) != 0)
 		exit(255);
 
+	freeaddrinfo(addrs);
+	packet_set_timeout(options.server_alive_interval,
+	    options.server_alive_count_max);
+
 	if (timeout_ms > 0)
 		debug3("timeout: %d ms remain after connect", timeout_ms);
 
@@ -1584,4 +1726,3 @@ main_sigchld_handler(int sig)
 	signal(sig, main_sigchld_handler);
 	errno = save_errno;
 }
-
Index: ssh_config.5
===================================================================
RCS file: /cvs/src/usr.bin/ssh/ssh_config.5,v
retrieving revision 1.168
diff -u -p -r1.168 ssh_config.5
--- ssh_config.5	20 Aug 2013 06:56:07 -0000	1.168
+++ ssh_config.5	12 Sep 2013 11:07:11 -0000
@@ -152,6 +152,77 @@ Note that this option does not work if
 .Cm UsePrivilegedPort
 is set to
 .Dq yes .
+.It Cm CanonicalDomains
+when
+.Cm CanonicaliseHostname
+is enabled, this option specifies the list of domain suffixes in which to
+search for the specified destination host.
+.It Cm CanonicaliseFallbackLocal
+specified whether to fail with an error when hostname canonicalisation fails.
+The default of
+.Dq no
+will attempt to lookup the unqualified hostname using the system resolver's
+search rules.
+A value of
+.Dq yes
+will cause
+.Xr ssh 1
+to fail instantly if
+.Cm CanonicaliseHostname
+is enabled and the target hostname cannot be found in any of the domains
+specified by
+.Cm CanonicalDomains .
+.It Cm CanonicaliseHostname
+controls whether explicit hostname canonicalisation is performed.
+The default
+.Dq no
+is not to perform any name rewriting and let the system resolver handle all
+hostname lookups.
+If set to
+.Dq yes
+then, for connections that do not use a
+.Cm ProxyCommand ,
+.Xr ssh 1
+will attempt to canonicalise the hostname specified on the command line
+using the
+.Cm CanonicalDomains
+suffixes and
+.Cm CanonicalisePermittedCNAMEs
+rules.
+If
+.Cm CanonicaliseHostname
+is set to
+.Dq always ,
+then canonicalisation is applied to proxied connections to.
+.It Cm CanonicaliseMaxDots
+specifies the maximum number of dot characters in a hostname name before
+canonicalisation is disabled.
+The default of
+.Dq 1
+allows a single dot (i.e. hostname.subdomain)
+.It Cm CanonicalisePermittedCNAMEs
+specifies rules to determine whether CNAMEs should be followed when
+canonicalising hostnames.
+The rules consist of one or more arguments of
+.Sm off
+.Ar source_domain_list : Ar target_domain_list
+.Sm on
+where
+.Ar source_domain_list
+is a pattern-list of domains that are may follow CNAMEs in canonicalisation
+and
+.Ar target_domain_list
+is a pattern-list of domains that they may resove to.
+.Pp
+For example,
+.Dq *.a.example.com:*.b.example.com,*.c.example.com
+will allow hostnames matching
+.Dq *.a.example.com
+to be canonicalised to names in the
+.Dq *.b.example.com
+or
+.Dq *.c.example.com
+domains.
 .It Cm ChallengeResponseAuthentication
 Specifies whether to use challenge-response authentication.
 The argument to this keyword must be
Index: sshconnect.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/sshconnect.c,v
retrieving revision 1.239
diff -u -p -r1.239 sshconnect.c
--- sshconnect.c	20 Aug 2013 00:11:38 -0000	1.239
+++ sshconnect.c	12 Sep 2013 11:07:11 -0000
@@ -76,7 +76,7 @@ expand_proxy_command(const char *proxy_c
 {
 	char *tmp, *ret, strport[NI_MAXSERV];
 
-	snprintf(strport, sizeof strport, "%hu", port);
+	snprintf(strport, sizeof strport, "%d", port);
 	xasprintf(&tmp, "exec %s", proxy_command);
 	ret = percent_expand(tmp, "h", host, "p", strport,
 	    "r", options.user, (char *)NULL);
@@ -160,8 +160,6 @@ ssh_proxy_fdpass_connect(const char *hos
 
 	/* Set the connection file descriptors. */
 	packet_set_connection(sock, sock);
-	packet_set_timeout(options.server_alive_interval,
-	    options.server_alive_count_max);
 
 	return 0;
 }
@@ -177,16 +175,6 @@ ssh_proxy_connect(const char *host, u_sh
 	pid_t pid;
 	char *shell;
 
-	if (!strcmp(proxy_command, "-")) {
-		packet_set_connection(STDIN_FILENO, STDOUT_FILENO);
-		packet_set_timeout(options.server_alive_interval,
-		    options.server_alive_count_max);
-		return 0;
-	}
-
-	if (options.proxy_use_fdpass)
-		return ssh_proxy_fdpass_connect(host, port, proxy_command);
-
 	if ((shell = getenv("SHELL")) == NULL || *shell == '\0')
 		shell = _PATH_BSHELL;
 
@@ -248,8 +236,6 @@ ssh_proxy_connect(const char *host, u_sh
 
 	/* Set the connection file descriptors. */
 	packet_set_connection(pout[0], pin[1]);
-	packet_set_timeout(options.server_alive_interval,
-	    options.server_alive_count_max);
 
 	/* Indicate OK return */
 	return 0;
@@ -418,33 +404,18 @@ timeout_connect(int sockfd, const struct
  * and %p substituted for host and port, respectively) to use to contact
  * the daemon.
  */
-int
-ssh_connect(const char *host, struct sockaddr_storage * hostaddr,
-    u_short port, int family, int connection_attempts, int *timeout_ms,
-    int want_keepalive, int needpriv, const char *proxy_command)
+static int
+ssh_connect_direct(const char *host, struct addrinfo *aitop,
+    struct sockaddr_storage *hostaddr, u_short port, int family,
+    int connection_attempts, int *timeout_ms, int want_keepalive, int needpriv)
 {
-	int gaierr;
 	int on = 1;
 	int sock = -1, attempt;
 	char ntop[NI_MAXHOST], strport[NI_MAXSERV];
-	struct addrinfo hints, *ai, *aitop;
+	struct addrinfo *ai;
 
 	debug2("ssh_connect: needpriv %d", needpriv);
 
-	/* If a proxy command is given, connect using it. */
-	if (proxy_command != NULL)
-		return ssh_proxy_connect(host, port, proxy_command);
-
-	/* No proxy command. */
-
-	memset(&hints, 0, sizeof(hints));
-	hints.ai_family = family;
-	hints.ai_socktype = SOCK_STREAM;
-	snprintf(strport, sizeof strport, "%u", port);
-	if ((gaierr = getaddrinfo(host, strport, &hints, &aitop)) != 0)
-		fatal("%s: Could not resolve hostname %.100s: %s", __progname,
-		    host, ssh_gai_strerror(gaierr));
-
 	for (attempt = 0; attempt < connection_attempts; attempt++) {
 		if (attempt > 0) {
 			/* Sleep a moment before retrying. */
@@ -456,7 +427,8 @@ ssh_connect(const char *host, struct soc
 		 * sequence until the connection succeeds.
 		 */
 		for (ai = aitop; ai; ai = ai->ai_next) {
-			if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
+			if (ai->ai_family != AF_INET &&
+			    ai->ai_family != AF_INET6)
 				continue;
 			if (getnameinfo(ai->ai_addr, ai->ai_addrlen,
 			    ntop, sizeof(ntop), strport, sizeof(strport),
@@ -489,8 +461,6 @@ ssh_connect(const char *host, struct soc
 			break;	/* Successful connection. */
 	}
 
-	freeaddrinfo(aitop);
-
 	/* Return failure if we didn't get a successful connection. */
 	if (sock == -1) {
 		error("ssh: connect to host %s port %s: %s",
@@ -508,12 +478,28 @@ ssh_connect(const char *host, struct soc
 
 	/* Set the connection. */
 	packet_set_connection(sock, sock);
-	packet_set_timeout(options.server_alive_interval,
-	    options.server_alive_count_max);
 
 	return 0;
 }
 
+int
+ssh_connect(const char *host, struct addrinfo *addrs,
+    struct sockaddr_storage *hostaddr, u_short port, int family,
+    int connection_attempts, int *timeout_ms, int want_keepalive, int needpriv)
+{
+	if (options.proxy_command == NULL) {
+		return ssh_connect_direct(host, addrs, hostaddr, port, family,
+		    connection_attempts, timeout_ms, want_keepalive, needpriv);
+	} else if (strcmp(options.proxy_command, "-") == 0) {
+		packet_set_connection(STDIN_FILENO, STDOUT_FILENO);
+		return 0; /* Always succeeds */
+	} else if (options.proxy_use_fdpass) {
+		return ssh_proxy_fdpass_connect(host, port,
+		    options.proxy_command);
+	}
+	return ssh_proxy_connect(host, port, options.proxy_command);
+}
+
 static void
 send_client_banner(int connection_out, int minor1)
 {
@@ -1238,7 +1224,7 @@ void
 ssh_login(Sensitive *sensitive, const char *orighost,
     struct sockaddr *hostaddr, u_short port, struct passwd *pw, int timeout_ms)
 {
-	char *host, *cp;
+	char *host;
 	char *server_user, *local_user;
 
 	local_user = xstrdup(pw->pw_name);
@@ -1246,9 +1232,7 @@ ssh_login(Sensitive *sensitive, const ch
 
 	/* Convert the user-supplied hostname into all lowercase. */
 	host = xstrdup(orighost);
-	for (cp = host; *cp; cp++)
-		if (isupper(*cp))
-			*cp = (char)tolower(*cp);
+	lowercase(host);
 
 	/* Exchange protocol version identification strings with the server. */
 	ssh_exchange_identification(timeout_ms);
Index: sshconnect.h
===================================================================
RCS file: /cvs/src/usr.bin/ssh/sshconnect.h,v
retrieving revision 1.27
diff -u -p -r1.27 sshconnect.h
--- sshconnect.h	29 Nov 2010 23:45:51 -0000	1.27
+++ sshconnect.h	12 Sep 2013 11:07:11 -0000
@@ -31,9 +31,9 @@ struct Sensitive {
 	int	external_keysign;
 };
 
-int
-ssh_connect(const char *, struct sockaddr_storage *, u_short, int, int,
-    int *, int, int, const char *);
+struct addrinfo;
+int	 ssh_connect(const char *, struct addrinfo *, struct sockaddr_storage *,
+    u_short, int, int, int *, int, int);
 void	 ssh_kill_proxy_command(void);
 
 void	 ssh_login(Sensitive *, const char *, struct sockaddr *, u_short,


More information about the openssh-unix-dev mailing list