Compare commits

..

1 Commits

Author SHA1 Message Date
9915d736fe Enhanced DNS support. Initial version. 2011-05-12 14:37:19 +10:00
9 changed files with 178 additions and 126 deletions

View File

@ -63,19 +63,7 @@ This is how you use it:
on your client machine. You'll need root or sudo on your client machine. You'll need root or sudo
access, and python needs to be installed. access, and python needs to be installed.
- The most basic use of sshuttle looks like: - <tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
<tt>./sshuttle -r username@sshserver 0.0.0.0/0 -vv</tt>
- There is a shortcut for 0.0.0.0/0 for those that value
their wrists
<tt>./sshuttle -r username@sshserver 0/0 -vv</tt>
- If you would also like your DNS queries to be proxied
through the DNS server of the server you are connect to:
<tt>./sshuttle --dns -rvv username@sshserver 0/0</tt>
The above is probably what you want to use to prevent
local network attacks such as Firesheep and friends.
(You may be prompted for one or more passwords; first, the (You may be prompted for one or more passwords; first, the
local password to become root using either sudo or su, and local password to become root using either sudo or su, and

119
client.py
View File

@ -175,8 +175,60 @@ class FirewallClient:
raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
def unpack_dns_name(buf, off):
name = ''
while True:
# get the next octet from buffer
n = ord(buf[off])
# zero octet terminates name
if n == 0:
off += 1
break
# top two bits on
# => a 2 octect pointer to another part of the buffer
elif (n & 0xc0) == 0xc0:
ptr = struct.unpack('>H', buf[off:off+2])[0] & 0x3fff
off = ptr
# an octet representing the number of bytes to process.
else:
off += 1
name = name + buf[off:off+n] + '.'
off += n
return name.strip('.'), off
class dnspkt:
def unpack(self, buf, off):
l = len(buf)
(self.id, self.op, self.qdcount, self.ancount, self.nscount, self.arcount) = struct.unpack("!HHHHHH",buf[off:off+12])
off += 12
self.q = []
for i in range(self.qdcount):
qname, off = unpack_dns_name(buf, off)
qtype, qclass = struct.unpack('!HH', buf[off:off+4])
off += 4
self.q.append( (qname,qtype,qclass) )
return off
def match_q_domain(self, domain):
l = len(domain)
for qname,qtype,qclass in self.q:
if qname[-l:] == domain:
if l==len(qname):
return True
elif qname[-l-1] == '.':
return True
return False
def _main(listener, fw, ssh_cmd, remotename, python, latency_control, def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
dnslistener, seed_hosts, auto_nets, dnslistener, dnsforwarder, dns_domains, dns_to,
seed_hosts, auto_nets,
syslog, daemon): syslog, daemon):
handlers = [] handlers = []
if helpers.verbose >= 1: if helpers.verbose >= 1:
@ -198,14 +250,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
handlers.append(mux) handlers.append(mux)
expected = 'SSHUTTLE0001' expected = 'SSHUTTLE0001'
try: try:
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
v = 'x'
while v and v != '\0':
v = serversock.recv(1)
initstring = serversock.recv(len(expected)) initstring = serversock.recv(len(expected))
except socket.error, e: except socket.error, e:
if e.args[0] == errno.ECONNRESET: if e.args[0] == errno.ECONNRESET:
@ -290,6 +335,7 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
handlers.append(Handler([listener], onaccept)) handlers.append(Handler([listener], onaccept))
dnsreqs = {} dnsreqs = {}
dnsforwards = {}
def dns_done(chan, data): def dns_done(chan, data):
peer,timeout = dnsreqs.get(chan) or (None,None) peer,timeout = dnsreqs.get(chan) or (None,None)
debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
@ -302,16 +348,54 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
now = time.time() now = time.time()
if pkt: if pkt:
debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt))) debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt)))
chan = mux.next_channel() dns = dnspkt()
dnsreqs[chan] = peer,now+30 dns.unpack(pkt, 0)
mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
mux.channels[chan] = lambda cmd,data: dns_done(chan,data) match=False
if dns_domains is not None:
for domain in dns_domains:
if dns.match_q_domain(domain):
match=True
break
if match:
debug3("We need to redirect this request remotely\n")
chan = mux.next_channel()
dnsreqs[chan] = peer,now+30
mux.send(chan, ssnet.CMD_DNS_REQ, pkt)
mux.channels[chan] = lambda cmd,data: dns_done(chan,data)
else:
debug3("We need to forward this request locally\n")
dnsforwarder.sendto(pkt, dns_to)
dnsforwards[dns.id] = peer,now+30
for chan,(peer,timeout) in dnsreqs.items(): for chan,(peer,timeout) in dnsreqs.items():
if timeout < now: if timeout < now:
del dnsreqs[chan] del dnsreqs[chan]
for chan,(peer,timeout) in dnsforwards.items():
if timeout < now:
del dnsforwards[chan]
debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
debug3('Remaining DNS forwards: %d\n' % len(dnsforwards))
if dnslistener: if dnslistener:
handlers.append(Handler([dnslistener], ondns)) handlers.append(Handler([dnslistener], ondns))
def ondnsforward():
debug1("We got a response.\n")
pkt,server = dnsforwarder.recvfrom(4096)
now = time.time()
if server[0] != dns_to[0] or server[1] != dns_to[1]:
debug1("Ooops. The response came from the wrong server. Ignoring\n")
else:
dns = dnspkt()
dns.unpack(pkt, 0)
chan=dns.id
peer,timeout = dnsforwards.get(chan) or (None,None)
debug3('dns_done: channel=%r peer=%r\n' % (chan, peer))
if peer:
del dnsforwards[chan]
debug3('doing sendto %r\n' % (peer,))
dnslistener.sendto(pkt, peer)
if dnsforwarder:
handlers.append(Handler([dnsforwarder], ondnsforward))
if seed_hosts != None: if seed_hosts != None:
debug1('seed_hosts: %r\n' % seed_hosts) debug1('seed_hosts: %r\n' % seed_hosts)
@ -328,7 +412,8 @@ def _main(listener, fw, ssh_cmd, remotename, python, latency_control,
mux.callback() mux.callback()
def main(listenip, ssh_cmd, remotename, python, latency_control, dns, def main(listenip, ssh_cmd, remotename, python, latency_control,
dns, dns_domains, dns_to,
seed_hosts, auto_nets, seed_hosts, auto_nets,
subnets_include, subnets_exclude, syslog, daemon, pidfile): subnets_include, subnets_exclude, syslog, daemon, pidfile):
if syslog: if syslog:
@ -373,15 +458,21 @@ def main(listenip, ssh_cmd, remotename, python, latency_control, dns,
dnsip = dnslistener.getsockname() dnsip = dnslistener.getsockname()
debug1('DNS listening on %r.\n' % (dnsip,)) debug1('DNS listening on %r.\n' % (dnsip,))
dnsport = dnsip[1] dnsport = dnsip[1]
dnsforwarder = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dnsforwarder.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
dnsforwarder.setsockopt(socket.SOL_IP, socket.IP_TTL, 42)
else: else:
dnsport = 0 dnsport = 0
dnslistener = None dnslistener = None
dnsforwarder = None
fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport) fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport)
try: try:
return _main(listener, fw, ssh_cmd, remotename, return _main(listener, fw, ssh_cmd, remotename,
python, latency_control, dnslistener, python, latency_control,
dnslistener, dnsforwarder, dns_domains, dns_to,
seed_hosts, auto_nets, syslog, daemon) seed_hosts, auto_nets, syslog, daemon)
finally: finally:
try: try:

115
do
View File

@ -8,14 +8,14 @@
# #
# By default, no output coloring. # By default, no output coloring.
green="" GREEN=""
bold="" BOLD=""
plain="" PLAIN=""
if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then
green="$(printf '\033[32m')" GREEN="$(printf '\033[32m')"
bold="$(printf '\033[1m')" BOLD="$(printf '\033[1m')"
plain="$(printf '\033[m')" PLAIN="$(printf '\033[m')"
fi fi
_dirsplit() _dirsplit()
@ -24,13 +24,6 @@ _dirsplit()
dir=${1%$base} dir=${1%$base}
} }
dirname()
(
_dirsplit "$1"
dir=${dir%/}
echo "${dir:-.}"
)
_dirsplit "$0" _dirsplit "$0"
export REDO=$(cd "${dir:-.}" && echo "$PWD/$base") export REDO=$(cd "${dir:-.}" && echo "$PWD/$base")
@ -61,105 +54,87 @@ fi
_find_dofile_pwd() _find_dofile_pwd()
{ {
dofile=default.$1.do DOFILE=default.$1.do
while :; do while :; do
dofile=default.${dofile#default.*.} DOFILE=default.${DOFILE#default.*.}
[ -e "$dofile" -o "$dofile" = default.do ] && break [ -e "$DOFILE" -o "$DOFILE" = default.do ] && break
done done
ext=${dofile#default} EXT=${DOFILE#default}
ext=${ext%.do} EXT=${EXT%.do}
base=${1%$ext} BASE=${1%$EXT}
} }
_find_dofile() _find_dofile()
{ {
local prefix= PREFIX=
while :; do while :; do
_find_dofile_pwd "$1" _find_dofile_pwd "$1"
[ -e "$dofile" ] && break [ -e "$DOFILE" ] && break
[ "$PWD" = "/" ] && break [ "$PWD" = "/" ] && break
target=${PWD##*/}/$target TARGET=${PWD##*/}/$TARGET
tmp=${PWD##*/}/$tmp PREFIX=${PWD##*/}/$PREFIX
prefix=${PWD##*/}/$prefix
cd .. cd ..
done done
base=$prefix$base BASE=$PREFIX$BASE
} }
_run_dofile() _run_dofile()
{ {
export DO_DEPTH="$DO_DEPTH " export DO_DEPTH="$DO_DEPTH "
export REDO_TARGET=$PWD/$target export REDO_TARGET=$PWD/$TARGET
local line1
set -e set -e
read line1 <"$PWD/$dofile" read line1 <"$PWD/$DOFILE"
cmd=${line1#"#!/"} cmd=${line1#"#!/"}
if [ "$cmd" != "$line1" ]; then if [ "$cmd" != "$line1" ]; then
/$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2" /$cmd "$PWD/$DOFILE" "$@" >"$TARGET.tmp2"
else else
:; . "$PWD/$dofile" >"$tmp.tmp2" . "$PWD/$DOFILE" >"$TARGET.tmp2"
fi fi
} }
_do() _do()
{ {
local dir=$1 target=$2 tmp=$3 DIR=$1
if [ ! -e "$target" ] || [ -d "$target" -a ! -e "$target.did" ]; then TARGET=$2
if [ ! -e "$TARGET" ] || [ -e "$TARGET/." -a ! -e "$TARGET.did" ]; then
printf '%sdo %s%s%s%s\n' \ printf '%sdo %s%s%s%s\n' \
"$green" "$DO_DEPTH" "$bold" "$dir$target" "$plain" >&2 "$GREEN" "$DO_DEPTH" "$BOLD" "$DIR$TARGET" "$PLAIN" >&2
echo "$PWD/$target" >>"$DO_BUILT" echo "$PWD/$TARGET" >>"$DO_BUILT"
dofile=$target.do DOFILE=$TARGET.do
base=$target BASE=$TARGET
ext= EXT=
[ -e "$target.do" ] || _find_dofile "$target" [ -e "$TARGET.do" ] || _find_dofile "$TARGET"
if [ ! -e "$dofile" ]; then if [ ! -e "$DOFILE" ]; then
echo "do: $target: no .do file" >&2 echo "do: $TARGET: no .do file" >&2
return 1 return 1
fi fi
[ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] || [ ! -e "$DO_BUILD" ] || : >>"$TARGET.did"
: >>"$target.did" ( _run_dofile "$BASE" "$EXT" "$TARGET.tmp" )
( _run_dofile "$base" "$ext" "$tmp.tmp" ) RV=$?
rv=$? if [ $RV != 0 ]; then
if [ $rv != 0 ]; then
printf "do: %s%s\n" "$DO_DEPTH" \ printf "do: %s%s\n" "$DO_DEPTH" \
"$dir$target: got exit code $rv" >&2 "$DIR$TARGET: got exit code $RV" >&2
rm -f "$tmp.tmp" "$tmp.tmp2" rm -f "$TARGET.tmp" "$TARGET.tmp2"
return $rv return $RV
fi fi
mv "$tmp.tmp" "$target" 2>/dev/null || mv "$TARGET.tmp" "$TARGET" 2>/dev/null ||
! test -s "$tmp.tmp2" || ! test -s "$TARGET.tmp2" ||
mv "$tmp.tmp2" "$target" 2>/dev/null mv "$TARGET.tmp2" "$TARGET" 2>/dev/null
rm -f "$tmp.tmp2" rm -f "$TARGET.tmp2"
else else
echo "do $DO_DEPTH$target exists." >&2 echo "do $DO_DEPTH$TARGET exists." >&2
fi fi
} }
# Make corrections for directories that don't actually exist yet.
_dir_shovel()
{
local dir base
xdir=$1 xbase=$2 xbasetmp=$2
while [ ! -d "$xdir" -a -n "$xdir" ]; do
_dirsplit "${xdir%/}"
xbasetmp=${base}__$xbase
xdir=$dir xbase=$base/$xbase
echo "xbasetmp='$xbasetmp'" >&2
done
}
redo() redo()
{ {
for i in "$@"; do for i in "$@"; do
_dirsplit "$i" _dirsplit "$i"
_dir_shovel "$dir" "$base" ( cd "$dir" && _do "$dir" "$base" ) || return 1
dir=$xdir base=$xbase basetmp=$xbasetmp
( cd "$dir" && _do "$dir" "$base" "$basetmp" ) || return 1
done done
} }

18
main.py
View File

@ -54,6 +54,8 @@ l,listen= transproxy to this ip address and port number [127.0.0.1:0]
H,auto-hosts scan for remote hostnames and update local /etc/hosts H,auto-hosts scan for remote hostnames and update local /etc/hosts
N,auto-nets automatically determine subnets to route N,auto-nets automatically determine subnets to route
dns capture local DNS requests and forward to the remote DNS server dns capture local DNS requests and forward to the remote DNS server
dns-domains= comma seperated list of DNS domains for DNS forwarding
dns-to= forward any DNS requests that don't match domains to this address
python= path to python interpreter on the remote server python= path to python interpreter on the remote server
r,remote= ssh hostname (and optional username) of remote sshuttle server r,remote= ssh hostname (and optional username) of remote sshuttle server
x,exclude= exclude this subnet (can be used more than once) x,exclude= exclude this subnet (can be used more than once)
@ -110,12 +112,26 @@ try:
sh = [] sh = []
else: else:
sh = None sh = None
if opt.dns and opt.dns_domains:
dns_domains = opt.dns_domains.split(",")
if opt.dns_to:
addr,colon,port = opt.dns_to.rpartition(":")
if colon == ":":
dns_to = ( addr, int(port) )
else:
dns_to = ( port, 53 )
else:
o.fatal('--dns-to=ip is required with --dns-domains=list')
else:
dns_domains = None
dns_to = None
sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'), sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'),
opt.ssh_cmd, opt.ssh_cmd,
remotename, remotename,
opt.python, opt.python,
opt.latency_control, opt.latency_control,
opt.dns, opt.dns, dns_domains, dns_to,
sh, sh,
opt.auto_nets, opt.auto_nets,
parse_subnets(includes), parse_subnets(includes),

View File

@ -130,7 +130,7 @@ class DnsProxy(Handler):
try: try:
self.sock.send(self.request) self.sock.send(self.request)
except socket.error, e: except socket.error, e:
if e.args[0] in ssnet.NET_ERRS: if e.args[0] in [errno.ECONNREFUSED, errno.EHOSTUNREACH]:
# might have been spurious; try again. # might have been spurious; try again.
# Note: these errors sometimes are reported by recv(), # Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both. # and sometimes by send(). We have to catch both.
@ -145,7 +145,7 @@ class DnsProxy(Handler):
try: try:
data = self.sock.recv(4096) data = self.sock.recv(4096)
except socket.error, e: except socket.error, e:
if e.args[0] in ssnet.NET_ERRS: if e.args[0] in [errno.ECONNREFUSED, errno.EHOSTUNREACH]:
# might have been spurious; try again. # might have been spurious; try again.
# Note: these errors sometimes are reported by recv(), # Note: these errors sometimes are reported by recv(),
# and sometimes by send(). We have to catch both. # and sometimes by send(). We have to catch both.
@ -173,7 +173,7 @@ def main():
debug1(' %s/%d\n' % r) debug1(' %s/%d\n' % r)
# synchronization header # synchronization header
sys.stdout.write('\0\0SSHUTTLE0001') sys.stdout.write('SSHUTTLE0001')
sys.stdout.flush() sys.stdout.flush()
handlers = [] handlers = []

2
ssh.py
View File

@ -85,7 +85,7 @@ def connect(ssh_cmd, rhostport, python, stderr, options):
pycmd = "'%s' -c '%s'" % (python, pyscript) pycmd = "'%s' -c '%s'" % (python, pyscript)
else: else:
pycmd = ("P=python2; $P -V 2>/dev/null || P=python; " pycmd = ("P=python2; $P -V 2>/dev/null || P=python; "
"exec \"$P\" -c '%s'") % pyscript "\"$P\" -c '%s'") % pyscript
argv = (sshl + argv = (sshl +
portl + portl +
ipv6flag + ipv6flag +

View File

@ -1,10 +1,5 @@
#!/bin/sh #!/bin/sh
EXE=$0 DIR=$(dirname "$0")
for i in 1 2 3 4 5 6 7 8 9 10; do
[ -L "$EXE" ] || break
EXE=$(readlink "$EXE")
done
DIR=$(dirname "$EXE")
if python2 -V 2>/dev/null; then if python2 -V 2>/dev/null; then
exec python2 "$DIR/main.py" python2 "$@" exec python2 "$DIR/main.py" python2 "$@"
else else

View File

@ -40,11 +40,7 @@ cmd_to_name = {
CMD_DNS_REQ: 'DNS_REQ', CMD_DNS_REQ: 'DNS_REQ',
CMD_DNS_RESPONSE: 'DNS_RESPONSE', CMD_DNS_RESPONSE: 'DNS_RESPONSE',
} }
NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT,
errno.EHOSTUNREACH, errno.ENETUNREACH,
errno.EHOSTDOWN, errno.ENETDOWN]
def _add(l, elem): def _add(l, elem):
@ -128,12 +124,6 @@ class SockWrapper:
return # already connected return # already connected
self.rsock.setblocking(False) self.rsock.setblocking(False)
debug3('%r: trying connect to %r\n' % (self, self.connect_to)) debug3('%r: trying connect to %r\n' % (self, self.connect_to))
if socket.inet_aton(self.connect_to[0])[0] == '\0':
self.seterr(Exception("Can't connect to %r: "
"IP address starts with zero\n"
% (self.connect_to,)))
self.connect_to = None
return
try: try:
self.rsock.connect(self.connect_to) self.rsock.connect(self.connect_to)
# connected successfully (Linux) # connected successfully (Linux)
@ -155,7 +145,9 @@ class SockWrapper:
elif e.args[0] == errno.EISCONN: elif e.args[0] == errno.EISCONN:
# connected successfully (BSD) # connected successfully (BSD)
self.connect_to = None self.connect_to = None
elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]: elif e.args[0] in [errno.ECONNREFUSED, errno.ETIMEDOUT,
errno.EHOSTUNREACH, errno.ENETUNREACH,
errno.EACCES, errno.EPERM]:
# a "normal" kind of error # a "normal" kind of error
self.connect_to = None self.connect_to = None
self.seterr(e) self.seterr(e)

View File

@ -1,10 +1,5 @@
exec >&2 exec >&2
redo-ifchange runpython.c redo-ifchange runpython.c
ARCHES="" gcc -Wall -o $3 runpython.c \
for d in /usr/libexec/gcc/darwin/*; do
ARCHES="$ARCHES -arch $(basename $d)"
done
gcc $ARCHES \
-Wall -o $3 runpython.c \
-I/usr/include/python2.5 \ -I/usr/include/python2.5 \
-lpython2.5 -lpython2.5