From 264788ca1a40ff999f42253a7372b4d1857ce1fa Mon Sep 17 00:00:00 2001 From: djm Date: Fri, 23 Feb 2018 02:34:33 +0000 Subject: [PATCH] Add BindInterface ssh_config directive and -B command-line argument to ssh(1) that directs it to bind its outgoing connection to the address of the specified network interface. BindInterface prefers to use addresses that aren't loopback or link- local, but will fall back to those if no other addresses of the required family are available on that interface. Based on patch by Mike Manning in bz#2820, ok dtucker@ --- usr.bin/ssh/readconf.c | 11 ++- usr.bin/ssh/readconf.h | 3 +- usr.bin/ssh/ssh.1 | 11 ++- usr.bin/ssh/ssh.c | 21 +++--- usr.bin/ssh/ssh_config.5 | 11 ++- usr.bin/ssh/sshconnect.c | 142 +++++++++++++++++++++++++++++++++------ 6 files changed, 162 insertions(+), 37 deletions(-) diff --git a/usr.bin/ssh/readconf.c b/usr.bin/ssh/readconf.c index 59f0032c523..db4632dba9e 100644 --- a/usr.bin/ssh/readconf.c +++ b/usr.bin/ssh/readconf.c @@ -1,4 +1,4 @@ -/* $OpenBSD: readconf.c,v 1.281 2017/12/05 23:59:47 dtucker Exp $ */ +/* $OpenBSD: readconf.c,v 1.282 2018/02/23 02:34:33 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -141,7 +141,7 @@ typedef enum { oPubkeyAuthentication, oKbdInteractiveAuthentication, oKbdInteractiveDevices, oHostKeyAlias, oDynamicForward, oPreferredAuthentications, oHostbasedAuthentication, - oHostKeyAlgorithms, oBindAddress, oPKCS11Provider, + oHostKeyAlgorithms, oBindAddress, oBindInterface, oPKCS11Provider, oClearAllForwardings, oNoHostAuthenticationForLocalhost, oEnableSSHKeysign, oRekeyLimit, oVerifyHostKeyDNS, oConnectTimeout, oAddressFamily, oGssAuthentication, oGssDelegateCreds, @@ -251,6 +251,7 @@ static struct { { "preferredauthentications", oPreferredAuthentications }, { "hostkeyalgorithms", oHostKeyAlgorithms }, { "bindaddress", oBindAddress }, + { "bindinterface", oBindInterface }, { "clearallforwardings", oClearAllForwardings }, { "enablesshkeysign", oEnableSSHKeysign }, { "verifyhostkeydns", oVerifyHostKeyDNS }, @@ -1084,6 +1085,10 @@ parse_char_array: charptr = &options->bind_address; goto parse_string; + case oBindInterface: + charptr = &options->bind_interface; + goto parse_string; + case oPKCS11Provider: charptr = &options->pkcs11_provider; goto parse_string; @@ -1785,6 +1790,7 @@ initialize_options(Options * options) options->log_level = SYSLOG_LEVEL_NOT_SET; options->preferred_authentications = NULL; options->bind_address = NULL; + options->bind_interface = NULL; options->pkcs11_provider = NULL; options->enable_ssh_keysign = - 1; options->no_host_authentication_for_localhost = - 1; @@ -2492,6 +2498,7 @@ dump_client_config(Options *o, const char *host) /* String options */ dump_cfg_string(oBindAddress, o->bind_address); + dump_cfg_string(oBindInterface, o->bind_interface); dump_cfg_string(oCiphers, o->ciphers ? o->ciphers : KEX_CLIENT_ENCRYPT); dump_cfg_string(oControlPath, o->control_path); dump_cfg_string(oHostKeyAlgorithms, o->hostkeyalgorithms); diff --git a/usr.bin/ssh/readconf.h b/usr.bin/ssh/readconf.h index 34aad83cf5b..f4d9e2b2657 100644 --- a/usr.bin/ssh/readconf.h +++ b/usr.bin/ssh/readconf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: readconf.h,v 1.124 2017/10/21 23:06:24 millert Exp $ */ +/* $OpenBSD: readconf.h,v 1.125 2018/02/23 02:34:33 djm Exp $ */ /* * Author: Tatu Ylonen @@ -81,6 +81,7 @@ typedef struct { char *user_hostfiles[SSH_MAX_HOSTS_FILES]; char *preferred_authentications; char *bind_address; /* local socket address for connection to sshd */ + char *bind_interface; /* local interface for bind address */ char *pkcs11_provider; /* PKCS#11 provider */ int verify_host_key_dns; /* Verify host key using DNS */ diff --git a/usr.bin/ssh/ssh.1 b/usr.bin/ssh/ssh.1 index 9de2a11bdd3..d754b3a419e 100644 --- a/usr.bin/ssh/ssh.1 +++ b/usr.bin/ssh/ssh.1 @@ -33,8 +33,8 @@ .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" -.\" $OpenBSD: ssh.1,v 1.389 2017/11/03 02:29:17 djm Exp $ -.Dd $Mdocdate: November 3 2017 $ +.\" $OpenBSD: ssh.1,v 1.390 2018/02/23 02:34:33 djm Exp $ +.Dd $Mdocdate: February 23 2018 $ .Dt SSH 1 .Os .Sh NAME @@ -43,6 +43,7 @@ .Sh SYNOPSIS .Nm ssh .Op Fl 46AaCfGgKkMNnqsTtVvXxYy +.Op Fl B Ar bind_interface .Op Fl b Ar bind_address .Op Fl c Ar cipher_spec .Op Fl D Oo Ar bind_address : Oc Ns Ar port @@ -124,6 +125,12 @@ authenticate using the identities loaded into the agent. .It Fl a Disables forwarding of the authentication agent connection. .Pp +.It Fl B Ar interface +Bind to the address of +.Ar interface +before attempting to connect to the destination host. +This is only useful on systems with more than one address. +.Pp .It Fl b Ar bind_address Use .Ar bind_address diff --git a/usr.bin/ssh/ssh.c b/usr.bin/ssh/ssh.c index d3845dfb721..55cafe2bcb5 100644 --- a/usr.bin/ssh/ssh.c +++ b/usr.bin/ssh/ssh.c @@ -1,4 +1,4 @@ -/* $OpenBSD: ssh.c,v 1.473 2018/02/13 03:36:56 djm Exp $ */ +/* $OpenBSD: ssh.c,v 1.474 2018/02/23 02:34:33 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -185,13 +185,13 @@ static void usage(void) { fprintf(stderr, -"usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-b bind_address] [-c cipher_spec]\n" -" [-D [bind_address:]port] [-E log_file] [-e escape_char]\n" -" [-F configfile] [-I pkcs11] [-i identity_file]\n" -" [-J [user@]host[:port]] [-L address] [-l login_name] [-m mac_spec]\n" -" [-O ctl_cmd] [-o option] [-p port] [-Q query_option] [-R address]\n" -" [-S ctl_path] [-W host:port] [-w local_tun[:remote_tun]]\n" -" destination [command]\n" +"usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]\n" +" [-b bind_address] [-c cipher_spec] [-D [bind_address:]port]\n" +" [-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]\n" +" [-i identity_file] [-J [user@]host[:port]] [-L address]\n" +" [-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]\n" +" [-Q query_option] [-R address] [-S ctl_path] [-W host:port]\n" +" [-w local_tun[:remote_tun]] destination [command]\n" ); exit(255); } @@ -632,7 +632,7 @@ main(int ac, char **av) again: while ((opt = getopt(ac, av, "1246ab:c:e:fgi:kl:m:no:p:qstvx" - "ACD:E:F:GI:J:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) { + "AB:CD:E:F:GI:J:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) { switch (opt) { case '1': fatal("SSH protocol v.1 is no longer supported"); @@ -942,6 +942,9 @@ main(int ac, char **av) case 'b': options.bind_address = optarg; break; + case 'B': + options.bind_interface = optarg; + break; case 'F': config = optarg; break; diff --git a/usr.bin/ssh/ssh_config.5 b/usr.bin/ssh/ssh_config.5 index a128e4f0e0d..bdf41371c7a 100644 --- a/usr.bin/ssh/ssh_config.5 +++ b/usr.bin/ssh/ssh_config.5 @@ -33,8 +33,8 @@ .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" -.\" $OpenBSD: ssh_config.5,v 1.266 2018/02/16 02:40:45 djm Exp $ -.Dd $Mdocdate: February 16 2018 $ +.\" $OpenBSD: ssh_config.5,v 1.267 2018/02/23 02:34:33 djm Exp $ +.Dd $Mdocdate: February 23 2018 $ .Dt SSH_CONFIG 5 .Os .Sh NAME @@ -254,6 +254,13 @@ The argument must be or .Cm no (the default). +.It Cm BindInterface +Use the address of the specified interface on the local machine as the +source address of the connection. +Note that this option does not work if +.Cm UsePrivilegedPort +is set to +.Cm yes . .It Cm BindAddress Use the specified address on the local machine as the source address of the connection. diff --git a/usr.bin/ssh/sshconnect.c b/usr.bin/ssh/sshconnect.c index ede82c06e91..2ea0d007ee2 100644 --- a/usr.bin/ssh/sshconnect.c +++ b/usr.bin/ssh/sshconnect.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sshconnect.c,v 1.294 2018/02/10 09:25:35 djm Exp $ */ +/* $OpenBSD: sshconnect.c,v 1.295 2018/02/23 02:34:33 djm Exp $ */ /* * Author: Tatu Ylonen * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -33,6 +34,7 @@ #include #include #include +#include #include "xmalloc.h" #include "ssh.h" @@ -258,14 +260,79 @@ ssh_kill_proxy_command(void) kill(proxy_command_pid, SIGHUP); } +/* + * Search a interface address list (returned from getifaddrs(3)) for an + * address that matches the desired address family on the specifed interface. + * Returns 0 and fills in *resultp and *rlenp on success. Returns -1 on failure. + */ +static int +check_ifaddrs(const char *ifname, int af, const struct ifaddrs *ifaddrs, + struct sockaddr_storage *resultp, socklen_t *rlenp) +{ + struct sockaddr_in6 *sa6; + struct sockaddr_in *sa; + struct in6_addr *v6addr; + const struct ifaddrs *ifa; + int allow_local; + + /* + * Prefer addresses that are not loopback or linklocal, but use them + * if nothing else matches. + */ + for (allow_local = 0; allow_local < 2; allow_local++) { + for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL || ifa->ifa_name == NULL || + (ifa->ifa_flags & IFF_UP) == 0 || + ifa->ifa_addr->sa_family != af || + strcmp(ifa->ifa_name, options.bind_interface) != 0) + continue; + switch (ifa->ifa_addr->sa_family) { + case AF_INET: + sa = (struct sockaddr_in *)ifa->ifa_addr; + if (!allow_local && sa->sin_addr.s_addr == + htonl(INADDR_LOOPBACK)) + continue; + if (*rlenp < sizeof(struct sockaddr_in)) { + error("%s: v4 addr doesn't fit", + __func__); + return -1; + } + *rlenp = sizeof(struct sockaddr_in); + memcpy(resultp, sa, *rlenp); + return 0; + case AF_INET6: + sa6 = (struct sockaddr_in6 *)ifa->ifa_addr; + v6addr = &sa6->sin6_addr; + if (!allow_local && + (IN6_IS_ADDR_LINKLOCAL(v6addr) || + IN6_IS_ADDR_LOOPBACK(v6addr))) + continue; + if (*rlenp < sizeof(struct sockaddr_in6)) { + error("%s: v6 addr doesn't fit", + __func__); + return -1; + } + *rlenp = sizeof(struct sockaddr_in6); + memcpy(resultp, sa6, *rlenp); + return 0; + } + } + } + return -1; +} + /* * Creates a (possibly privileged) socket for use as the ssh connection. */ static int ssh_create_socket(int privileged, struct addrinfo *ai) { - int sock, r, gaierr; + int sock, r, oerrno; + struct sockaddr_storage bindaddr; + socklen_t bindaddrlen = 0; struct addrinfo hints, *res = NULL; + struct ifaddrs *ifaddrs = NULL; + char ntop[NI_MAXHOST]; sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if (sock < 0) { @@ -275,48 +342,81 @@ ssh_create_socket(int privileged, struct addrinfo *ai) fcntl(sock, F_SETFD, FD_CLOEXEC); /* Bind the socket to an alternative local IP address */ - if (options.bind_address == NULL && !privileged) + if (options.bind_address == NULL && options.bind_interface == NULL && + !privileged) return sock; - if (options.bind_address) { + if (options.bind_address != NULL) { memset(&hints, 0, sizeof(hints)); hints.ai_family = ai->ai_family; hints.ai_socktype = ai->ai_socktype; hints.ai_protocol = ai->ai_protocol; hints.ai_flags = AI_PASSIVE; - gaierr = getaddrinfo(options.bind_address, NULL, &hints, &res); - if (gaierr) { + if ((r = getaddrinfo(options.bind_address, NULL, + &hints, &res)) != 0) { error("getaddrinfo: %s: %s", options.bind_address, - ssh_gai_strerror(gaierr)); - close(sock); - return -1; + ssh_gai_strerror(r)); + goto fail; + } + if (res == NULL) + error("getaddrinfo: no addrs"); + goto fail; + if (res->ai_addrlen > sizeof(bindaddr)) { + error("%s: addr doesn't fit", __func__); + goto fail; + } + memcpy(&bindaddr, res->ai_addr, res->ai_addrlen); + bindaddrlen = res->ai_addrlen; + } else if (options.bind_interface != NULL) { + if ((r = getifaddrs(&ifaddrs)) != 0) { + error("getifaddrs: %s: %s", options.bind_interface, + strerror(errno)); + goto fail; + } + bindaddrlen = sizeof(bindaddr); + if (check_ifaddrs(options.bind_interface, ai->ai_family, + ifaddrs, &bindaddr, &bindaddrlen) != 0) { + logit("getifaddrs: %s: no suitable addresses", + options.bind_interface); + goto fail; } } + if ((r = getnameinfo((struct sockaddr *)&bindaddr, bindaddrlen, + ntop, sizeof(ntop), NULL, 0, NI_NUMERICHOST)) != 0) { + error("%s: getnameinfo failed: %s", __func__, + ssh_gai_strerror(r)); + goto fail; + } /* * If we are running as root and want to connect to a privileged * port, bind our own socket to a privileged port. */ if (privileged) { PRIV_START; - r = bindresvport_sa(sock, res ? res->ai_addr : NULL); + r = bindresvport_sa(sock, + bindaddrlen == 0 ? NULL : (struct sockaddr *)&bindaddr); + oerrno = errno; PRIV_END; if (r < 0) { - error("bindresvport_sa: af=%d %s", ai->ai_family, - strerror(errno)); + error("bindresvport_sa %s: %s", ntop, + strerror(oerrno)); goto fail; } - } else { - if (bind(sock, res->ai_addr, res->ai_addrlen) < 0) { - error("bind: %s: %s", options.bind_address, - strerror(errno)); - fail: - close(sock); - freeaddrinfo(res); - return -1; - } + } else if (bind(sock, (struct sockaddr *)&bindaddr, bindaddrlen) != 0) { + error("bind %s: %s", ntop, strerror(errno)); + goto fail; } + debug("%s: bound to %s", __func__, ntop); + /* success */ + goto out; +fail: + close(sock); + sock = -1; + out: if (res != NULL) freeaddrinfo(res); + if (ifaddrs != NULL) + freeifaddrs(ifaddrs); return sock; } -- 2.20.1