mirror of
https://github.com/sshuttle/sshuttle.git
synced 2024-11-29 03:14:20 +01:00
167a57e739
In instances where a cluster pod in a local VM needs to access a server that is sshuttle'd from the host, since the packets arriving at the host already made a hop, their TTL is 63 and so get ignored by sshuttle. Allowing an override of the firewall TTL rule allows the packets to go through.
1006 lines
35 KiB
Python
1006 lines
35 KiB
Python
import errno
|
|
import re
|
|
import signal
|
|
import time
|
|
import subprocess as ssubprocess
|
|
import os
|
|
import sys
|
|
import platform
|
|
import psutil
|
|
|
|
import sshuttle.helpers as helpers
|
|
import sshuttle.ssnet as ssnet
|
|
import sshuttle.ssh as ssh
|
|
import sshuttle.ssyslog as ssyslog
|
|
import sshuttle.sdnotify as sdnotify
|
|
from sshuttle.ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper
|
|
from sshuttle.helpers import log, debug1, debug2, debug3, Fatal, islocal, \
|
|
resolvconf_nameservers, which
|
|
from sshuttle.methods import get_method, Features
|
|
from sshuttle import __version__
|
|
try:
|
|
from pwd import getpwnam
|
|
except ImportError:
|
|
getpwnam = None
|
|
|
|
try:
|
|
# try getting recvmsg from python
|
|
import socket as pythonsocket
|
|
getattr(pythonsocket.socket, "recvmsg")
|
|
socket = pythonsocket
|
|
except AttributeError:
|
|
# try getting recvmsg from socket_ext library
|
|
try:
|
|
import socket_ext
|
|
getattr(socket_ext.socket, "recvmsg")
|
|
socket = socket_ext
|
|
except ImportError:
|
|
import socket
|
|
|
|
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
|
|
|
|
|
def got_signal(signum, frame):
|
|
log('exiting on signal %d' % signum)
|
|
sys.exit(1)
|
|
|
|
|
|
_pidname = None
|
|
|
|
|
|
def check_daemon(pidfile):
|
|
global _pidname
|
|
_pidname = os.path.abspath(pidfile)
|
|
try:
|
|
oldpid = open(_pidname).read(1024)
|
|
except IOError as e:
|
|
if e.errno == errno.ENOENT:
|
|
return # no pidfile, ok
|
|
else:
|
|
raise Fatal("can't read %s: %s" % (_pidname, e))
|
|
if not oldpid:
|
|
os.unlink(_pidname)
|
|
return # invalid pidfile, ok
|
|
oldpid = int(oldpid.strip() or 0)
|
|
if oldpid <= 0:
|
|
os.unlink(_pidname)
|
|
return # invalid pidfile, ok
|
|
try:
|
|
os.kill(oldpid, 0)
|
|
except OSError as e:
|
|
if e.errno == errno.ESRCH:
|
|
os.unlink(_pidname)
|
|
return # outdated pidfile, ok
|
|
elif e.errno == errno.EPERM:
|
|
pass
|
|
else:
|
|
raise
|
|
raise Fatal("%s: sshuttle is already running (pid=%d)"
|
|
% (_pidname, oldpid))
|
|
|
|
|
|
def daemonize():
|
|
if os.fork():
|
|
os._exit(0)
|
|
os.setsid()
|
|
if os.fork():
|
|
os._exit(0)
|
|
|
|
outfd = os.open(_pidname, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
|
|
try:
|
|
os.write(outfd, b'%d\n' % os.getpid())
|
|
finally:
|
|
os.close(outfd)
|
|
os.chdir("/")
|
|
|
|
# Normal exit when killed, or try/finally won't work and the pidfile won't
|
|
# be deleted.
|
|
signal.signal(signal.SIGTERM, got_signal)
|
|
|
|
si = open(os.devnull, 'r+')
|
|
os.dup2(si.fileno(), 0)
|
|
os.dup2(si.fileno(), 1)
|
|
si.close()
|
|
|
|
|
|
def daemon_cleanup():
|
|
try:
|
|
os.unlink(_pidname)
|
|
except OSError as e:
|
|
if e.errno == errno.ENOENT:
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
|
|
class MultiListener:
|
|
|
|
def __init__(self, kind=socket.SOCK_STREAM, proto=0):
|
|
self.type = kind
|
|
self.proto = proto
|
|
self.v6 = None
|
|
self.v4 = None
|
|
self.bind_called = False
|
|
|
|
def setsockopt(self, level, optname, value):
|
|
assert(self.bind_called)
|
|
if self.v6:
|
|
self.v6.setsockopt(level, optname, value)
|
|
if self.v4:
|
|
self.v4.setsockopt(level, optname, value)
|
|
|
|
def add_handler(self, handlers, callback, method, mux):
|
|
assert(self.bind_called)
|
|
socks = []
|
|
if self.v6:
|
|
socks.append(self.v6)
|
|
if self.v4:
|
|
socks.append(self.v4)
|
|
|
|
handlers.append(
|
|
Handler(
|
|
socks,
|
|
lambda sock: callback(sock, method, mux, handlers)
|
|
)
|
|
)
|
|
|
|
def listen(self, backlog):
|
|
assert(self.bind_called)
|
|
if self.v6:
|
|
self.v6.listen(backlog)
|
|
if self.v4:
|
|
try:
|
|
self.v4.listen(backlog)
|
|
except socket.error as e:
|
|
# on some systems v4 bind will fail if the v6 suceeded,
|
|
# in this case the v6 socket will receive v4 too.
|
|
if e.errno == errno.EADDRINUSE and self.v6:
|
|
self.v4 = None
|
|
else:
|
|
raise e
|
|
|
|
def bind(self, address_v6, address_v4):
|
|
assert(not self.bind_called)
|
|
self.bind_called = True
|
|
if address_v6 is not None:
|
|
self.v6 = socket.socket(socket.AF_INET6, self.type, self.proto)
|
|
self.v6.bind(address_v6)
|
|
else:
|
|
self.v6 = None
|
|
if address_v4 is not None:
|
|
self.v4 = socket.socket(socket.AF_INET, self.type, self.proto)
|
|
self.v4.bind(address_v4)
|
|
else:
|
|
self.v4 = None
|
|
|
|
def print_listening(self, what):
|
|
assert(self.bind_called)
|
|
if self.v6:
|
|
listenip = self.v6.getsockname()
|
|
debug1('%s listening on %r.' % (what, listenip))
|
|
debug2('%s listening with %r.' % (what, self.v6))
|
|
if self.v4:
|
|
listenip = self.v4.getsockname()
|
|
debug1('%s listening on %r.' % (what, listenip))
|
|
debug2('%s listening with %r.' % (what, self.v4))
|
|
|
|
|
|
class FirewallClient:
|
|
|
|
def __init__(self, method_name, sudo_pythonpath, ttl):
|
|
self.auto_nets = []
|
|
python_path = os.path.dirname(os.path.dirname(__file__))
|
|
argvbase = ([sys.executable, sys.argv[0]] +
|
|
['-v'] * (helpers.verbose or 0) +
|
|
['--method', method_name] +
|
|
['--firewall'] +
|
|
['--ttl', ttl])
|
|
if ssyslog._p:
|
|
argvbase += ['--syslog']
|
|
|
|
# Determine how to prefix the command in order to elevate privileges.
|
|
if platform.platform().startswith('OpenBSD'):
|
|
elev_prefix = ['doas'] # OpenBSD uses built in `doas`
|
|
else:
|
|
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
|
|
|
|
# Look for binary and switch to absolute path if we can find
|
|
# it.
|
|
path = which(elev_prefix[0])
|
|
if path:
|
|
elev_prefix[0] = path
|
|
|
|
if sudo_pythonpath:
|
|
elev_prefix += ['/usr/bin/env',
|
|
'PYTHONPATH=%s' % python_path]
|
|
argv_tries = [elev_prefix + argvbase, argvbase]
|
|
|
|
# we can't use stdin/stdout=subprocess.PIPE here, as we normally would,
|
|
# because stupid Linux 'su' requires that stdin be attached to a tty.
|
|
# Instead, attach a *bidirectional* socket to its stdout, and use
|
|
# that for talking in both directions.
|
|
(s1, s2) = socket.socketpair()
|
|
|
|
def setup():
|
|
# run in the child process
|
|
s2.close()
|
|
if os.getuid() == 0:
|
|
argv_tries = argv_tries[-1:] # last entry only
|
|
for argv in argv_tries:
|
|
try:
|
|
if argv[0] == 'su':
|
|
sys.stderr.write('[local su] ')
|
|
self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup)
|
|
# No env: Talking to `FirewallClient.start`, which has no i18n.
|
|
break
|
|
except OSError as e:
|
|
log('Spawning firewall manager: %r' % argv)
|
|
raise Fatal(e)
|
|
self.argv = argv
|
|
s1.close()
|
|
self.pfile = s2.makefile('rwb')
|
|
line = self.pfile.readline()
|
|
self.check()
|
|
if line[0:5] != b'READY':
|
|
raise Fatal('%r expected READY, got %r' % (self.argv, line))
|
|
method_name = line[6:-1]
|
|
self.method = get_method(method_name.decode("ASCII"))
|
|
self.method.set_firewall(self)
|
|
|
|
def setup(self, subnets_include, subnets_exclude, nslist,
|
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4, udp,
|
|
user, tmark, ttl):
|
|
self.subnets_include = subnets_include
|
|
self.subnets_exclude = subnets_exclude
|
|
self.nslist = nslist
|
|
self.redirectport_v6 = redirectport_v6
|
|
self.redirectport_v4 = redirectport_v4
|
|
self.dnsport_v6 = dnsport_v6
|
|
self.dnsport_v4 = dnsport_v4
|
|
self.udp = udp
|
|
self.user = user
|
|
self.tmark = tmark
|
|
self.ttl = ttl
|
|
|
|
def check(self):
|
|
rv = self.p.poll()
|
|
if rv:
|
|
raise Fatal('%r returned %d' % (self.argv, rv))
|
|
|
|
def start(self):
|
|
self.pfile.write(b'ROUTES\n')
|
|
for (family, ip, width, fport, lport) \
|
|
in self.subnets_include + self.auto_nets:
|
|
self.pfile.write(b'%d,%d,0,%s,%d,%d\n' % (family, width,
|
|
ip.encode("ASCII"),
|
|
fport, lport))
|
|
for (family, ip, width, fport, lport) in self.subnets_exclude:
|
|
self.pfile.write(b'%d,%d,1,%s,%d,%d\n' % (family, width,
|
|
ip.encode("ASCII"),
|
|
fport, lport))
|
|
|
|
self.pfile.write(b'NSLIST\n')
|
|
for (family, ip) in self.nslist:
|
|
self.pfile.write(b'%d,%s\n'
|
|
% (family, ip.encode("ASCII")))
|
|
|
|
self.pfile.write(
|
|
b'PORTS %d,%d,%d,%d\n'
|
|
% (self.redirectport_v6, self.redirectport_v4,
|
|
self.dnsport_v6, self.dnsport_v4))
|
|
|
|
udp = 0
|
|
if self.udp:
|
|
udp = 1
|
|
if self.user is None:
|
|
user = b'-'
|
|
elif isinstance(self.user, str):
|
|
user = bytes(self.user, 'utf-8')
|
|
else:
|
|
user = b'%d' % self.user
|
|
|
|
self.pfile.write(b'GO %d %s\n' % (udp, user))
|
|
self.pfile.flush()
|
|
|
|
line = self.pfile.readline()
|
|
self.check()
|
|
if line != b'STARTED\n':
|
|
raise Fatal('%r expected STARTED, got %r' % (self.argv, line))
|
|
|
|
def sethostip(self, hostname, ip):
|
|
assert(not re.search(br'[^-\w\.]', hostname))
|
|
assert(not re.search(br'[^0-9.]', ip))
|
|
self.pfile.write(b'HOST %s,%s\n' % (hostname, ip))
|
|
self.pfile.flush()
|
|
|
|
def done(self):
|
|
self.pfile.close()
|
|
rv = self.p.wait()
|
|
if rv:
|
|
raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
|
|
|
|
|
|
dnsreqs = {}
|
|
udp_by_src = {}
|
|
|
|
|
|
def expire_connections(now, mux):
|
|
remove = []
|
|
for chan, timeout in dnsreqs.items():
|
|
if timeout < now:
|
|
debug3('expiring dnsreqs channel=%d' % chan)
|
|
remove.append(chan)
|
|
del mux.channels[chan]
|
|
for chan in remove:
|
|
del dnsreqs[chan]
|
|
debug3('Remaining DNS requests: %d' % len(dnsreqs))
|
|
|
|
remove = []
|
|
for peer, (chan, timeout) in udp_by_src.items():
|
|
if timeout < now:
|
|
debug3('expiring UDP channel channel=%d peer=%r' % (chan, peer))
|
|
mux.send(chan, ssnet.CMD_UDP_CLOSE, b'')
|
|
remove.append(peer)
|
|
del mux.channels[chan]
|
|
for peer in remove:
|
|
del udp_by_src[peer]
|
|
debug3('Remaining UDP channels: %d' % len(udp_by_src))
|
|
|
|
|
|
def onaccept_tcp(listener, method, mux, handlers):
|
|
global _extra_fd
|
|
try:
|
|
sock, srcip = listener.accept()
|
|
except socket.error as e:
|
|
if e.args[0] in [errno.EMFILE, errno.ENFILE]:
|
|
debug1('Rejected incoming connection: too many open files!')
|
|
# free up an fd so we can eat the connection
|
|
os.close(_extra_fd)
|
|
try:
|
|
sock, srcip = listener.accept()
|
|
sock.close()
|
|
finally:
|
|
_extra_fd = os.open(os.devnull, os.O_RDONLY)
|
|
return
|
|
else:
|
|
raise
|
|
|
|
dstip = method.get_tcp_dstip(sock)
|
|
debug1('Accept TCP: %s:%r -> %s:%r.' % (srcip[0], srcip[1],
|
|
dstip[0], dstip[1]))
|
|
if dstip[1] == sock.getsockname()[1] and islocal(dstip[0], sock.family):
|
|
debug1("-- ignored: that's my address!")
|
|
sock.close()
|
|
return
|
|
chan = mux.next_channel()
|
|
if not chan:
|
|
log('warning: too many open channels. Discarded connection.')
|
|
sock.close()
|
|
return
|
|
mux.send(chan, ssnet.CMD_TCP_CONNECT, b'%d,%s,%d' %
|
|
(sock.family, dstip[0].encode("ASCII"), dstip[1]))
|
|
outwrap = MuxWrapper(mux, chan)
|
|
handlers.append(Proxy(SockWrapper(sock, sock), outwrap))
|
|
expire_connections(time.time(), mux)
|
|
|
|
|
|
def udp_done(chan, data, method, sock, dstip):
|
|
(src, srcport, data) = data.split(b",", 2)
|
|
srcip = (src, int(srcport))
|
|
debug3('doing send from %r to %r' % (srcip, dstip,))
|
|
method.send_udp(sock, srcip, dstip, data)
|
|
|
|
|
|
def onaccept_udp(listener, method, mux, handlers):
|
|
now = time.time()
|
|
t = method.recv_udp(listener, 4096)
|
|
if t is None:
|
|
return
|
|
srcip, dstip, data = t
|
|
debug1('Accept UDP: %r -> %r.' % (srcip, dstip,))
|
|
if srcip in udp_by_src:
|
|
chan, _ = udp_by_src[srcip]
|
|
else:
|
|
chan = mux.next_channel()
|
|
mux.channels[chan] = lambda cmd, data: udp_done(
|
|
chan, data, method, listener, dstip=srcip)
|
|
mux.send(chan, ssnet.CMD_UDP_OPEN, b"%d" % listener.family)
|
|
udp_by_src[srcip] = chan, now + 30
|
|
|
|
hdr = b"%s,%d," % (dstip[0].encode("ASCII"), dstip[1])
|
|
mux.send(chan, ssnet.CMD_UDP_DATA, hdr + data)
|
|
|
|
expire_connections(now, mux)
|
|
|
|
|
|
def dns_done(chan, data, method, sock, srcip, dstip, mux):
|
|
debug3('dns_done: channel=%d src=%r dst=%r' % (chan, srcip, dstip))
|
|
del mux.channels[chan]
|
|
del dnsreqs[chan]
|
|
method.send_udp(sock, srcip, dstip, data)
|
|
|
|
|
|
def ondns(listener, method, mux, handlers):
|
|
now = time.time()
|
|
t = method.recv_udp(listener, 4096)
|
|
if t is None:
|
|
return
|
|
srcip, dstip, data = t
|
|
# dstip is None if we are using a method where we can't determine
|
|
# the destination IP of the DNS request that we captured from the client.
|
|
if dstip is None:
|
|
debug1('DNS request from %r: %d bytes' % (srcip, len(data)))
|
|
else:
|
|
debug1('DNS request from %r to %r: %d bytes' %
|
|
(srcip, dstip, len(data)))
|
|
chan = mux.next_channel()
|
|
dnsreqs[chan] = now + 30
|
|
mux.send(chan, ssnet.CMD_DNS_REQ, data)
|
|
mux.channels[chan] = lambda cmd, data: dns_done(
|
|
chan, data, method, listener, srcip=dstip, dstip=srcip, mux=mux)
|
|
expire_connections(now, mux)
|
|
|
|
|
|
def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|
python, latency_control, latency_buffer_size,
|
|
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
|
|
to_nameserver, ttl):
|
|
|
|
helpers.logprefix = 'c : '
|
|
debug1('Starting client with Python version %s'
|
|
% platform.python_version())
|
|
|
|
method = fw.method
|
|
|
|
handlers = []
|
|
debug1('Connecting to server...')
|
|
|
|
try:
|
|
(serverproc, serversock) = ssh.connect(
|
|
ssh_cmd, remotename, python,
|
|
stderr=ssyslog._p and ssyslog._p.stdin,
|
|
options=dict(latency_control=latency_control,
|
|
latency_buffer_size=latency_buffer_size,
|
|
auto_hosts=auto_hosts,
|
|
to_nameserver=to_nameserver,
|
|
auto_nets=auto_nets,
|
|
ttl=ttl))
|
|
except socket.error as e:
|
|
if e.args[0] == errno.EPIPE:
|
|
raise Fatal("failed to establish ssh session (1)")
|
|
else:
|
|
raise
|
|
mux = Mux(serversock.makefile("rb"), serversock.makefile("wb"))
|
|
handlers.append(mux)
|
|
|
|
expected = b'SSHUTTLE0001'
|
|
|
|
try:
|
|
v = 'x'
|
|
while v and v != b'\0':
|
|
v = serversock.recv(1)
|
|
v = 'x'
|
|
while v and v != b'\0':
|
|
v = serversock.recv(1)
|
|
initstring = serversock.recv(len(expected))
|
|
except socket.error as e:
|
|
if e.args[0] == errno.ECONNRESET:
|
|
raise Fatal("failed to establish ssh session (2)")
|
|
else:
|
|
raise
|
|
|
|
# Returns None if process is still running (or returns exit code)
|
|
rv = serverproc.poll()
|
|
if rv is not None:
|
|
errmsg = "server died with error code %d\n" % rv
|
|
|
|
# Our fatal exceptions return exit code 99
|
|
if rv == 99:
|
|
errmsg += "This error code likely means that python started and " \
|
|
"the sshuttle server started. However, the sshuttle server " \
|
|
"may have raised a 'Fatal' exception after it started."
|
|
elif rv == 98:
|
|
errmsg += "This error code likely means that we were able to " \
|
|
"run python on the server, but that the program continued " \
|
|
"to the line after we call python's exec() to execute " \
|
|
"sshuttle's server code. Try specifying the python " \
|
|
"executable to user on the server by passing --python " \
|
|
"to sshuttle."
|
|
|
|
# This error should only be possible when --python is not specified.
|
|
elif rv == 97 and not python:
|
|
errmsg += "This error code likely means that either we " \
|
|
"couldn't find python3 or python in the PATH on the " \
|
|
"server or that we do not have permission to run 'exec' in " \
|
|
"the /bin/sh shell on the server. Try specifying the " \
|
|
"python executable to use on the server by passing " \
|
|
"--python to sshuttle."
|
|
|
|
# POSIX sh standards says error code 127 is used when you try
|
|
# to execute a program that does not exist. See section 2.8.2
|
|
# of
|
|
# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08
|
|
elif rv == 127:
|
|
if python:
|
|
errmsg += "This error code likely means that we were not " \
|
|
"able to execute the python executable that specified " \
|
|
"with --python. You specified '%s'.\n" % python
|
|
if python.startswith("/"):
|
|
errmsg += "\nTip for users in a restricted shell on the " \
|
|
"server: The server may refuse to run programs " \
|
|
"specified with an absolute path. Try specifying " \
|
|
"just the name of the python executable. However, " \
|
|
"if python is not in your PATH and you cannot " \
|
|
"run programs specified with an absolute path, " \
|
|
"it is possible that sshuttle will not work."
|
|
else:
|
|
errmsg += "This error code likely means that we were unable " \
|
|
"to execute /bin/sh on the remote server. This can " \
|
|
"happen if /bin/sh does not exist on the server or if " \
|
|
"you are in a restricted shell that does not allow you " \
|
|
"to run programs specified with an absolute path. " \
|
|
"Try rerunning sshuttle with the --python parameter."
|
|
|
|
# When the redirected subnet includes the remote ssh host, the
|
|
# firewall rules can interrupt the ssh connection to the
|
|
# remote machine. This issue impacts some Linux machines. The
|
|
# user sees that the server dies with a broken pipe error and
|
|
# code 255.
|
|
#
|
|
# The solution to this problem is to exclude the remote
|
|
# server.
|
|
#
|
|
# There are many github issues from users encountering this
|
|
# problem. Most of the discussion on the topic is here:
|
|
# https://github.com/sshuttle/sshuttle/issues/191
|
|
elif rv == 255:
|
|
errmsg += "It might be possible to resolve this error by " \
|
|
"excluding the server that you are ssh'ing to. For example, " \
|
|
"if you are running 'sshuttle -v -r example.com 0/0' to " \
|
|
"redirect all traffic through example.com, then try " \
|
|
"'sshuttle -v -r example.com -x example.com 0/0' to " \
|
|
"exclude redirecting the connection to example.com itself " \
|
|
"(i.e., sshuttle's firewall rules may be breaking the " \
|
|
"ssh connection that it previously established). " \
|
|
"Alternatively, you may be able to use 'sshuttle -v -r " \
|
|
"example.com -x example.com:22 0/0' to redirect " \
|
|
"everything except ssh connections between your machine " \
|
|
"and example.com."
|
|
|
|
raise Fatal(errmsg)
|
|
|
|
if initstring != expected:
|
|
raise Fatal('expected server init string %r; got %r'
|
|
% (expected, initstring))
|
|
log('Connected to server.')
|
|
sys.stdout.flush()
|
|
if daemon:
|
|
daemonize()
|
|
log('daemonizing (%s).' % _pidname)
|
|
|
|
def onroutes(routestr):
|
|
if auto_nets:
|
|
for line in routestr.strip().split(b'\n'):
|
|
if not line:
|
|
continue
|
|
(family, ip, width) = line.split(b',', 2)
|
|
family = int(family)
|
|
width = int(width)
|
|
ip = ip.decode("ASCII")
|
|
if family == socket.AF_INET6 and tcp_listener.v6 is None:
|
|
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
|
|
if family == socket.AF_INET and tcp_listener.v4 is None:
|
|
debug2("Ignored auto net %d/%s/%d" % (family, ip, width))
|
|
else:
|
|
debug2("Adding auto net %d/%s/%d" % (family, ip, width))
|
|
fw.auto_nets.append((family, ip, width, 0, 0))
|
|
|
|
# we definitely want to do this *after* starting ssh, or we might end
|
|
# up intercepting the ssh connection!
|
|
#
|
|
# Moreover, now that we have the --auto-nets option, we have to wait
|
|
# for the server to send us that message anyway. Even if we haven't
|
|
# set --auto-nets, we might as well wait for the message first, then
|
|
# ignore its contents.
|
|
mux.got_routes = None
|
|
serverready()
|
|
|
|
mux.got_routes = onroutes
|
|
|
|
def serverready():
|
|
fw.start()
|
|
sdnotify.send(sdnotify.ready(), sdnotify.status('Connected'))
|
|
|
|
def onhostlist(hostlist):
|
|
debug2('got host list: %r' % hostlist)
|
|
for line in hostlist.strip().split():
|
|
if line:
|
|
name, ip = line.split(b',', 1)
|
|
fw.sethostip(name, ip)
|
|
mux.got_host_list = onhostlist
|
|
|
|
tcp_listener.add_handler(handlers, onaccept_tcp, method, mux)
|
|
|
|
if udp_listener:
|
|
udp_listener.add_handler(handlers, onaccept_udp, method, mux)
|
|
|
|
if dns_listener:
|
|
dns_listener.add_handler(handlers, ondns, method, mux)
|
|
|
|
if seed_hosts is not None:
|
|
debug1('seed_hosts: %r' % seed_hosts)
|
|
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
|
|
|
|
def check_ssh_alive():
|
|
if daemon:
|
|
# poll() won't tell us when process exited since the
|
|
# process is no longer our child (it returns 0 all the
|
|
# time).
|
|
if not psutil.pid_exists(serverproc.pid):
|
|
raise Fatal('ssh connection to server (pid %d) exited.' %
|
|
serverproc.pid)
|
|
else:
|
|
rv = serverproc.poll()
|
|
# poll returns None if process hasn't exited.
|
|
if rv is not None:
|
|
raise Fatal('ssh connection to server (pid %d) exited '
|
|
'with returncode %d' % (serverproc.pid, rv))
|
|
|
|
while 1:
|
|
check_ssh_alive()
|
|
ssnet.runonce(handlers, mux)
|
|
if latency_control:
|
|
mux.check_fullness()
|
|
|
|
|
|
def main(listenip_v6, listenip_v4,
|
|
ssh_cmd, remotename, python, latency_control,
|
|
latency_buffer_size, dns, nslist,
|
|
method_name, seed_hosts, auto_hosts, auto_nets,
|
|
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
|
user, sudo_pythonpath, tmark, ttl):
|
|
|
|
if not remotename:
|
|
print("WARNING: You must specify -r/--remote to securely route "
|
|
"traffic to a remote machine. Running without -r/--remote "
|
|
"is only recommended for testing.")
|
|
|
|
if daemon:
|
|
try:
|
|
check_daemon(pidfile)
|
|
except Fatal as e:
|
|
log("%s" % e)
|
|
return 5
|
|
debug1('Starting sshuttle proxy (version %s).' % __version__)
|
|
helpers.logprefix = 'c : '
|
|
|
|
fw = FirewallClient(method_name, sudo_pythonpath, ttl)
|
|
|
|
# If --dns is used, store the IP addresses that the client
|
|
# normally uses for DNS lookups in nslist. The firewall needs to
|
|
# redirect packets outgoing to this server to the remote host
|
|
# instead.
|
|
if dns:
|
|
nslist += resolvconf_nameservers(True)
|
|
if to_nameserver is not None:
|
|
to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
|
|
else:
|
|
# option doesn't make sense if we aren't proxying dns
|
|
if to_nameserver and len(to_nameserver) > 0:
|
|
print("WARNING: --to-ns option is ignored because --dns was not "
|
|
"used.")
|
|
to_nameserver = None
|
|
|
|
# Get family specific subnet lists. Also, the user may not specify
|
|
# any subnets if they use --auto-nets. In this case, our subnets
|
|
# list will be empty and the forwarded subnets will be determined
|
|
# later by the server.
|
|
subnets_v4 = [i for i in subnets_include if i[0] == socket.AF_INET]
|
|
subnets_v6 = [i for i in subnets_include if i[0] == socket.AF_INET6]
|
|
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
|
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
|
|
|
# Get available features from the firewall method
|
|
avail = fw.method.get_supported_features()
|
|
|
|
# A feature is "required" if the user supplies us parameters which
|
|
# implies that the feature is needed.
|
|
required = Features()
|
|
|
|
# Select the default addresses to bind to / listen to.
|
|
|
|
# Assume IPv4 is always available and should always be enabled. If
|
|
# a method doesn't provide IPv4 support or if we wish to run
|
|
# ipv6-only, changes to this code are required.
|
|
assert avail.ipv4
|
|
required.ipv4 = True
|
|
|
|
# listenip_v4 contains user specified value or it is set to "auto".
|
|
if listenip_v4 == "auto":
|
|
listenip_v4 = ('127.0.0.1', 0)
|
|
|
|
# listenip_v6 is...
|
|
# None when IPv6 is disabled.
|
|
# "auto" when listen address is unspecified.
|
|
# The user specified address if provided by user
|
|
if listenip_v6 is None:
|
|
debug1("IPv6 disabled by --disable-ipv6")
|
|
if listenip_v6 == "auto":
|
|
if avail.ipv6:
|
|
debug1("IPv6 enabled: Using default IPv6 listen address ::1")
|
|
listenip_v6 = ('::1', 0)
|
|
else:
|
|
debug1("IPv6 disabled since it isn't supported by method "
|
|
"%s." % fw.method.name)
|
|
listenip_v6 = None
|
|
|
|
# Make final decision about enabling IPv6:
|
|
required.ipv6 = False
|
|
if listenip_v6:
|
|
required.ipv6 = True
|
|
|
|
# If we get here, it is possible that listenip_v6 was user
|
|
# specified but not supported by the current method.
|
|
if required.ipv6 and not avail.ipv6:
|
|
raise Fatal("An IPv6 listen address was supplied, but IPv6 is "
|
|
"disabled at your request or is unsupported by the %s "
|
|
"method." % fw.method.name)
|
|
|
|
if user is not None:
|
|
if getpwnam is None:
|
|
raise Fatal("Routing by user not available on this system.")
|
|
try:
|
|
user = getpwnam(user).pw_uid
|
|
except KeyError:
|
|
raise Fatal("User %s does not exist." % user)
|
|
required.user = False if user is None else True
|
|
|
|
if not required.ipv6 and len(subnets_v6) > 0:
|
|
print("WARNING: IPv6 subnets were ignored because IPv6 is disabled "
|
|
"in sshuttle.")
|
|
subnets_v6 = []
|
|
subnets_include = subnets_v4
|
|
|
|
required.udp = avail.udp # automatically enable UDP if it is available
|
|
required.dns = len(nslist) > 0
|
|
|
|
# Remove DNS servers using IPv6.
|
|
if required.dns:
|
|
if not required.ipv6 and len(nslist_v6) > 0:
|
|
print("WARNING: Your system is configured to use an IPv6 DNS "
|
|
"server but sshuttle is not using IPv6. Therefore DNS "
|
|
"traffic your system sends to the IPv6 DNS server won't "
|
|
"be redirected via sshuttle to the remote machine.")
|
|
nslist_v6 = []
|
|
nslist = nslist_v4
|
|
|
|
if len(nslist) == 0:
|
|
raise Fatal("Can't redirect DNS traffic since IPv6 is not "
|
|
"enabled in sshuttle and all of the system DNS "
|
|
"servers are IPv6.")
|
|
|
|
# If we aren't using IPv6, we can safely ignore excluded IPv6 subnets.
|
|
if not required.ipv6:
|
|
orig_len = len(subnets_exclude)
|
|
subnets_exclude = [i for i in subnets_exclude
|
|
if i[0] == socket.AF_INET]
|
|
if len(subnets_exclude) < orig_len:
|
|
print("WARNING: Ignoring one or more excluded IPv6 subnets "
|
|
"because IPv6 is not enabled.")
|
|
|
|
# This will print error messages if we required a feature that
|
|
# isn't available by the current method.
|
|
fw.method.assert_features(required)
|
|
|
|
# display features enabled
|
|
def feature_status(label, enabled, available):
|
|
msg = label + ": "
|
|
if enabled:
|
|
msg += "on"
|
|
else:
|
|
msg += "off "
|
|
if available:
|
|
msg += "(available)"
|
|
else:
|
|
msg += "(not available with %s method)" % fw.method.name
|
|
debug1(msg)
|
|
|
|
debug1("Method: %s" % fw.method.name)
|
|
feature_status("IPv4", required.ipv4, avail.ipv4)
|
|
feature_status("IPv6", required.ipv6, avail.ipv6)
|
|
feature_status("UDP ", required.udp, avail.udp)
|
|
feature_status("DNS ", required.dns, avail.dns)
|
|
feature_status("User", required.user, avail.user)
|
|
|
|
# Exclude traffic destined to our listen addresses.
|
|
if required.ipv4 and \
|
|
not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
|
|
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0))
|
|
|
|
if required.ipv6 and \
|
|
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
|
|
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
|
|
|
|
# We don't print the IP+port of where we are listening here
|
|
# because we do that below when we have identified the ports to
|
|
# listen on.
|
|
debug1("Subnets to forward through remote host (type, IP, cidr mask "
|
|
"width, startPort, endPort):")
|
|
for i in subnets_include:
|
|
debug1(" "+str(i))
|
|
if auto_nets:
|
|
debug1("NOTE: Additional subnets to forward may be added below by "
|
|
"--auto-nets.")
|
|
debug1("Subnets to exclude from forwarding:")
|
|
for i in subnets_exclude:
|
|
debug1(" "+str(i))
|
|
if required.dns:
|
|
debug1("DNS requests normally directed at these servers will be "
|
|
"redirected to remote:")
|
|
for i in nslist:
|
|
debug1(" "+str(i))
|
|
|
|
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
|
|
# if both ports given, no need to search for a spare port
|
|
ports = [0, ]
|
|
else:
|
|
# if at least one port missing, we have to search
|
|
ports = range(12300, 9000, -1)
|
|
# keep track of failed bindings and used ports to avoid trying to
|
|
# bind to the same socket address twice in different listeners
|
|
used_ports = []
|
|
|
|
# search for free ports and try to bind
|
|
last_e = None
|
|
redirectport_v6 = 0
|
|
redirectport_v4 = 0
|
|
bound = False
|
|
for port in ports:
|
|
debug2('Trying to bind redirector on port %d' % port)
|
|
tcp_listener = MultiListener()
|
|
|
|
if required.udp:
|
|
udp_listener = MultiListener(socket.SOCK_DGRAM)
|
|
else:
|
|
udp_listener = None
|
|
|
|
if listenip_v6 and listenip_v6[1]:
|
|
lv6 = listenip_v6
|
|
redirectport_v6 = lv6[1]
|
|
elif listenip_v6:
|
|
lv6 = (listenip_v6[0], port)
|
|
redirectport_v6 = port
|
|
else:
|
|
lv6 = None
|
|
redirectport_v6 = 0
|
|
|
|
if listenip_v4 and listenip_v4[1]:
|
|
lv4 = listenip_v4
|
|
redirectport_v4 = lv4[1]
|
|
elif listenip_v4:
|
|
lv4 = (listenip_v4[0], port)
|
|
redirectport_v4 = port
|
|
else:
|
|
lv4 = None
|
|
redirectport_v4 = 0
|
|
|
|
try:
|
|
tcp_listener.bind(lv6, lv4)
|
|
if udp_listener:
|
|
udp_listener.bind(lv6, lv4)
|
|
bound = True
|
|
used_ports.append(port)
|
|
break
|
|
except socket.error as e:
|
|
if e.errno == errno.EADDRINUSE:
|
|
last_e = e
|
|
used_ports.append(port)
|
|
else:
|
|
raise e
|
|
|
|
if not bound:
|
|
assert(last_e)
|
|
raise last_e
|
|
tcp_listener.listen(10)
|
|
tcp_listener.print_listening("TCP redirector")
|
|
if udp_listener:
|
|
udp_listener.print_listening("UDP redirector")
|
|
|
|
bound = False
|
|
if required.dns:
|
|
# search for spare port for DNS
|
|
ports = range(12300, 9000, -1)
|
|
for port in ports:
|
|
debug2('Trying to bind DNS redirector on port %d' % port)
|
|
if port in used_ports:
|
|
continue
|
|
|
|
dns_listener = MultiListener(socket.SOCK_DGRAM)
|
|
|
|
if listenip_v6:
|
|
lv6 = (listenip_v6[0], port)
|
|
dnsport_v6 = port
|
|
else:
|
|
lv6 = None
|
|
dnsport_v6 = 0
|
|
|
|
if listenip_v4:
|
|
lv4 = (listenip_v4[0], port)
|
|
dnsport_v4 = port
|
|
else:
|
|
lv4 = None
|
|
dnsport_v4 = 0
|
|
|
|
try:
|
|
dns_listener.bind(lv6, lv4)
|
|
bound = True
|
|
used_ports.append(port)
|
|
break
|
|
except socket.error as e:
|
|
if e.errno == errno.EADDRINUSE:
|
|
last_e = e
|
|
used_ports.append(port)
|
|
else:
|
|
raise e
|
|
|
|
dns_listener.print_listening("DNS")
|
|
if not bound:
|
|
assert(last_e)
|
|
raise last_e
|
|
else:
|
|
dnsport_v6 = 0
|
|
dnsport_v4 = 0
|
|
dns_listener = None
|
|
|
|
# Last minute sanity checks.
|
|
# These should never fail.
|
|
# If these do fail, something is broken above.
|
|
if subnets_v6:
|
|
assert required.ipv6
|
|
if redirectport_v6 == 0:
|
|
raise Fatal("IPv6 subnets defined but not listening")
|
|
|
|
if nslist_v6:
|
|
assert required.dns
|
|
assert required.ipv6
|
|
if dnsport_v6 == 0:
|
|
raise Fatal("IPv6 ns servers defined but not listening")
|
|
|
|
if subnets_v4:
|
|
if redirectport_v4 == 0:
|
|
raise Fatal("IPv4 subnets defined but not listening")
|
|
|
|
if nslist_v4:
|
|
if dnsport_v4 == 0:
|
|
raise Fatal("IPv4 ns servers defined but not listening")
|
|
|
|
# setup method specific stuff on listeners
|
|
fw.method.setup_tcp_listener(tcp_listener)
|
|
if udp_listener:
|
|
fw.method.setup_udp_listener(udp_listener)
|
|
if dns_listener:
|
|
fw.method.setup_udp_listener(dns_listener)
|
|
|
|
# start the firewall
|
|
fw.setup(subnets_include, subnets_exclude, nslist,
|
|
redirectport_v6, redirectport_v4, dnsport_v6, dnsport_v4,
|
|
required.udp, user, tmark, ttl)
|
|
|
|
# start the client process
|
|
try:
|
|
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|
python, latency_control, latency_buffer_size,
|
|
dns_listener, seed_hosts, auto_hosts, auto_nets,
|
|
daemon, to_nameserver, ttl)
|
|
finally:
|
|
try:
|
|
if daemon:
|
|
# it's not our child anymore; can't waitpid
|
|
fw.p.returncode = 0
|
|
fw.done()
|
|
sdnotify.send(sdnotify.stop())
|
|
|
|
finally:
|
|
if daemon:
|
|
daemon_cleanup()
|