Add BindInterface ssh_config directive and -B command-line argument
authordjm <djm@openbsd.org>
Fri, 23 Feb 2018 02:34:33 +0000 (02:34 +0000)
committerdjm <djm@openbsd.org>
Fri, 23 Feb 2018 02:34:33 +0000 (02:34 +0000)
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
usr.bin/ssh/readconf.h
usr.bin/ssh/ssh.1
usr.bin/ssh/ssh.c
usr.bin/ssh/ssh_config.5
usr.bin/ssh/sshconnect.c

index 59f0032..db4632d 100644 (file)
@@ -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 <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, 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);
index 34aad83..f4d9e2b 100644 (file)
@@ -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 <ylo@cs.hut.fi>
@@ -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 */
 
index 9de2a11..d754b3a 100644 (file)
@@ -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
index d3845df..55cafe2 100644 (file)
@@ -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 <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, 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;
index a128e4f..bdf4137 100644 (file)
@@ -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.
index ede82c0..2ea0d00 100644 (file)
@@ -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 <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -19,6 +19,7 @@
 #include <sys/socket.h>
 #include <sys/time.h>
 
+#include <net/if.h>
 #include <netinet/in.h>
 
 #include <ctype.h>
@@ -33,6 +34,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include <ifaddrs.h>
 
 #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;
 }