From ed877ef653847d056bb433975d731b7a1132a979 Mon Sep 17 00:00:00 2001 From: "djm@openbsd.org" Date: Fri, 15 Jul 2016 00:24:30 +0000 Subject: upstream commit Add a ProxyJump ssh_config(5) option and corresponding -J ssh(1) 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@ Upstream-ID: fa899cb8b26d889da8f142eb9774c1ea36b04397 --- misc.c | 63 +++++++++++++++++++++++++++++++++++++++- misc.h | 3 +- readconf.c | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- readconf.h | 8 ++++- ssh.1 | 24 +++++++++++++-- ssh.c | 77 +++++++++++++++++++++++++++++++++++++++--------- ssh_config.5 | 28 ++++++++++++++++-- 7 files changed, 271 insertions(+), 27 deletions(-) diff --git a/misc.c b/misc.c index 9d59ca6b..9421b4d3 100644 --- a/misc.c +++ b/misc.c @@ -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. @@ -451,6 +451,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, ...) diff --git a/misc.h b/misc.h index 01432ba8..7c76a6a7 100644 --- a/misc.h +++ b/misc.h @@ -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 @@ -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__)); diff --git a/readconf.c b/readconf.c index 9dcc383d..cb2999d8 100644 --- a/readconf.c +++ b/readconf.c @@ -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 * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -170,7 +170,7 @@ typedef enum { oCanonicalizeFallbackLocal, oCanonicalizePermittedCNAMEs, oStreamLocalBindMask, oStreamLocalBindUnlink, oRevokedHostKeys, oFingerprintHash, oUpdateHostkeys, oHostbasedKeyTypes, - oPubkeyAcceptedKeyTypes, + oPubkeyAcceptedKeyTypes, oProxyJump, oIgnoredUnknownOption, oDeprecated, oUnsupported } OpCodes; @@ -295,6 +295,7 @@ static struct { { "hostbasedkeytypes", oHostbasedKeyTypes }, { "pubkeyacceptedkeytypes", oPubkeyAcceptedKeyTypes }, { "ignoreunknown", oIgnoreUnknown }, + { "proxyjump", oProxyJump }, { NULL, oBadOption } }; @@ -1121,6 +1122,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); @@ -1129,6 +1133,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: @@ -1789,6 +1805,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; @@ -2261,6 +2281,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) @@ -2412,7 +2470,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) @@ -2490,7 +2548,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); @@ -2551,8 +2608,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 */ @@ -2566,4 +2623,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); + } } diff --git a/readconf.h b/readconf.h index a8b0b918..cef55f71 100644 --- a/readconf.h +++ b/readconf.h @@ -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 @@ -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); diff --git a/ssh.1 b/ssh.1 index 32949b05..f3492b4d 100644 --- a/ssh.1 +++ b/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.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 diff --git a/ssh.c b/ssh.c index e7d4fd91..a9b68534 100644 --- a/ssh.c +++ b/ssh.c @@ -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 * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland @@ -330,7 +330,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; @@ -342,9 +342,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); @@ -371,7 +371,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; @@ -382,7 +382,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; @@ -437,7 +439,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); } @@ -510,7 +512,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; @@ -603,7 +605,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; @@ -728,6 +730,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; @@ -739,8 +750,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': @@ -1038,9 +1051,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) { @@ -1048,7 +1062,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); } /* @@ -1073,6 +1087,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); diff --git a/ssh_config.5 b/ssh_config.5 index 45fe8920..86057702 100644 --- a/ssh_config.5 +++ b/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.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 -- cgit v1.2.3