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
This commit is contained in:
djm@openbsd.org 2016-07-15 00:24:30 +00:00 committed by Damien Miller
parent 5c02dd1262
commit ed877ef653
7 changed files with 271 additions and 27 deletions

63
misc.c
View 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.
@ -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, ...)

3
misc.h
View 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__));

View 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
@ -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);
}
}

View 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);

24
ssh.1
View 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

77
ssh.c
View 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
@ -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);

View 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