From f568270c4ec5b0472e0a49af510fd58f1f65155b Mon Sep 17 00:00:00 2001
From: Mara Sophie Grosch <littlefox@lf-net.org>
Date: Sat, 13 Feb 2021 23:01:02 +0100
Subject: [PATCH 1/3] Resolve SRV record for hostname

Very crude first implementation, needs more polishing.

Only tries to resolve via SRV if a port is not given manually, via
config or command line.

Only the highest-prio lowest-weight host is used currently, it does
not do load balancing! Still useful for non-standard ports.

Working towards fixing https://bugzilla.mindrot.org/show_bug.cgi?id=2217
---
 dns.c |   2 +-
 dns.h |   3 ++
 ssh.c | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
 3 files changed, 135 insertions(+), 14 deletions(-)

diff --git a/dns.c b/dns.c
index 939241440..e4423655e 100644
--- a/dns.c
+++ b/dns.c
@@ -52,7 +52,7 @@ static const char * const errset_text[] = {
 	"data does not exist",	/* 5 ERRSET_NODATA */
 };
 
-static const char *
+const char *
 dns_result_totext(unsigned int res)
 {
 	switch (res) {
diff --git a/dns.h b/dns.h
index 864ab7d00..bba0095db 100644
--- a/dns.h
+++ b/dns.h
@@ -46,6 +46,7 @@ enum sshfp_hashes {
 
 #define DNS_RDATACLASS_IN	1
 #define DNS_RDATATYPE_SSHFP	44
+#define DNS_RDATATYPE_SRV	33
 
 #define DNS_VERIFY_FOUND	0x00000001
 #define DNS_VERIFY_MATCH	0x00000002
@@ -56,4 +57,6 @@ int	verify_host_key_dns(const char *, struct sockaddr *,
     struct sshkey *, int *);
 int	export_dns_rr(const char *, struct sshkey *, FILE *, int, int);
 
+const char* dns_result_totext(unsigned int res);
+
 #endif /* DNS_H */
diff --git a/ssh.c b/ssh.c
index 0019281f4..95871013f 100644
--- a/ssh.c
+++ b/ssh.c
@@ -108,6 +108,7 @@
 #include "ssherr.h"
 #include "myproposal.h"
 #include "utf8.h"
+#include "dns.h"
 
 #ifdef ENABLE_PKCS11
 #include "ssh-pkcs11.h"
@@ -243,6 +244,105 @@ default_client_percent_dollar_expand(const char *str,
 	return ret;
 }
 
+/*
+ * Attempt to resolve _ssh._tcp.$orig_name SRV record to the highest-priority
+ * host and port.
+ * Returns the port of the target service via out-argument
+ * Returns NULL on failure and when no SRV record is found.
+ */
+static char *
+resolve_srv(const char* orig_name, int *port) {
+    const size_t prefix_len = 10;
+    const size_t orig_len = strlen(orig_name);
+    char* name = malloc(prefix_len + orig_len + 1);
+    snprintf(name, prefix_len + orig_len + 1, "_ssh._tcp.%s", orig_name);
+
+    debug3_f("Trying to lookup SRV for: %s (%s)", orig_name, name);
+
+    struct rrsetinfo *srvs = NULL;
+    int result = getrrsetbyname(name, DNS_RDATACLASS_IN, DNS_RDATATYPE_SRV, 0, &srvs);
+
+    if(result) {
+        debug2_f("SRV lookup error: %s", dns_result_totext(result));
+        goto fail;
+    }
+
+    if(!srvs->rri_nrdatas) {
+        debug2_f("No SRV records found for %s", name);
+        goto fail;
+    }
+
+    uint16_t cur_prio = UINT16_MAX;
+    uint16_t cur_weight = 0;
+    uint16_t cur_port   = 0;
+    char*    cur_target = NULL;
+
+    for(unsigned i = 0; i < srvs->rri_nrdatas; ++i) {
+        struct rdatainfo srv = srvs->rri_rdatas[i];
+
+        if(srv.rdi_length >= 9) {
+            uint16_t prio   = ntohs(*(uint16_t*)srv.rdi_data);
+            uint16_t weight = ntohs(*(uint16_t*)(srv.rdi_data + 2));
+            uint16_t port   = ntohs(*(uint16_t*)(srv.rdi_data + 4));
+
+            if(prio < cur_prio || (prio == cur_prio && weight > cur_weight)) {
+                const char* target_labels = srv.rdi_data + 6;
+                size_t      target_index  = 0;
+
+                char*  target = NULL;
+                size_t target_size = 0;
+
+                uint8_t next_label_len;
+                do {
+                    next_label_len = target_labels[target_index];
+
+                    char* old_target = target;
+
+                    if(old_target) {
+                        target = malloc(target_size + next_label_len + 2);
+                        memcpy(target, old_target, target_size);
+                        target[target_size] = '.';
+                        memcpy(target + target_size + 1, target_labels + target_index + 1, next_label_len);
+                        target[target_size + next_label_len + 1] = 0;
+
+                        target_size += 1;
+
+                        free(old_target);
+                    }
+                    else {
+                        target = malloc(next_label_len + 1);
+                        memcpy(target, target_labels + target_index + 1, next_label_len);
+                        target[next_label_len] = 0;
+                    }
+
+                    target_size  += next_label_len;
+                    target_index += next_label_len + 1;
+                } while(next_label_len > 0 && target_index + next_label_len <= srv.rdi_length - 6);
+
+                debug2_f("Found SRV record pointing at %s:%u (weight %u, prio %u)", target, port, weight, prio);
+
+                cur_port   = port;
+                cur_target = target;
+                cur_weight = weight;
+                cur_prio   = prio;
+            }
+        }
+    }
+
+fail:
+    freerrset(srvs);
+    free(name);
+
+    if(cur_target) {
+        debug_f("Connecting via SRV record to %s:%u", cur_target, cur_port);
+
+        *port = cur_port;
+        return cur_target;
+    }
+
+    return NULL;
+}
+
 /*
  * Attempt to resolve a host name / port to a set of addresses and
  * optionally return any CNAMEs encountered along the way.
@@ -250,21 +350,34 @@ default_client_percent_dollar_expand(const char *str,
  * NB. this function must operate with a options having undefined members.
  */
 static struct addrinfo *
-resolve_host(const char *name, int port, int logerr, char *cname, size_t clen)
+resolve_host(const char *orig_name, int *port, int logerr, char *cname, size_t clen)
 {
+    char* name = NULL;
+
+    if(*port <= 0) {
+        name = resolve_srv(orig_name, port);
+    }
+
+    if(!name) {
+        name = malloc(strlen(orig_name) + 1);
+        strcpy(name, orig_name);
+    }
+
 	char strport[NI_MAXSERV];
 	const char *errstr = NULL;
 	struct addrinfo hints, *res;
 	int gaierr;
 	LogLevel loglevel = SYSLOG_LEVEL_DEBUG1;
 
-	if (port <= 0)
-		port = default_ssh_port();
-	if (cname != NULL)
+	if (*port <= 0) {
+		*port = default_ssh_port();
+    }
+	if (cname != NULL) {
 		*cname = '\0';
-	debug3_f("lookup %s:%d", name, port);
+    }
+	debug3_f("lookup %s:%d", name, *port);
 
-	snprintf(strport, sizeof strport, "%d", port);
+	snprintf(strport, sizeof strport, "%d", *port);
 	memset(&hints, 0, sizeof(hints));
 	hints.ai_family = options.address_family == -1 ?
 	    AF_UNSPEC : options.address_family;
@@ -276,6 +389,7 @@ resolve_host(const char *name, int port, int logerr, char *cname, size_t clen)
 			loglevel = SYSLOG_LEVEL_ERROR;
 		do_log2(loglevel, "%s: Could not resolve hostname %.100s: %s",
 		    __progname, name, ssh_gai_strerror(gaierr));
+        free(name);
 		return NULL;
 	}
 	if (cname != NULL && res->ai_canonname != NULL) {
@@ -289,6 +403,8 @@ resolve_host(const char *name, int port, int logerr, char *cname, size_t clen)
 				*cname = '\0';
 		}
 	}
+
+    free(name);
 	return res;
 }
 
@@ -423,7 +539,7 @@ check_follow_cname(int direct, char **namep, const char *cname)
  * NB. this function must operate with a options having undefined members.
  */
 static struct addrinfo *
-resolve_canonicalize(char **hostp, int port)
+resolve_canonicalize(char **hostp, int *port)
 {
 	int i, direct, ndots;
 	char *cp, *fullhost, newname[NI_MAXHOST];
@@ -433,7 +549,7 @@ resolve_canonicalize(char **hostp, int port)
 	 * Attempt to canonicalise addresses, regardless of
 	 * whether hostname canonicalisation was requested
 	 */
-	if ((addrs = resolve_addr(*hostp, port,
+	if ((addrs = resolve_addr(*hostp, *port,
 	    newname, sizeof(newname))) != NULL) {
 		debug2_f("hostname %.100s is address", *hostp);
 		if (strcasecmp(*hostp, newname) != 0) {
@@ -1227,12 +1343,14 @@ main(int ac, char **av)
 	if ((was_addr = is_addr(host)) == 0)
 		lowercase(host);
 
+    int port = options.port;
+
 	/*
 	 * Try to canonicalize if requested by configuration or the
 	 * hostname is an address.
 	 */
 	if (options.canonicalize_hostname != SSH_CANONICALISE_NO || was_addr)
-		addrs = resolve_canonicalize(&host, options.port);
+		addrs = resolve_canonicalize(&host, &port);
 
 	/*
 	 * If CanonicalizePermittedCNAMEs have been specified but
@@ -1251,7 +1369,7 @@ main(int ac, char **av)
 	    option_clear_or_none(options.jump_host);
 	if (addrs == NULL && config_has_permitted_cnames(&options) && (direct ||
 	    options.canonicalize_hostname == SSH_CANONICALISE_ALWAYS)) {
-		if ((addrs = resolve_host(host, options.port,
+		if ((addrs = resolve_host(host, &port,
 		    direct, cname, sizeof(cname))) == NULL) {
 			/* Don't fatal proxied host names not in the DNS */
 			if (direct)
@@ -1280,8 +1398,8 @@ main(int ac, char **av)
 		 * enabled and the port number may have changed since, so
 		 * reset it in address list
 		 */
-		if (addrs != NULL && options.port > 0)
-			set_addrinfo_port(addrs, options.port);
+		if (addrs != NULL && port > 0)
+			set_addrinfo_port(addrs, port);
 	}
 
 	/* Fill configuration defaults. */
@@ -1606,7 +1724,7 @@ main(int ac, char **av)
 	 */
 	if (addrs == NULL && options.proxy_command == NULL) {
 		debug2("resolving \"%s\" port %d", host, options.port);
-		if ((addrs = resolve_host(host, options.port, 1,
+		if ((addrs = resolve_host(host, &port, 1,
 		    cname, sizeof(cname))) == NULL)
 			cleanup_exit(255); /* resolve_host logs the error */
 	}
-- 
2.44.1


From 885aca548189e24d50431f59b1c7c14b378c4d91 Mon Sep 17 00:00:00 2001
From: Mara Sophie Grosch <littlefox@lf-net.org>
Date: Sat, 13 Feb 2021 23:12:31 +0100
Subject: [PATCH 2/3] Move DNS name decoding to dns.{c/h}

---
 dns.c | 43 +++++++++++++++++++++++++++++++++++++++++++
 dns.h |  2 ++
 ssh.c | 38 +++-----------------------------------
 3 files changed, 48 insertions(+), 35 deletions(-)

diff --git a/dns.c b/dns.c
index e4423655e..ab156f5c3 100644
--- a/dns.c
+++ b/dns.c
@@ -342,3 +342,46 @@ export_dns_rr(const char *hostname, struct sshkey *key, FILE *f, int generic,
 
 	return success;
 }
+
+/*
+ * Decoded a DNS wire-format name to the commonly
+ * used, dot-separated format of labels.
+ * Returns a dynamically allocated string on success
+ * and NULL on error.
+ */
+char*
+dns_decode_name(const char* data, size_t len) {
+    size_t      index  = 0;
+
+    char*  decoded = NULL;
+    size_t size    = 0;
+
+    uint8_t next_label_len;
+    do {
+        next_label_len = data[index];
+
+        char* old_decoded = decoded;
+
+        if(old_decoded) {
+            decoded = malloc(size + next_label_len + 2);
+            memcpy(decoded, old_decoded, size);
+            decoded[size] = '.';
+            memcpy(decoded + size + 1, data + index + 1, next_label_len);
+            decoded[size + next_label_len + 1] = 0;
+
+            size += 1;
+
+            free(old_decoded);
+        }
+        else {
+            decoded = malloc(next_label_len + 1);
+            memcpy(decoded, data + index + 1, next_label_len);
+            decoded[next_label_len] = 0;
+        }
+
+        size  += next_label_len;
+        index += next_label_len + 1;
+    } while(next_label_len > 0 && index + next_label_len <= len);
+
+    return decoded;
+}
diff --git a/dns.h b/dns.h
index bba0095db..e0a737f12 100644
--- a/dns.h
+++ b/dns.h
@@ -59,4 +59,6 @@ int	export_dns_rr(const char *, struct sshkey *, FILE *, int, int);
 
 const char* dns_result_totext(unsigned int res);
 
+char* dns_decode_name(const char* data, size_t len);
+
 #endif /* DNS_H */
diff --git a/ssh.c b/ssh.c
index 95871013f..c295653e4 100644
--- a/ssh.c
+++ b/ssh.c
@@ -284,43 +284,11 @@ resolve_srv(const char* orig_name, int *port) {
             uint16_t prio   = ntohs(*(uint16_t*)srv.rdi_data);
             uint16_t weight = ntohs(*(uint16_t*)(srv.rdi_data + 2));
             uint16_t port   = ntohs(*(uint16_t*)(srv.rdi_data + 4));
+            char* target    = dns_decode_name(srv.rdi_data + 6, srv.rdi_length - 6);
 
-            if(prio < cur_prio || (prio == cur_prio && weight > cur_weight)) {
-                const char* target_labels = srv.rdi_data + 6;
-                size_t      target_index  = 0;
-
-                char*  target = NULL;
-                size_t target_size = 0;
-
-                uint8_t next_label_len;
-                do {
-                    next_label_len = target_labels[target_index];
-
-                    char* old_target = target;
-
-                    if(old_target) {
-                        target = malloc(target_size + next_label_len + 2);
-                        memcpy(target, old_target, target_size);
-                        target[target_size] = '.';
-                        memcpy(target + target_size + 1, target_labels + target_index + 1, next_label_len);
-                        target[target_size + next_label_len + 1] = 0;
-
-                        target_size += 1;
-
-                        free(old_target);
-                    }
-                    else {
-                        target = malloc(next_label_len + 1);
-                        memcpy(target, target_labels + target_index + 1, next_label_len);
-                        target[next_label_len] = 0;
-                    }
-
-                    target_size  += next_label_len;
-                    target_index += next_label_len + 1;
-                } while(next_label_len > 0 && target_index + next_label_len <= srv.rdi_length - 6);
-
-                debug2_f("Found SRV record pointing at %s:%u (weight %u, prio %u)", target, port, weight, prio);
+            debug2_f("Found SRV record pointing at %s:%u (weight %u, prio %u)", target, port, weight, prio);
 
+            if(prio < cur_prio || (prio == cur_prio && weight > cur_weight)) {
                 cur_port   = port;
                 cur_target = target;
                 cur_weight = weight;
-- 
2.44.1


From afa1e00156dc0fb899e3ed48d08480cece390d03 Mon Sep 17 00:00:00 2001
From: Mara Sophie Grosch <littlefox@lf-net.org>
Date: Sun, 14 Feb 2021 00:35:53 +0100
Subject: [PATCH 3/3] Code reordering: fix uninitialized use

---
 ssh.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/ssh.c b/ssh.c
index c295653e4..144825214 100644
--- a/ssh.c
+++ b/ssh.c
@@ -252,6 +252,9 @@ default_client_percent_dollar_expand(const char *str,
  */
 static char *
 resolve_srv(const char* orig_name, int *port) {
+    uint16_t cur_port   = 0;
+    char*    cur_target = NULL;
+
     const size_t prefix_len = 10;
     const size_t orig_len = strlen(orig_name);
     char* name = malloc(prefix_len + orig_len + 1);
@@ -274,8 +277,6 @@ resolve_srv(const char* orig_name, int *port) {
 
     uint16_t cur_prio = UINT16_MAX;
     uint16_t cur_weight = 0;
-    uint16_t cur_port   = 0;
-    char*    cur_target = NULL;
 
     for(unsigned i = 0; i < srvs->rri_nrdatas; ++i) {
         struct rdatainfo srv = srvs->rri_rdatas[i];
-- 
2.44.1

