mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-04-17 15:58:52 +02:00
Python2 ignores the byte string qualification (b’foo’) but falls over for the combination rb for this regexp. Switching the qualification to br appears to fix this and works in both python2 and python3.
812 lines
25 KiB
Python
812 lines
25 KiB
Python
import errno
|
|
import re
|
|
import signal
|
|
import time
|
|
import subprocess as ssubprocess
|
|
import os
|
|
import sys
|
|
import platform
|
|
|
|
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
|
|
from sshuttle.methods import get_method, Features
|
|
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\n' % 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.\n' % (what, listenip))
|
|
debug2('%s listening with %r.\n' % (what, self.v6))
|
|
if self.v4:
|
|
listenip = self.v4.getsockname()
|
|
debug1('%s listening on %r.\n' % (what, listenip))
|
|
debug2('%s listening with %r.\n' % (what, self.v4))
|
|
|
|
|
|
class FirewallClient:
|
|
|
|
def __init__(self, method_name, sudo_pythonpath):
|
|
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'])
|
|
if ssyslog._p:
|
|
argvbase += ['--syslog']
|
|
# Default to sudo unless on OpenBSD in which case use built in `doas`
|
|
if platform.platform().startswith('OpenBSD'):
|
|
elev_prefix = ['doas']
|
|
else:
|
|
elev_prefix = ['sudo', '-p', '[local sudo] Password: ']
|
|
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()
|
|
e = None
|
|
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.
|
|
e = None
|
|
break
|
|
except OSError:
|
|
pass
|
|
self.argv = argv
|
|
s1.close()
|
|
self.pfile = s2.makefile('rwb')
|
|
if e:
|
|
log('Spawning firewall manager: %r\n' % self.argv)
|
|
raise Fatal(e)
|
|
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):
|
|
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
|
|
|
|
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\n' % chan)
|
|
remove.append(chan)
|
|
del mux.channels[chan]
|
|
for chan in remove:
|
|
del dnsreqs[chan]
|
|
debug3('Remaining DNS requests: %d\n' % len(dnsreqs))
|
|
|
|
remove = []
|
|
for peer, (chan, timeout) in udp_by_src.items():
|
|
if timeout < now:
|
|
debug3('expiring UDP channel channel=%d peer=%r\n' % (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\n' % 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!\n')
|
|
# 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.\n' % (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!\n")
|
|
sock.close()
|
|
return
|
|
chan = mux.next_channel()
|
|
if not chan:
|
|
log('warning: too many open channels. Discarded connection.\n')
|
|
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\n' % (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.\n' % (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\n' % (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
|
|
debug1('DNS request from %r to %r: %d bytes\n' % (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,
|
|
dns_listener, seed_hosts, auto_hosts, auto_nets, daemon,
|
|
to_nameserver):
|
|
|
|
debug1('Starting client with Python version %s\n'
|
|
% platform.python_version())
|
|
|
|
method = fw.method
|
|
|
|
handlers = []
|
|
if helpers.verbose >= 1:
|
|
helpers.logprefix = 'c : '
|
|
else:
|
|
helpers.logprefix = 'client: '
|
|
debug1('connecting to server...\n')
|
|
|
|
try:
|
|
(serverproc, serversock) = ssh.connect(
|
|
ssh_cmd, remotename, python,
|
|
stderr=ssyslog._p and ssyslog._p.stdin,
|
|
options=dict(latency_control=latency_control,
|
|
auto_hosts=auto_hosts,
|
|
to_nameserver=to_nameserver,
|
|
auto_nets=auto_nets))
|
|
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
|
|
|
|
rv = serverproc.poll()
|
|
if rv:
|
|
raise Fatal('server died with error code %d' % rv)
|
|
|
|
if initstring != expected:
|
|
raise Fatal('expected server init string %r; got %r'
|
|
% (expected, initstring))
|
|
log('Connected.\n')
|
|
sys.stdout.flush()
|
|
if daemon:
|
|
daemonize()
|
|
log('daemonizing (%s).\n' % _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\n" % (family, ip, width))
|
|
if family == socket.AF_INET and tcp_listener.v4 is None:
|
|
debug2("Ignored auto net %d/%s/%d\n" % (family, ip, width))
|
|
else:
|
|
debug2("Adding auto net %d/%s/%d\n" % (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\n' % 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\n' % seed_hosts)
|
|
mux.send(0, ssnet.CMD_HOST_REQ, str.encode('\n'.join(seed_hosts)))
|
|
|
|
while 1:
|
|
rv = serverproc.poll()
|
|
if rv:
|
|
raise Fatal('server died with error code %d' % rv)
|
|
|
|
ssnet.runonce(handlers, mux)
|
|
if latency_control:
|
|
mux.check_fullness()
|
|
|
|
|
|
def main(listenip_v6, listenip_v4,
|
|
ssh_cmd, remotename, python, latency_control, dns, nslist,
|
|
method_name, seed_hosts, auto_hosts, auto_nets,
|
|
subnets_include, subnets_exclude, daemon, to_nameserver, pidfile,
|
|
user, sudo_pythonpath):
|
|
|
|
if not remotename:
|
|
# XXX: We can't make it required at the argparse level,
|
|
# because sshuttle calls out to itself in FirewallClient.
|
|
raise Fatal("You must specify -r/--remote.")
|
|
|
|
if daemon:
|
|
try:
|
|
check_daemon(pidfile)
|
|
except Fatal as e:
|
|
log("%s\n" % e)
|
|
return 5
|
|
debug1('Starting sshuttle proxy.\n')
|
|
|
|
fw = FirewallClient(method_name, sudo_pythonpath)
|
|
|
|
# Get family specific subnet lists
|
|
if dns:
|
|
nslist += resolvconf_nameservers()
|
|
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
|
|
to_nameserver = None
|
|
|
|
subnets = subnets_include + subnets_exclude # we don't care here
|
|
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
|
|
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
|
|
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
|
|
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
|
|
|
|
# Check features available
|
|
avail = fw.method.get_supported_features()
|
|
required = Features()
|
|
|
|
if listenip_v6 == "auto":
|
|
if avail.ipv6:
|
|
listenip_v6 = ('::1', 0)
|
|
else:
|
|
listenip_v6 = None
|
|
|
|
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)
|
|
|
|
if fw.method.name != 'nat':
|
|
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
|
|
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
|
|
else:
|
|
required.ipv6 = None
|
|
required.ipv4 = None
|
|
|
|
required.udp = avail.udp
|
|
required.dns = len(nslist) > 0
|
|
required.user = False if user is None else True
|
|
|
|
# if IPv6 not supported, ignore IPv6 DNS servers
|
|
if not required.ipv6:
|
|
nslist_v6 = []
|
|
nslist = nslist_v4
|
|
|
|
fw.method.assert_features(required)
|
|
|
|
if required.ipv6 and listenip_v6 is None:
|
|
raise Fatal("IPv6 required but not listening.")
|
|
|
|
# display features enabled
|
|
debug1("IPv6 enabled: %r\n" % required.ipv6)
|
|
debug1("UDP enabled: %r\n" % required.udp)
|
|
debug1("DNS enabled: %r\n" % required.dns)
|
|
debug1("User enabled: %r\n" % required.user)
|
|
|
|
# bind to required ports
|
|
if listenip_v4 == "auto":
|
|
listenip_v4 = ('127.0.0.1', 0)
|
|
|
|
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))
|
|
|
|
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
|
|
debug2('Binding redirector:')
|
|
for port in ports:
|
|
debug2(' %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
|
|
|
|
debug2('\n')
|
|
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
|
|
debug2('Binding DNS:')
|
|
ports = range(12300, 9000, -1)
|
|
for port in ports:
|
|
debug2(' %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
|
|
debug2('\n')
|
|
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)
|
|
|
|
# start the client process
|
|
try:
|
|
return _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
|
|
python, latency_control, dns_listener,
|
|
seed_hosts, auto_hosts, auto_nets, daemon, to_nameserver)
|
|
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()
|