From 84fe33adbc3a277c0cae1e1b874c7fe96edf6a5b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 11 Apr 2016 11:25:53 +0300 Subject: Add sshvia --- NEWS | 2 +- sshvia | 219 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100755 sshvia diff --git a/NEWS b/NEWS index 0ff4f84..58e6aed 100644 --- a/NEWS +++ b/NEWS @@ -9,7 +9,7 @@ I'll assume I'm the only user. Version 1.20160229.2+git, not yet released ------------------------------------------ -* Add `mbox2maildir`. +* Add `mbox2maildir` and `sshvia`. Version 1.20160229.2, released 2016-02-29 ---------------------------------------- diff --git a/sshvia b/sshvia new file mode 100755 index 0000000..ea94018 --- /dev/null +++ b/sshvia @@ -0,0 +1,219 @@ +#!/usr/bin/python +# +# Summary +# ======= +# +# ssh user1%foo:2222+user2%bar +# +# This logs in a user2@bar via user1@foo (port 2222). All +# authentication happens locally, not on remote hosts. Private keys +# are only locally, not on foo. +# +# Note that port for the last host must be given using ssh -p, not a +# :22 suffix to the host. This is because this script doesn't get +# called in that case and can't handle it, and ssh itself doesn't +# handle it. +# +# Description +# =========== +# +# This is a helper to allow ssh to be used in a "via this host" +# manner. The user runs "ssh foo+bar" and this effectively logs them +# into bar by first logging into foo and running ssh there. +# +# This is useful when bar is not directly accessible from the local +# machine, but foo is. Authentication (ssh private keys, password +# entry) is done on the local machine, even for the login to bar: +# there is no need to, for example, store ssh private keys on foo. +# +# Example use case: one has a set of virtual machines behind a +# firewall or NAT, and only one of them is accessible from anywhere on +# the Internet. This is called a jump host. You're expected to access +# the other hosts via the jumphost. +# +# Setup +# ===== +# +# User needs to setup their ssh on the local host (via ~/.ssh/config) +# to run this when there is a plus sign in the target hostname: +# +# Host *+* +# ProxyCommand $(sshvia %h %p) +# +# Also, this script should be executable and accessible via $PATH, or +# else the config file should use the full path to this script. +# +# There is no need to configure anything on remote hosts, assuming +# they have a working ssh already. +# +# Implementation +# ============== +# +# This program outputs the magic ssh command line to connect to the +# first target on the command line and to forward its stdin, stdout to +# to that host. In other words, given the following command line: +# +# ssh foo+bar +# +# Each target separated by + has the following form: +# +# [user%]host[:port] +# +# Note the use of % instead of @ to get around the parsing that ssh +# itself does. +# +# Using the above configuration, when the user runs "ssh foo+bar", +# the following happens: +# +# * ssh matches "foo+bar" to the "Host *+*" section, and runs +# the proxy command. +# +# * The proxy command invokes "ssh foo -W bar:22", which means +# it connects to foo and forwards the local stdin/stdout to +# bar's ssh port. The forwarding happens over the secure link +# to foo. +# +# * The ssh started by the user uses the proxy command's +# stdin/stdout to talk to what it thinks is the "foo+bar" +# server. In other words, instead of making a TCP connection +# to the host named "foo+bar" and sets up an encrypted channel +# over that TCP connection, it uses the stdin/stdout of the +# proxy command instead. +# +# In other words, the ssh client started by the user talks to +# the ssh server on bar, but the communication goes via the +# secure channel to foo. +# +# Additionally, this script gives ssh options to set the remote user +# and port for the first host in a chain. + +import re +import sys + +import cliapp + + +def debug(msg): + # Set condition below to true to get debug output to stderr. + if False: + sys.stderr.write('SSHVIA DEBUG: {}\n'.format(msg)) + + +def panic(msg): + sys.stderr.write('SSHVIA ERROR: {}\n'.format(msg)) + sys.exit(1) + + +def extract_last_target(full_target): + # Input is foo+bar. Return foo, bar. + i = full_target.rfind('+') + if i == -1: + return None, full_target + return full_target[:i], full_target[i+1:] + + +def extract_targets(full_target): + targets = [] + remaining = full_target + while remaining: + remaining, target = extract_last_target(remaining) + targets.append(target) + return list(reversed(targets)) + + +def parse_target(target): + # Input is of the form: [user%]host[:port] + # Return user, host, port (None for user, port if missing). + + percent = target.find('%') + colon = target.find(':') + + user = None + port = None + + if percent == -1 and colon == -1: + # It's just the host. + host = target + elif percent >= 0 and colon >= 0 and percent < colon: + # It's user%host:port + user = target[:percent] + host = target[percent+1:colon] + port = target[colon+1:] + elif percent >= 0 and colon >= 0 and percent >= colon: + # it's an error + panic('Do not understand %r' % target) + elif percent >= 0: + # It's user%host + user = target[:percent] + host = target[percent+1:] + elif colon >= 0: + # It's host:port + host = target[:colon] + port = target[colon+1:] + else: + # it's a programming error + panic('Confused by %r' % target) + + return user, host, port + + +# ssh parses its own command line, and extracts target host, target +# port, and target (or remote) username. However, since ssh doesn't +# actually understand the + syntax itself, it gets things muddled: it +# parses user1@host1:8888+user2@host2:2222 in ways that will confuse +# users. Thus, we want the user to use % instead of @ so that ssh +# doesn't get in the way. +# +# ssh also allows the user to provide the remote user with the -l +# option, but we do not need to care about that, since it'll only be +# used to log into the last host in the chain. +# +# We do need to care about ports. ssh's -p option may be used for the +# last host in the chain, but every host in the chain may use : to +# separate a port number. + + +# Parse command line. We expect the full host (%h) and the port for +# thst last host in the chain (%p). That port may be the default 22, +# and will be overriden by the post in the full host if given. + +debug('argv: %r' % sys.argv) +full_target, last_target_default_port = sys.argv[1:] + +targets = extract_targets(full_target) +debug('targets: %r' % targets) +if len(targets) < 2: + panic('ERROR: Must be called with foo+bar syntax!') + +first_target = targets[0] +middle_targets = targets[1:-1] +last_target = targets[-1] + + +# Extract user from first target. +first_user, first_host, first_port = parse_target(first_target) + +# Extract port from last target. Replace with default if missing. +last_user, last_host, last_port = parse_target(last_target) +if last_port is None: + last_port = last_target_default_port + +# Set up the argv array. +argv = ['ssh', '-W', '{}:{}'.format(last_host, last_port)] + +if first_user is not None: + argv.extend(['-l', first_user]) +if first_port is not None: + argv.extend(['-p', first_port]) +all_but_last = [first_host] + middle_targets +argv.append('+'.join(all_but_last)) + + +# Shell-quote everything in argv, to avoid shell metacharacters from +# causing trouble. +argv = [cliapp.shell_quote(x) for x in argv] + + +# Output the command. +debug('result: %r' % argv) +sys.stdout.write(' '.join(argv) + '\n') -- cgit v1.2.1