#!/bin/bash
HOST=myserver.com
PROXY=proxy.acme.com
PROXY_PORT=8080
(
echo -en "CONNECT ${HOST}:443 HTTP/1.1\n"
echo -en "Host: ${HOST}:443\n"
echo -en "\r\n"
cat
) | (
nc ${PROXY} ${PROXY_PORT}
) | (
head -3 >/dev/null # eat unwanted proxy http headers
cat
)
Then I'd ssh -p 2222 localhost and the script would connect me to my
home machine through the http proxy.
These days it's even easier; openssh's ssh client has an option
-oProxyCommand that will let you run the proxy script directly from ssh
(instead of having to run it from inetd):
ssh -oProxyCommand=proxy.sh user@host
There are actually several programs for doing this now. proxytunnel,
ssh-https-tunnel, corkscrew and probably many others. None of them really
do anything different than the above shell script. They all use CONNECT.
On some proxies CONNECT just isn't available or is restricted to a
limited number of whitelisted hosts and ports. The ports problem is
usually easy to overcome by making sshd listen on port 443 (as I did for
the old shell script method). If CONNECT is enabled at all it will
normally be allowed for 443 as that is the https port which is the
reason d'etre for CONNECT in the first place. Normally regular HTTP
GET/POST are less restricted and can often connect to pretty much
anywhere.
In theory http is request/response, but in practice it isn't. Although
proxies and apache generally enforce either a read or a write mode they
always allow end-to-end transmission before the http "transaction" is
complete.
This presents an opportunity for rather a nice...
ObHack:
We can get around the pseudo request/response limitation just by using
two simultaneous http requests; one continuously reading, the other
continuously writing.
The obligatory ASCII diagram:
ssh<--. .--> sshd
| |
| |
| (continuous GET request to ssh.cgi) |
v curl <--- proxy <--- apache <--- ssh.cgi <--- nc <---'
.-----------. | ^
| |<--' |p
| client.sh |---. |i
| | | |p
~~~~~~~~~~~ v |e
nc ---> proxy ---> apache ---> ssh.cgi -----'
(continuous POST request to ssh.cgi)
The overhead is very low - once established it's about the same as CONNECT
or even straight ssh, except for the fact that you're using two tcp
connections instead of one. From the proxy server's perspective it sees two
requests, one slowly downloading and the other slowly uploading. In the
logs generally all you will see is two requests, the time they completed
(when you eventually hang up) and the amount of data transmitted in either
direction.
Usage information:
ssh.cgi :
This is the server side script. It runs as a regular CGI on a
webserver. It requires nc (netcat) to be installed.
client.sh :
This is the client script. It requires both nc and curl to be
installed.
You use client.sh as an ssh ProxyCommand like so:
ssh -oProxyCommand="client.sh %h" us...@host.com
I generally like to make short aliases such as:
alias scpx='scp -oProxyCommand="client.sh %h"'
alias sshx='ssh -oProxyCommand="client.sh %h"'
So that I can just type sshx user@host to ssh via the proxy
scripts.
So.. here are the scripts:
[client.sh]
#!/bin/bash
#
# This is the client program.
#
# Usage: ssh -oProxyCommand="./client.sh %h" us...@domain.com
#
# Robert McKay <rob...@mckay.com>
#
# Requires: curl, nc
#
#
# Configure me
cgi_server="www.mywebserver.com"
cgi_server_port=80
cgi_uri="/cgi-bin/ssh.cgi"
cgi_max_content=1000000000
proxy_host="squid.myorg.org"
proxy_port="3128"
# Shouldn't need editing after here.
host=$1
# Are we using a proxy? Or just directly connecting to an http server?
if [ ! -z "${proxy_host}" ];
then
purl="http://${cgi_server}:${cgi_server_port}/${cgi_uri}"
connect_to=${proxy_host}
connect_port=${proxy_port}
proxyopts="-x ${proxy_host}:${proxy_port}"
else
connect_to=${cgi_server}
connect_port=${cgi_server_port}
proxyopts=
fi
sessionkey="/tmp/sessionkey.$$"
if [ -f "${sessionkey}" ];
then
echo "Session ${sessionkey} exists! stale?"
exit 1;
fi
touch "${sessionkey}" # create the sessionkey file
chmod 600 "${sessionkey}" # secure the sessionkey file
# Make the first request. This will initialize the remote end,
# and it will give us a session key. Then the link will become our
# regular receive channel for data coming from the server.
curl ${proxyopts} -sN "http://${cgi_server}/${cgi_uri}?host=${host}" |
(
head -2 > "${sessionkey}" ; # save the session key
cat - # process channel output (forever)
)&
lynxpid=`jobs -p`
#lynxpid=$!
# Try and clean up when we die
trap "kill ${lynxpid}" SIGTERM SIGHUP SIGPIPE
# The first connection was made in the background, so we may not
# have the key yet. This busy-waits for the key to arrive.
while [ -z "$key" ];
do
key=`tail -1 "${sessionkey}" 2>/dev/null | cut -f2 -d":"`
done
rm -f "${sessionkey}" # we've got the key now. clean up.
# The receive (GET) channel is up and we have a key to it
# Now we make a second connection for the send (POST) channel
# using the same key.
connect_url=${purl:-${cgi_uri}}
(
cat<<EOF
POST ${connect_url}?key=${key} HTTP/1.1
Host: ${cgi_server}
Content-Type: application/octet-stream
Content-Length: ${cgi_max_content}
EOF
cat -
) | nc "${connect_to}" "${connect_port}"
# I wanted to do the above using something like this:
#
# curl ${proxyopts} -d@- -sN "http://server.com/cgi-bin/ssh.cgi?key=${key}"
#
# but unfortunately:
# curl's -d option is designed to read the entire file in before it
# connects to the remote site. This won't work for our application :-(
# libcurl does support a callback POST mechanism so I could either re-write
# this client in C, or maybe try and patch the curl cli and push the feature
# upstream. maybe both?
[end of client.sh]
[ssh.cgi]
#!/bin/bash
#
# This is the server script. Install as a cgi script on a webserver.
#
# Robert McKay <rob...@mckay.com>
#
# Requires: nc
padding=4096
if [ "${REQUEST_METHOD}" == "POST" ];
then
# If the request is a POST then it's the RX mode
hashkey=`echo "${QUERY_STRING}" | cut -f2 -d"="`
sessionkey=`echo "${hashkey}" | md5sum - | cut -f1 -d" "`
pipe="/tmp/outpipe.${sessionkey}"
if [ -p "${pipe}" ];
then
cat > "${pipe}"
else
echo -en "Content-Type: text/plain\n\n"
echo "Session not found."
exit 0
fi
echo -en "Content-Type: application/octet-stream\n\n"
echo "End of script"
else
# If it's a GET then we're the TX
host=`echo "${QUERY_STRING}" | cut -f2 -d"="`
${host:=localhost} # default to localhost
echo -en "Content-Type: application/octet-stream\n\n"
echo -en "Padding proxy cache..."
c=0
while [ $c -lt "$padding" ];
do
echo -en "."
((c++))
done
echo "done"
hashkey=`head -c 1024 /dev/urandom | md5sum - | cut -f1 -d" "`
echo "Key:"${hashkey}
sessionkey=`echo "${hashkey}" | md5sum - | cut -f1 -d" "`
pipe="/tmp/outpipe.${sessionkey}"
mknod "${pipe}" p
nc "${host}" 22 < "${pipe}"
fi
rm -rf "${pipe}"
[end of ssh.cgi]
Happy hacking,
Robert McKay