Add a ProxyJump ssh_config(5) option and corresponding -J ssh(1)
authordjm <djm@openbsd.org>
Fri, 15 Jul 2016 00:24:30 +0000 (00:24 +0000)
committerdjm <djm@openbsd.org>
Fri, 15 Jul 2016 00:24:30 +0000 (00:24 +0000)
command-line flag to allow simplified indirection through a
SSH bastion or "jump host".

These options construct a proxy command that connects to the
specified jump host(s) (more than one may be specified) and uses
port-forwarding to establish a connection to the next destination.

This codifies the safest way of indirecting connections through SSH
servers and makes it easy to use.

ok markus@

usr.bin/ssh/misc.c
usr.bin/ssh/misc.h
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

index bb26c3a..0856543 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: misc.c,v 1.104 2016/04/06 06:42:17 djm Exp $ */
+/* $OpenBSD: misc.c,v 1.105 2016/07/15 00:24:30 djm Exp $ */
 /*
  * Copyright (c) 2000 Markus Friedl.  All rights reserved.
  * Copyright (c) 2005,2006 Damien Miller.  All rights reserved.
@@ -434,6 +434,67 @@ colon(char *cp)
        return NULL;
 }
 
+/*
+ * Parse a [user@]host[:port] string.
+ * Caller must free returned user and host.
+ * Any of the pointer return arguments may be NULL (useful for syntax checking).
+ * If user was not specified then *userp will be set to NULL.
+ * If port was not specified then *portp will be -1.
+ * Returns 0 on success, -1 on failure.
+ */
+int
+parse_user_host_port(const char *s, char **userp, char **hostp, int *portp)
+{
+       char *sdup, *cp, *tmp;
+       char *user = NULL, *host = NULL;
+       int port = -1, ret = -1;
+
+       if (userp != NULL)
+               *userp = NULL;
+       if (hostp != NULL)
+               *hostp = NULL;
+       if (portp != NULL)
+               *portp = -1;
+
+       if ((sdup = tmp = strdup(s)) == NULL)
+               return -1;
+       /* Extract optional username */
+       if ((cp = strchr(tmp, '@')) != NULL) {
+               *cp = '\0';
+               if (*tmp == '\0')
+                       goto out;
+               if ((user = strdup(tmp)) == NULL)
+                       goto out;
+               tmp = cp + 1;
+       }
+       /* Extract mandatory hostname */
+       if ((cp = hpdelim(&tmp)) == NULL || *cp == '\0')
+               goto out;
+       host = xstrdup(cleanhostname(cp));
+       /* Convert and verify optional port */
+       if (tmp != NULL && *tmp != '\0') {
+               if ((port = a2port(tmp)) <= 0)
+                       goto out;
+       }
+       /* Success */
+       if (userp != NULL) {
+               *userp = user;
+               user = NULL;
+       }
+       if (hostp != NULL) {
+               *hostp = host;
+               host = NULL;
+       }
+       if (portp != NULL)
+               *portp = port;
+       ret = 0;
+ out:
+       free(sdup);
+       free(user);
+       free(host);
+       return ret;
+}
+
 /* function to assist building execv() arguments */
 void
 addargs(arglist *args, char *fmt, ...)
index 58f3f77..c6b919b 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: misc.h,v 1.56 2016/04/06 06:42:17 djm Exp $ */
+/* $OpenBSD: misc.h,v 1.57 2016/07/15 00:24:30 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -49,6 +49,7 @@ char  *put_host_port(const char *, u_short);
 char   *hpdelim(char **);
 char   *cleanhostname(char *);
 char   *colon(char *);
+int     parse_user_host_port(const char *, char **, char **, int *);
 long    convtime(const char *);
 char   *tilde_expand_filename(const char *, uid_t);
 char   *percent_expand(const char *, ...) __attribute__((__sentinel__));
index 0070440..293880c 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: readconf.c,v 1.256 2016/06/03 04:09:38 dtucker Exp $ */
+/* $OpenBSD: readconf.c,v 1.257 2016/07/15 00:24:30 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -155,7 +155,7 @@ typedef enum {
        oCanonicalizeFallbackLocal, oCanonicalizePermittedCNAMEs,
        oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys,
        oFingerprintHash, oUpdateHostkeys, oHostbasedKeyTypes,
-       oPubkeyAcceptedKeyTypes,
+       oPubkeyAcceptedKeyTypes, oProxyJump,
        oIgnoredUnknownOption, oDeprecated, oUnsupported
 } OpCodes;
 
@@ -280,6 +280,7 @@ static struct {
        { "hostbasedkeytypes", oHostbasedKeyTypes },
        { "pubkeyacceptedkeytypes", oPubkeyAcceptedKeyTypes },
        { "ignoreunknown", oIgnoreUnknown },
+       { "proxyjump", oProxyJump },
 
        { NULL, oBadOption }
 };
@@ -1106,6 +1107,9 @@ parse_char_array:
 
        case oProxyCommand:
                charptr = &options->proxy_command;
+               /* Ignore ProxyCommand if ProxyJump already specified */
+               if (options->jump_host != NULL)
+                       charptr = &options->jump_host; /* Skip below */
 parse_command:
                if (s == NULL)
                        fatal("%.200s line %d: Missing argument.", filename, linenum);
@@ -1114,6 +1118,18 @@ parse_command:
                        *charptr = xstrdup(s + len);
                return 0;
 
+       case oProxyJump:
+               if (s == NULL) {
+                       fatal("%.200s line %d: Missing argument.",
+                           filename, linenum);
+               }
+               len = strspn(s, WHITESPACE "=");
+               if (parse_jump(s + len, options, *activep) == -1) {
+                       fatal("%.200s line %d: Invalid ProxyJump \"%s\"",
+                           filename, linenum, s + len);
+               }
+               return 0;
+
        case oPort:
                intptr = &options->port;
 parse_int:
@@ -1774,6 +1790,10 @@ initialize_options(Options * options)
        options->hostname = NULL;
        options->host_key_alias = NULL;
        options->proxy_command = NULL;
+       options->jump_user = NULL;
+       options->jump_host = NULL;
+       options->jump_port = -1;
+       options->jump_extra = NULL;
        options->user = NULL;
        options->escape_char = -1;
        options->num_system_hostfiles = 0;
@@ -2244,6 +2264,44 @@ parse_forward(struct Forward *fwd, const char *fwdspec, int dynamicfwd, int remo
        return (0);
 }
 
+int
+parse_jump(const char *s, Options *o, int active)
+{
+       char *orig, *sdup, *cp;
+       char *host = NULL, *user = NULL;
+       int ret = -1, port = -1;
+
+       active &= o->proxy_command == NULL && o->jump_host == NULL;
+
+       orig = sdup = xstrdup(s);
+       while ((cp = strsep(&sdup, ",")) && cp != NULL) {
+               if (active) {
+                       /* First argument and configuration is active */
+                       if (parse_user_host_port(cp, &user, &host, &port) != 0)
+                               goto out;
+               } else {
+                       /* Subsequent argument or inactive configuration */
+                       if (parse_user_host_port(cp, NULL, NULL, NULL) != 0)
+                               goto out;
+               }
+               active = 0; /* only check syntax for subsequent hosts */
+       }
+       /* success */
+       free(orig);
+       o->jump_user = user;
+       o->jump_host = host;
+       o->jump_port = port;
+       o->proxy_command = xstrdup("none");
+       user = host = NULL;
+       if ((cp = strchr(s, ',')) != NULL && cp[1] != '\0')
+               o->jump_extra = xstrdup(cp + 1);
+       ret = 0;
+ out:
+       free(user);
+       free(host);
+       return ret;
+}
+
 /* XXX the following is a near-vebatim copy from servconf.c; refactor */
 static const char *
 fmt_multistate_int(int val, const struct multistate *m)
@@ -2395,7 +2453,7 @@ void
 dump_client_config(Options *o, const char *host)
 {
        int i;
-       char vbuf[5];
+       char buf[8];
 
        /* This is normally prepared in ssh_kex2 */
        if (kex_assemble_names(KEX_DEFAULT_PK_ALG, &o->hostkeyalgorithms) != 0)
@@ -2473,7 +2531,6 @@ dump_client_config(Options *o, const char *host)
        dump_cfg_string(oMacs, o->macs ? o->macs : KEX_CLIENT_MAC);
        dump_cfg_string(oPKCS11Provider, o->pkcs11_provider);
        dump_cfg_string(oPreferredAuthentications, o->preferred_authentications);
-       dump_cfg_string(oProxyCommand, o->proxy_command);
        dump_cfg_string(oPubkeyAcceptedKeyTypes, o->pubkey_key_types);
        dump_cfg_string(oRevokedHostKeys, o->revoked_host_keys);
        dump_cfg_string(oXAuthLocation, o->xauth_location);
@@ -2534,8 +2591,8 @@ dump_client_config(Options *o, const char *host)
        if (o->escape_char == SSH_ESCAPECHAR_NONE)
                printf("escapechar none\n");
        else {
-               vis(vbuf, o->escape_char, VIS_WHITE, 0);
-               printf("escapechar %s\n", vbuf);
+               vis(buf, o->escape_char, VIS_WHITE, 0);
+               printf("escapechar %s\n", buf);
        }
 
        /* oIPQoS */
@@ -2549,4 +2606,30 @@ dump_client_config(Options *o, const char *host)
        /* oStreamLocalBindMask */
        printf("streamlocalbindmask 0%o\n",
            o->fwd_opts.streamlocal_bind_mask);
+
+       /* oProxyCommand / oProxyJump */
+       if (o->jump_host == NULL)
+               dump_cfg_string(oProxyCommand, o->proxy_command);
+       else {
+               /* Check for numeric addresses */
+               i = strchr(o->jump_host, ':') != NULL ||
+                   strspn(o->jump_host, "1234567890.") == strlen(o->jump_host);
+               snprintf(buf, sizeof(buf), "%d", o->jump_port);
+               printf("proxyjump %s%s%s%s%s%s%s%s%s\n",
+                   /* optional user */
+                   o->jump_user == NULL ? "" : o->jump_user,
+                   o->jump_user == NULL ? "" : "@",
+                   /* opening [ if hostname is numeric */
+                   i ? "[" : "",
+                   /* mandatory hostname */
+                   o->jump_host,
+                   /* closing ] if hostname is numeric */
+                   i ? "]" : "",
+                   /* optional port number */
+                   o->jump_port <= 0 ? "" : ":",
+                   o->jump_port <= 0 ? "" : buf,
+                   /* optional additional jump spec */
+                   o->jump_extra == NULL ? "" : ",",
+                   o->jump_extra == NULL ? "" : o->jump_extra);
+       }
 }
index a8b0b91..cef55f7 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: readconf.h,v 1.116 2016/06/03 03:14:41 dtucker Exp $ */
+/* $OpenBSD: readconf.h,v 1.117 2016/07/15 00:24:30 djm Exp $ */
 
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -163,6 +163,11 @@ typedef struct {
        char   *hostbased_key_types;
        char   *pubkey_key_types;
 
+       char   *jump_user;
+       char   *jump_host;
+       int     jump_port;
+       char   *jump_extra;
+
        char    *ignored_unknown; /* Pattern list of unknown tokens to ignore */
 }       Options;
 
@@ -198,6 +203,7 @@ int  process_config_line(Options *, struct passwd *, const char *,
 int     read_config_file(const char *, struct passwd *, const char *,
     const char *, Options *, int);
 int     parse_forward(struct Forward *, const char *, int, int);
+int     parse_jump(const char *, Options *, int);
 int     default_ssh_port(void);
 int     option_clear_or_none(const char *);
 void    dump_client_config(Options *o, const char *host);
index 32949b0..f3492b4 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.374 2016/06/29 17:14:28 jmc Exp $
-.Dd $Mdocdate: June 29 2016 $
+.\" $OpenBSD: ssh.1,v 1.375 2016/07/15 00:24:30 djm Exp $
+.Dd $Mdocdate: July 15 2016 $
 .Dt SSH 1
 .Os
 .Sh NAME
@@ -52,6 +52,7 @@
 .Op Fl F Ar configfile
 .Op Fl I Ar pkcs11
 .Op Fl i Ar identity_file
+.Oo Fl J Ar user Ns @ Oc Ns Ar host Ns Op : Ns Ar port
 .Op Fl L Ar address
 .Op Fl l Ar login_name
 .Op Fl m Ar mac_spec
@@ -312,6 +313,24 @@ by appending
 .Pa -cert.pub
 to identity filenames.
 .Pp
+.It Fl J Xo
+.Sm off
+.Oo Ar jump_user @ Oc
+.Ar jump_host
+.Ns Op : Ns Ar jump_port
+.Sm on
+.Xc
+Connect to the target host by first making a
+.Nm
+connection to
+.Ar jump_host
+and then establishing a TCP forward to the ultimate destination from
+there.
+Multiple jump hops may be specified separated by comma characters.
+This is a shortcut to specify a
+.Cm ProxyJump
+configuration directive.
+.Pp
 .It Fl K
 Enables GSSAPI-based authentication and forwarding (delegation) of GSSAPI
 credentials to the server.
@@ -523,6 +542,7 @@ For full details of the options listed below, and their possible values, see
 .It PreferredAuthentications
 .It Protocol
 .It ProxyCommand
+.It ProxyJump
 .It ProxyUseFdpass
 .It PubkeyAcceptedKeyTypes
 .It PubkeyAuthentication
index b82ce96..e9d84ad 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: ssh.c,v 1.442 2016/06/03 04:09:39 dtucker Exp $ */
+/* $OpenBSD: ssh.c,v 1.443 2016/07/15 00:24:30 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -315,7 +315,7 @@ resolve_addr(const char *name, int port, char *caddr, size_t clen)
  * NB. this function must operate with a options having undefined members.
  */
 static int
-check_follow_cname(char **namep, const char *cname)
+check_follow_cname(int direct, char **namep, const char *cname)
 {
        int i;
        struct allowed_cname *rule;
@@ -327,9 +327,9 @@ check_follow_cname(char **namep, const char *cname)
                return 0;
        /*
         * Don't attempt to canonicalize names that will be interpreted by
-        * a proxy unless the user specifically requests so.
+        * a proxy or jump host unless the user specifically requests so.
         */
-       if (!option_clear_or_none(options.proxy_command) &&
+       if (!direct &&
            options.canonicalize_hostname != SSH_CANONICALISE_ALWAYS)
                return 0;
        debug3("%s: check \"%s\" CNAME \"%s\"", __func__, *namep, cname);
@@ -356,7 +356,7 @@ check_follow_cname(char **namep, const char *cname)
 static struct addrinfo *
 resolve_canonicalize(char **hostp, int port)
 {
-       int i, ndots;
+       int i, direct, ndots;
        char *cp, *fullhost, newname[NI_MAXHOST];
        struct addrinfo *addrs;
 
@@ -367,7 +367,9 @@ resolve_canonicalize(char **hostp, int port)
         * Don't attempt to canonicalize names that will be interpreted by
         * a proxy unless the user specifically requests so.
         */
-       if (!option_clear_or_none(options.proxy_command) &&
+       direct = option_clear_or_none(options.proxy_command) &&
+           options.jump_host == NULL;
+       if (!direct &&
            options.canonicalize_hostname != SSH_CANONICALISE_ALWAYS)
                return NULL;
 
@@ -422,7 +424,7 @@ resolve_canonicalize(char **hostp, int port)
                /* Remove trailing '.' */
                fullhost[strlen(fullhost) - 1] = '\0';
                /* Follow CNAME if requested */
-               if (!check_follow_cname(&fullhost, newname)) {
+               if (!check_follow_cname(direct, &fullhost, newname)) {
                        debug("Canonicalized hostname \"%s\" => \"%s\"",
                            *hostp, fullhost);
                }
@@ -495,7 +497,7 @@ int
 main(int ac, char **av)
 {
        struct ssh *ssh = NULL;
-       int i, r, opt, exit_status, use_syslog, config_test = 0;
+       int i, r, opt, exit_status, use_syslog, direct, config_test = 0;
        char *p, *cp, *line, *argv0, buf[PATH_MAX], *host_arg, *logfile;
        char thishost[NI_MAXHOST], shorthost[NI_MAXHOST], portstr[NI_MAXSERV];
        char cname[NI_MAXHOST], uidstr[32], *conn_hash_hex;
@@ -573,7 +575,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:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) {
+           "ACD:E:F:GI:J:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) {
                switch (opt) {
                case '1':
                        options.protocol = SSH_PROTO_1;
@@ -698,6 +700,15 @@ main(int ac, char **av)
                        fprintf(stderr, "no support for PKCS#11.\n");
 #endif
                        break;
+               case 'J':
+                       if (options.jump_host != NULL)
+                               fatal("Only a single -J option permitted");
+                       if (options.proxy_command != NULL)
+                               fatal("Cannot specify -J with ProxyCommand");
+                       if (parse_jump(optarg, &options, 1) == -1)
+                               fatal("Invalid -J argument");
+                       options.proxy_command = xstrdup("none");
+                       break;
                case 't':
                        if (options.request_tty == REQUEST_TTY_YES)
                                options.request_tty = REQUEST_TTY_FORCE;
@@ -709,8 +720,10 @@ main(int ac, char **av)
                                debug_flag = 1;
                                options.log_level = SYSLOG_LEVEL_DEBUG1;
                        } else {
-                               if (options.log_level < SYSLOG_LEVEL_DEBUG3)
+                               if (options.log_level < SYSLOG_LEVEL_DEBUG3) {
+                                       debug_flag++;
                                        options.log_level++;
+                               }
                        }
                        break;
                case 'V':
@@ -1008,9 +1021,10 @@ main(int ac, char **av)
         * has specifically requested canonicalisation for this case via
         * CanonicalizeHostname=always
         */
-       if (addrs == NULL && options.num_permitted_cnames != 0 &&
-           (option_clear_or_none(options.proxy_command) ||
-            options.canonicalize_hostname == SSH_CANONICALISE_ALWAYS)) {
+       direct = option_clear_or_none(options.proxy_command) &&
+           options.jump_host == NULL;
+       if (addrs == NULL && options.num_permitted_cnames != 0 && (direct ||
+           options.canonicalize_hostname == SSH_CANONICALISE_ALWAYS)) {
                if ((addrs = resolve_host(host, options.port,
                    option_clear_or_none(options.proxy_command),
                    cname, sizeof(cname))) == NULL) {
@@ -1018,7 +1032,7 @@ main(int ac, char **av)
                        if (option_clear_or_none(options.proxy_command))
                                cleanup_exit(255); /* logged in resolve_host */
                } else
-                       check_follow_cname(&host, cname);
+                       check_follow_cname(direct, &host, cname);
        }
 
        /*
@@ -1043,6 +1057,41 @@ main(int ac, char **av)
        /* Fill configuration defaults. */
        fill_default_options(&options);
 
+       /*
+        * If ProxyJump option specified, then construct a ProxyCommand now.
+        */
+       if (options.jump_host != NULL) {
+               char port_s[8];
+
+               /* Consistency check */
+               if (options.proxy_command != NULL)
+                       fatal("inconsistent options: ProxyCommand+ProxyJump");
+               /* Never use FD passing for ProxyJump */
+               options.proxy_use_fdpass = 0;
+               snprintf(port_s, sizeof(port_s), "%d", options.jump_port);
+               xasprintf(&options.proxy_command,
+                   "ssh%s%s%s%s%s%s%s%s%s%.*s -W %%h:%%p %s",
+                   /* Optional "-l user" argument if jump_user set */
+                   options.jump_user == NULL ? "" : " -l ",
+                   options.jump_user == NULL ? "" : options.jump_user,
+                   /* Optional "-p port" argument if jump_port set */
+                   options.jump_port <= 0 ? "" : " -p ",
+                   options.jump_port <= 0 ? "" : port_s,
+                   /* Optional additional jump hosts ",..." */
+                   options.jump_extra == NULL ? "" : " -J ",
+                   options.jump_extra == NULL ? "" : options.jump_extra,
+                   /* Optional "-F" argumment if -F specified */
+                   config == NULL ? "" : " -F ",
+                   config == NULL ? "" : config,
+                   /* Optional "-v" arguments if -v set */
+                   debug_flag ? " -" : "",
+                   debug_flag, "vvv",
+                   /* Mandatory hostname */
+                   options.jump_host);
+               debug("Setting implicit ProxyCommand from ProxyJump: %s",
+                   options.proxy_command);
+       }
+
        if (options.port == 0)
                options.port = default_ssh_port();
        channel_set_af(options.address_family);
index 45fe892..8605770 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.232 2016/05/04 14:29:58 markus Exp $
-.Dd $Mdocdate: May 4 2016 $
+.\" $OpenBSD: ssh_config.5,v 1.233 2016/07/15 00:24:30 djm Exp $
+.Dd $Mdocdate: July 15 2016 $
 .Dt SSH_CONFIG 5
 .Os
 .Sh NAME
@@ -1358,6 +1358,30 @@ For example, the following directive would connect via an HTTP proxy at
 .Bd -literal -offset 3n
 ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
 .Ed
+.It Cm ProxyJump
+Specifies one or more jump proxies as
+.Xo
+.Sm off
+.Oo Ar user @ Oc
+.Ar host
+.Ns Op : Ns Ar port
+.Sm on
+.Xc .
+Multiple proxies may be separated by comma characters.
+Setting this option will cause
+.Xr ssh 1
+to connect to the target host by first making a
+.Xr ssh 1
+connection to the specified
+.Cm ProxyJump
+host and then establishing a
+a TCP forwarding to the ultimate target from there.
+.Pp
+Note that this option will compete with the
+.Cm ProxyCommand
+option - whichever is specified first will prevent later instances of the
+other from taking effect.
+.Pp
 .It Cm ProxyUseFdpass
 Specifies that
 .Cm ProxyCommand