Anton Khirnov
2021-Apr-10 14:04 UTC
[PATCH] Add an option for RFC5014 IPv6 source address preference
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