[PATCH] Add an option for RFC5014 IPv6 source address preference

Anton Khirnov anton at khirnov.net
Sun Apr 11 00:04:17 AEST 2021


For hosts with multiple types of IPv6 addresses, this allows to prefer
one type over others, without explicitly hardcoding the address through
BindAddress.

A typical configuration where this is useful is a host using IPv6
privacy extensions (i.e. short-lived random "temporary" addresses) and a
static "public" address. To provide privacy, it would default to the
temporary addresses for outbound connections. But since temporary
addresses have a limited lifetime, this would break long-running
connections. Therefore one might want to configure the SSH client to
prefer the public address.

Currently only Linux seems to support this.

Inspired by a patch posted by Maciej Żenczykowski at
https://bugzilla.redhat.com/show_bug.cgi?id=512032
---
 configure.ac              |  4 +++
 defines.h                 | 13 ++++++++
 openbsd-compat/port-net.c | 30 ++++++++++++++++++
 openbsd-compat/port-net.h |  2 ++
 readconf.c                | 67 ++++++++++++++++++++++++++++++++++++++-
 readconf.h                |  2 ++
 ssh.1                     |  1 +
 ssh_config.5              | 20 ++++++++++++
 sshconnect.c              |  4 +++
 9 files changed, 142 insertions(+), 1 deletion(-)

diff --git a/configure.ac b/configure.ac
index 1c2757ca..7de46ba8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -860,6 +860,10 @@ main() { if (NSVersionOfRunTimeLibrary("System") >= (60 << 16))
 	    ])
 	AC_CHECK_HEADERS([linux/seccomp.h linux/filter.h linux/audit.h], [],
 	    [], [#include <linux/types.h>])
+	AC_CHECK_DECLS([IPV6_ADDR_PREFERENCES],
+		AC_DEFINE([SYS_IPV6_ADDR_PREF_LINUX], [1],
+			[IPv6 address preferences on Linux]), [],
+		[#include <linux/ipv6.h>])
 	# Obtain MIPS ABI
 	case "$host" in
 	mips*)
diff --git a/defines.h b/defines.h
index d6a1d014..1bda872d 100644
--- a/defines.h
+++ b/defines.h
@@ -901,4 +901,17 @@ struct winsize {
 #ifdef VARIABLE_LENGTH_ARRAYS
 # define USE_SNTRUP761X25519 1
 #endif
+
+/* RFC5014 IPv6 source address selection flags.
+ * They do not have standard values or a header
+ * so we define our own values and convert to the
+ * platform-specific ones on use.
+ */
+#define SSH_IPV6_PREFER_SRC_HOME   (1 << 0)
+#define SSH_IPV6_PREFER_SRC_COA    (1 << 1)
+#define SSH_IPV6_PREFER_SRC_TMP    (1 << 2)
+#define SSH_IPV6_PREFER_SRC_PUBLIC (1 << 3)
+#define SSH_IPV6_PREFER_SRC_CGA    (1 << 4)
+#define SSH_IPV6_PREFER_SRC_NONCGA (1 << 5)
+
 #endif /* _DEFINES_H */
diff --git a/openbsd-compat/port-net.c b/openbsd-compat/port-net.c
index 198e73f0..cbb937a7 100644
--- a/openbsd-compat/port-net.c
+++ b/openbsd-compat/port-net.c
@@ -46,6 +46,10 @@
 #include <linux/if.h>
 #endif
 
+#ifdef SYS_IPV6_ADDR_PREF_LINUX
+#include <linux/ipv6.h>
+#endif
+
 #if defined(SYS_RDOMAIN_LINUX)
 char *
 sys_get_rdomain(int fd)
@@ -376,3 +380,29 @@ sys_tun_outfilter(struct ssh *ssh, struct Channel *c,
 	return (buf);
 }
 #endif /* SSH_TUN_FILTER */
+
+void sys_set_ipv6_addrpref(int sock, int addr_pref)
+{
+#ifdef SYS_IPV6_ADDR_PREF_LINUX
+	int val = 0, err;
+
+	if (addr_pref & SSH_IPV6_PREFER_SRC_HOME)
+		val |= IPV6_PREFER_SRC_HOME;
+	if (addr_pref & SSH_IPV6_PREFER_SRC_COA)
+		val |= IPV6_PREFER_SRC_COA;
+	if (addr_pref & SSH_IPV6_PREFER_SRC_TMP)
+		val |= IPV6_PREFER_SRC_TMP;
+	if (addr_pref & SSH_IPV6_PREFER_SRC_PUBLIC)
+		val |= IPV6_PREFER_SRC_PUBLIC;
+	if (addr_pref & SSH_IPV6_PREFER_SRC_CGA)
+		val |= IPV6_PREFER_SRC_CGA;
+	if (addr_pref & SSH_IPV6_PREFER_SRC_NONCGA)
+		val |= IPV6_PREFER_SRC_NONCGA;
+
+	if (setsockopt(sock, IPPROTO_IPV6, IPV6_ADDR_PREFERENCES,
+				   &val, sizeof(val)) == -1) {
+		error("setsockopt %d, IPV6_ADDR_PREFERENCES %d: %.100s",
+			  sock, val, strerror(errno));
+	}
+#endif
+}
diff --git a/openbsd-compat/port-net.h b/openbsd-compat/port-net.h
index 3a0d1104..040e2120 100644
--- a/openbsd-compat/port-net.h
+++ b/openbsd-compat/port-net.h
@@ -45,4 +45,6 @@ int sys_valid_rdomain(const char *name);
 void sys_set_process_rdomain(const char *name);
 #endif
 
+void sys_set_ipv6_addrpref(int sock, int addr_pref);
+
 #endif
diff --git a/readconf.c b/readconf.c
index 0f27652b..888ab811 100644
--- a/readconf.c
+++ b/readconf.c
@@ -173,7 +173,7 @@ typedef enum {
 	oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys,
 	oFingerprintHash, oUpdateHostkeys, oHostbasedAcceptedAlgorithms,
 	oPubkeyAcceptedAlgorithms, oCASignatureAlgorithms, oProxyJump,
-	oSecurityKeyProvider, oKnownHostsCommand,
+	oSecurityKeyProvider, oKnownHostsCommand, oIPv6AddressPreference,
 	oIgnore, oIgnoredUnknownOption, oDeprecated, oUnsupported
 } OpCodes;
 
@@ -316,6 +316,7 @@ static struct {
 	{ "proxyjump", oProxyJump },
 	{ "securitykeyprovider", oSecurityKeyProvider },
 	{ "knownhostscommand", oKnownHostsCommand },
+	{ "ipv6addresspreference", oIPv6AddressPreference },
 
 	{ NULL, oBadOption }
 };
@@ -2050,6 +2051,46 @@ parse_pubkey_algos:
 			*charptr = xstrdup(arg);
 		break;
 
+	case oIPv6AddressPreference:
+		arg = strdelim(&s);
+		if (!arg || *arg == '\0') {
+			error("%.200s line %d: Missing argument.",
+			    filename, linenum);
+			return -1;
+		}
+
+		value = options->ipv6_address_preference;
+		if (value == -1)
+			value = 0;
+
+		if (strcmp(arg, "home") == 0) {
+			value |= SSH_IPV6_PREFER_SRC_HOME;
+			value &= ~SSH_IPV6_PREFER_SRC_COA;
+		} else if (strcmp(arg, "coa") == 0) {
+			value |= SSH_IPV6_PREFER_SRC_COA;
+			value &= ~SSH_IPV6_PREFER_SRC_HOME;
+		} else if (strcmp(arg, "tmp") == 0) {
+			value |= SSH_IPV6_PREFER_SRC_TMP;
+			value &= ~SSH_IPV6_PREFER_SRC_PUBLIC;
+		} else if (strcmp(arg, "public") == 0) {
+			value |= SSH_IPV6_PREFER_SRC_PUBLIC;
+			value &= ~SSH_IPV6_PREFER_SRC_TMP;
+		} else if (strcmp(arg, "cga") == 0) {
+			value |= SSH_IPV6_PREFER_SRC_CGA ;
+			value &= ~SSH_IPV6_PREFER_SRC_NONCGA;
+		} else if (strcmp(arg, "noncga") == 0) {
+			value |= SSH_IPV6_PREFER_SRC_NONCGA;
+			value &= ~SSH_IPV6_PREFER_SRC_CGA;
+		} else if (strcmp(arg, "none") == 0) {
+			value = 0;
+		} else {
+			error("%.200s line %d: Bad argument.", filename, linenum);
+			return -1;
+		}
+
+		options->ipv6_address_preference = value;
+		break;
+
 	case oDeprecated:
 		debug("%s line %d: Deprecated option \"%s\"",
 		    filename, linenum, keyword);
@@ -2275,6 +2316,7 @@ initialize_options(Options * options)
 	options->hostbased_accepted_algos = NULL;
 	options->pubkey_accepted_algos = NULL;
 	options->known_hosts_command = NULL;
+	options->ipv6_address_preference = -1;
 }
 
 /*
@@ -2460,6 +2502,8 @@ fill_default_options(Options * options)
 		options->canonicalize_hostname = SSH_CANONICALISE_NO;
 	if (options->fingerprint_hash == -1)
 		options->fingerprint_hash = SSH_FP_HASH_DEFAULT;
+	if (options->ipv6_address_preference == -1)
+		options->ipv6_address_preference = SSH_IPV6_PREFER_SRC_PUBLIC;
 #ifdef ENABLE_SK_INTERNAL
 	if (options->sk_provider == NULL)
 		options->sk_provider = xstrdup("internal");
@@ -3280,4 +3324,25 @@ dump_client_config(Options *o, const char *host)
 		    o->jump_port <= 0 ? "" : ":",
 		    o->jump_port <= 0 ? "" : buf);
 	}
+
+	/* oIPv6AddressPreference */
+	printf("ipv6addresspreference ");
+	if (o->ipv6_address_preference == 0) {
+		printf("none");
+	} else {
+		i = o->ipv6_address_preference;
+		if (i & SSH_IPV6_PREFER_SRC_HOME)
+			printf("home ");
+		if (i & SSH_IPV6_PREFER_SRC_COA)
+			printf("coa ");
+		if (i & SSH_IPV6_PREFER_SRC_TMP)
+			printf("tmp ");
+		if (i & SSH_IPV6_PREFER_SRC_PUBLIC)
+			printf("public ");
+		if (i & SSH_IPV6_PREFER_SRC_CGA)
+			printf("cga ");
+		if (i & SSH_IPV6_PREFER_SRC_NONCGA)
+			printf("noncga ");
+	}
+	printf("\n");
 }
diff --git a/readconf.h b/readconf.h
index 2fba866e..da4834c2 100644
--- a/readconf.h
+++ b/readconf.h
@@ -175,6 +175,8 @@ typedef struct {
 
 	char   *known_hosts_command;
 
+	int ipv6_address_preference;
+
 	char	*ignored_unknown; /* Pattern list of unknown tokens to ignore */
 }       Options;
 
diff --git a/ssh.1 b/ssh.1
index 0a01767e..230f0e1f 100644
--- a/ssh.1
+++ b/ssh.1
@@ -517,6 +517,7 @@ For full details of the options listed below, and their possible values, see
 .It IdentitiesOnly
 .It IdentityAgent
 .It IdentityFile
+.It IPv6AddressPreference
 .It IPQoS
 .It KbdInteractiveAuthentication
 .It KbdInteractiveDevices
diff --git a/ssh_config.5 b/ssh_config.5
index 37a55e23..c0cea3ca 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -1078,6 +1078,26 @@ for interactive sessions and
 .Cm cs1
 (Lower Effort)
 for non-interactive sessions.
+.It Cm IPv6AddressPreference
+Specifies the RFC5014 IPv6 source address preference.
+Valid values of the argument are the string
+.Cm none
+(no preference, use OS defaults) or one the flags:
+.Cm home,
+.Cm coa
+(care-of address),
+.Cm tmp
+(temporary address),
+.Cm public
+(the default),
+.Cm cga
+(cryptographically generated address),
+.Cm noncga.
+.Pp
+This option may be specified multiple times. Flag arguments update the
+previous value; the
+.Cm none
+argument overrides the previous value.
 .It Cm KbdInteractiveAuthentication
 Specifies whether to use keyboard-interactive authentication.
 The argument to this keyword must be
diff --git a/sshconnect.c b/sshconnect.c
index 47f0b1c9..26056ede 100644
--- a/sshconnect.c
+++ b/sshconnect.c
@@ -370,6 +370,10 @@ ssh_create_socket(struct addrinfo *ai)
 	if (options.ip_qos_interactive != INT_MAX)
 		set_sock_tos(sock, options.ip_qos_interactive);
 
+	if (options.ipv6_address_preference != 0 &&
+		ai->ai_family == AF_INET6 && options.bind_address == NULL)
+		sys_set_ipv6_addrpref(sock, options.ipv6_address_preference);
+
 	/* Bind the socket to an alternative local IP address */
 	if (options.bind_address == NULL && options.bind_interface == NULL)
 		return sock;
-- 
2.20.1



More information about the openssh-unix-dev mailing list