Exemple #1
0
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: tshuttle is already running (pid=%d)" %
                (_pidname, oldpid))
Exemple #2
0
 def recv_udp(listener, bufsize):
     debug3('Accept UDP using socket_ext recvmsg.')
     srcip, data, adata, _ = listener.recvmsg((bufsize, ),
                                              socket.CMSG_SPACE(24))
     dstip = None
     family = None
     for a in adata:
         if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_ORIGDSTADDR:
             family, port = struct.unpack('=HH', a.cmsg_data[0:4])
             port = socket.htons(port)
             if family == socket.AF_INET:
                 start = 4
                 length = 4
             else:
                 raise Fatal("Unsupported socket type '%s'" % family)
             ip = socket.inet_ntop(family,
                                   a.cmsg_data[start:start + length])
             dstip = (ip, port)
             break
         elif a.cmsg_level == SOL_IPV6 and a.cmsg_type == IPV6_ORIGDSTADDR:
             family, port = struct.unpack('=HH', a.cmsg_data[0:4])
             port = socket.htons(port)
             if family == socket.AF_INET6:
                 start = 8
                 length = 16
             else:
                 raise Fatal("Unsupported socket type '%s'" % family)
             ip = socket.inet_ntop(family,
                                   a.cmsg_data[start:start + length])
             dstip = (ip, port)
             break
     return (srcip, dstip, data[0])
Exemple #3
0
    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']

        # 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: '******'/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)
Exemple #4
0
def _fill_oldctls(prefix):
    argv = ['sysctl', prefix]
    p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
    for line in p.stdout:
        line = line.decode()
        assert (line[-1] == '\n')
        (k, v) = line[:-1].split(': ', 1)
        _oldctls[k] = v.strip()
    rv = p.wait()
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
    if not line:
        raise Fatal('%r returned no data' % (argv, ))
Exemple #5
0
 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))
Exemple #6
0
def ipfw_rule_exists(n):
    argv = ['ipfw', 'list']
    p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())

    found = False
    for line in p.stdout:
        if line.startswith(b'%05d ' % n):
            if not ('ipttl 63' in line or 'check-state' in line):
                log('non-tshuttle ipfw rule: %r' % line.strip())
                raise Fatal('non-tshuttle ipfw rule #%d already exists!' % n)
            found = True
    rv = p.wait()
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
    return found
Exemple #7
0
    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))
Exemple #8
0
 def assert_features(self, features):
     avail = self.get_supported_features()
     for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
         if getattr(features, key) and not getattr(avail, key):
             raise Fatal(
                 "Feature %s not supported with method %s." %
                 (key, self.name))
Exemple #9
0
def runonce(handlers, mux):
    r = []
    w = []
    x = []
    to_remove = [s for s in handlers if not s.ok]
    for h in to_remove:
        handlers.remove(h)

    for s in handlers:
        s.pre_select(r, w, x)
    debug2(
        'Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)' %
        (len(handlers), _fds(r), _fds(w), _fds(x), mux.fullness, mux.too_full))
    (r, w, x) = select.select(r, w, x)
    debug2('  Ready: %d r=%r w=%r x=%r' %
           (len(handlers), _fds(r), _fds(w), _fds(x)))
    ready = r + w + x
    did = {}
    for h in handlers:
        for s in h.socks:
            if s in ready:
                h.callback(s)
                did[s] = 1
    for s in ready:
        if s not in did:
            raise Fatal('socket %r was not used by any handler' % s)
Exemple #10
0
def ipfw(*args):
    argv = ['ipfw', '-q'] + list(args)
    debug1('>> %s' % ' '.join(argv))
    rv = ssubprocess.call(argv, env=get_env())
    # No env: No output. (Or error that won't be parsed.)
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
Exemple #11
0
def nft(family, table, action, *args):
    if family in (socket.AF_INET, socket.AF_INET6):
        argv = ['nft', action, 'inet', table] + list(args)
    else:
        raise Exception('Unsupported family "%s"' % family_to_string(family))
    debug1('%s' % ' '.join(argv))
    rv = ssubprocess.call(argv, env=get_env())
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
Exemple #12
0
def ipt(family, table, *args):
    if family == socket.AF_INET6:
        argv = ['ip6tables', '-t', table] + list(args)
    elif family == socket.AF_INET:
        argv = ['iptables', '-t', table] + list(args)
    else:
        raise Exception('Unsupported family "%s"' % family_to_string(family))
    debug1('%s' % ' '.join(argv))
    rv = ssubprocess.call(argv, env=get_env())
    if rv:
        raise Fatal('%r returned %d' % (argv, rv))
Exemple #13
0
 def udp_open(channel, data):
     debug2('Incoming UDP open.')
     family = int(data)
     mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd,
                                                       data)
     if channel in udphandlers:
         raise Fatal('UDP connection channel %d already open' %
                     channel)
     else:
         h = UdpProxy(mux, channel, family)
         handlers.append(h)
         udphandlers[channel] = h
Exemple #14
0
def pfctl(args, stdin=None):
    argv = ['pfctl'] + shlex.split(args)
    debug1('>> %s' % ' '.join(argv))
    p = ssubprocess.Popen(argv,
                          stdin=ssubprocess.PIPE,
                          stdout=ssubprocess.PIPE,
                          stderr=ssubprocess.PIPE,
                          env=get_env())
    o = p.communicate(stdin)
    if p.returncode:
        raise Fatal('%r returned %d' % (argv, p.returncode))

    return o
Exemple #15
0
def get_auto_method():
    debug3("Selecting a method automatically...")
    # Try these methods, in order:
    methods_to_try = ["nat", "nft", "pf", "ipfw"]
    for m in methods_to_try:
        method = get_method(m)
        if method.is_supported():
            debug3("Method '%s' was automatically selected." % m)
            return method

    raise Fatal("Unable to automatically find a supported method. Check that "
                "the appropriate programs are in your PATH. We tried "
                "methods: %s" % str(methods_to_try))
Exemple #16
0
 def hostwatch_ready(sock):
     assert(hw.pid)
     content = hw.sock.recv(4096)
     if content:
         lines = (hw.leftover + content).split(b('\n'))
         if lines[-1]:
             # no terminating newline: entry isn't complete yet!
             hw.leftover = lines.pop()
             lines.append(b(''))
         else:
             hw.leftover = b('')
         mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
     else:
         raise Fatal('hostwatch process died')
Exemple #17
0
def ipt_chain_exists(family, table, name):
    if family == socket.AF_INET6:
        cmd = 'ip6tables'
    elif family == socket.AF_INET:
        cmd = 'iptables'
    else:
        raise Exception('Unsupported family "%s"' % family_to_string(family))
    argv = [cmd, '-t', table, '-nL']
    try:
        output = ssubprocess.check_output(argv, env=get_env())
        for line in output.decode('ASCII').split('\n'):
            if line.startswith('Chain %s ' % name):
                return True
    except ssubprocess.CalledProcessError as e:
        raise Fatal('%r returned %d' % (argv, e.returncode))
Exemple #18
0
 def fill(self):
     try:
         os.set_blocking(self.rfile.fileno(), False)
     except AttributeError:
         # python < 3.5
         flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_GETFL)
         flags |= os.O_NONBLOCK
         flags = fcntl.fcntl(self.rfile.fileno(), fcntl.F_SETFL, flags)
     try:
         # If LATENCY_BUFFER_SIZE is inappropriately large, we will
         # get a MemoryError here. Read no more than 1MiB.
         read = _nb_clean(os.read, self.rfile.fileno(),
                          min(1048576, LATENCY_BUFFER_SIZE))
     except OSError:
         _, e = sys.exc_info()[:2]
         raise Fatal('other end: %r' % e)
     # log('<<< %r' % b)
     if read == b(''):  # EOF
         self.ok = False
     if read:
         self.inbuf += read
Exemple #19
0
def setup_daemon():
    if os.getuid() != 0:
        raise Fatal('You must be root (or enable su/sudo) to set the firewall')

    # don't disappear if our controlling terminal or stdout/stderr
    # disappears; we still have to clean up.
    signal.signal(signal.SIGHUP, signal.SIG_IGN)
    signal.signal(signal.SIGPIPE, signal.SIG_IGN)
    signal.signal(signal.SIGTERM, signal.SIG_IGN)
    signal.signal(signal.SIGINT, signal.SIG_IGN)

    # ctrl-c shouldn't be passed along to me.  When the main tshuttle dies,
    # I'll die automatically.
    os.setsid()

    # because of limitations of the 'su' command, the *real* stdin/stdout
    # are both attached to stdout initially.  Clone stdout into stdin so we
    # can read from it.
    os.dup2(1, 0)

    return sys.stdin, sys.stdout
Exemple #20
0
def original_dst(sock):
    ip = "0.0.0.0"
    port = -1
    try:
        family = sock.family
        SO_ORIGINAL_DST = 80

        if family == socket.AF_INET:
            SOCKADDR_MIN = 16
            sockaddr_in = sock.getsockopt(socket.SOL_IP,
                                          SO_ORIGINAL_DST, SOCKADDR_MIN)
            port, raw_ip = struct.unpack_from('!2xH4s', sockaddr_in[:8])
            ip = str(ipaddress.IPv4Address(raw_ip))
        elif family == socket.AF_INET6:
            sockaddr_in = sock.getsockopt(41, SO_ORIGINAL_DST, 64)
            port, raw_ip = struct.unpack_from("!2xH4x16s", sockaddr_in)
            ip = str(ipaddress.IPv6Address(raw_ip))
        else:
            raise Fatal("fw: Unknown family type.")
    except socket.error as e:
        if e.args[0] == errno.ENOPROTOOPT:
            return sock.getsockname()
        raise
    return (ip, port)
Exemple #21
0
def main(latency_control, latency_buffer_size, auto_hosts, to_nameserver,
         auto_nets):
    try:
        helpers.logprefix = ' s: '
        debug1('Starting server with Python version %s'
               % platform.python_version())

        debug1('latency control setting = %r' % latency_control)
        if latency_buffer_size:
            import tshuttle.ssnet as ssnet
            ssnet.LATENCY_BUFFER_SIZE = latency_buffer_size

        # synchronization header
        sys.stdout.write('\0\0SSHUTTLE0001')
        sys.stdout.flush()

        handlers = []
        mux = Mux(sys.stdin, sys.stdout)
        handlers.append(mux)

        debug1('auto-nets:' + str(auto_nets))
        if auto_nets:
            routes = list(list_routes())
            debug1('available routes:')
            for r in routes:
                debug1('  %d/%s/%d' % r)
        else:
            routes = []

        routepkt = ''
        for r in routes:
            routepkt += '%d,%s,%d\n' % r
        mux.send(0, ssnet.CMD_ROUTES, b(routepkt))

        hw = Hostwatch()
        hw.leftover = b('')

        def hostwatch_ready(sock):
            assert(hw.pid)
            content = hw.sock.recv(4096)
            if content:
                lines = (hw.leftover + content).split(b('\n'))
                if lines[-1]:
                    # no terminating newline: entry isn't complete yet!
                    hw.leftover = lines.pop()
                    lines.append(b(''))
                else:
                    hw.leftover = b('')
                mux.send(0, ssnet.CMD_HOST_LIST, b('\n').join(lines))
            else:
                raise Fatal('hostwatch process died')

        def got_host_req(data):
            if not hw.pid:
                (hw.pid, hw.sock) = start_hostwatch(
                        data.decode("ASCII").strip().split(), auto_hosts)
                handlers.append(Handler(socks=[hw.sock],
                                        callback=hostwatch_ready))
        mux.got_host_req = got_host_req

        def new_channel(channel, data):
            (family, dstip, dstport) = data.decode("ASCII").split(',', 2)
            family = int(family)
            # AF_INET is the same constant on Linux and BSD but AF_INET6
            # is different. As the client and server can be running on
            # different platforms we can not just set the socket family
            # to what comes in the wire.
            if family != socket.AF_INET:
                family = socket.AF_INET6
            dstport = int(dstport)
            outwrap = ssnet.connect_dst(family, dstip, dstport)
            handlers.append(Proxy(MuxWrapper(mux, channel), outwrap))
        mux.new_channel = new_channel

        dnshandlers = {}

        def dns_req(channel, data):
            debug2('Incoming DNS request channel=%d.' % channel)
            h = DnsProxy(mux, channel, data, to_nameserver)
            handlers.append(h)
            dnshandlers[channel] = h
        mux.got_dns_req = dns_req

        udphandlers = {}

        def udp_req(channel, cmd, data):
            debug2('Incoming UDP request channel=%d, cmd=%d' %
                   (channel, cmd))
            if cmd == ssnet.CMD_UDP_DATA:
                (dstip, dstport, data) = data.split(b(','), 2)
                dstport = int(dstport)
                debug2('is incoming UDP data. %r %d.' % (dstip, dstport))
                h = udphandlers[channel]
                h.send((dstip, dstport), data)
            elif cmd == ssnet.CMD_UDP_CLOSE:
                debug2('is incoming UDP close')
                h = udphandlers[channel]
                h.ok = False
                del mux.channels[channel]

        def udp_open(channel, data):
            debug2('Incoming UDP open.')
            family = int(data)
            mux.channels[channel] = lambda cmd, data: udp_req(channel, cmd,
                                                              data)
            if channel in udphandlers:
                raise Fatal('UDP connection channel %d already open' %
                            channel)
            else:
                h = UdpProxy(mux, channel, family)
                handlers.append(h)
                udphandlers[channel] = h
        mux.got_udp_open = udp_open

        while mux.ok:
            if hw.pid:
                assert(hw.pid > 0)
                (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG)
                if rpid:
                    raise Fatal(
                        'hostwatch exited unexpectedly: code 0x%04x' % rv)

            ssnet.runonce(handlers, mux)
            if latency_control:
                mux.check_fullness()

            if dnshandlers:
                now = time.time()
                remove = []
                for channel, h in dnshandlers.items():
                    if h.timeout < now or not h.ok:
                        debug3('expiring dnsreqs channel=%d' % channel)
                        remove.append(channel)
                        h.ok = False
                for channel in remove:
                    del dnshandlers[channel]
            if udphandlers:
                remove = []
                for channel, h in udphandlers.items():
                    if not h.ok:
                        debug3('expiring UDP channel=%d' % channel)
                        remove.append(channel)
                        h.ok = False
                for channel in remove:
                    del udphandlers[channel]

    except Fatal as e:
        log('fatal: %s' % e)
        sys.exit(99)
Exemple #22
0
def main(method_name, syslog):
    helpers.logprefix = 'fw: '
    stdin, stdout = setup_daemon()
    hostmap = {}
    debug1('Starting firewall with Python version %s' %
           platform.python_version())

    if method_name == "auto":
        method = get_auto_method()
    else:
        method = get_method(method_name)

    if syslog:
        ssyslog.start_syslog()
        ssyslog.stderr_to_syslog()

    if not method.is_supported():
        raise Fatal("The %s method is not supported on this machine. "
                    "Check that the appropriate programs are in your "
                    "PATH." % method_name)

    debug1('ready method name %s.' % method.name)
    stdout.write('READY %s\n' % method.name)
    stdout.flush()

    # we wait until we get some input before creating the rules.  That way,
    # tshuttle can launch us as early as possible (and get sudo password
    # authentication as early in the startup process as possible).
    line = stdin.readline(128)
    if not line:
        return  # parent died; nothing to do

    subnets = []
    if line != 'ROUTES\n':
        raise Fatal('expected ROUTES but got %r' % line)
    while 1:
        line = stdin.readline(128)
        if not line:
            raise Fatal('expected route but got %r' % line)
        elif line.startswith("NSLIST\n"):
            break
        try:
            (family, width, exclude, ip, fport, lport) = \
                    line.strip().split(',', 5)
        except BaseException:
            raise Fatal('expected route or NSLIST but got %r' % line)
        subnets.append((int(family), int(width), bool(int(exclude)), ip,
                        int(fport), int(lport)))
    debug2('Got subnets: %r' % subnets)

    nslist = []
    if line != 'NSLIST\n':
        raise Fatal('expected NSLIST but got %r' % line)
    while 1:
        line = stdin.readline(128)
        if not line:
            raise Fatal('expected nslist but got %r' % line)
        elif line.startswith("PORTS "):
            break
        try:
            (family, ip) = line.strip().split(',', 1)
        except BaseException:
            raise Fatal('expected nslist or PORTS but got %r' % line)
        nslist.append((int(family), ip))
        debug2('Got partial nslist: %r' % nslist)
    debug2('Got nslist: %r' % nslist)

    if not line.startswith('PORTS '):
        raise Fatal('expected PORTS but got %r' % line)
    _, _, ports = line.partition(" ")
    ports = ports.split(",")
    if len(ports) != 4:
        raise Fatal('expected 4 ports but got %d' % len(ports))
    port_v6 = int(ports[0])
    port_v4 = int(ports[1])
    dnsport_v6 = int(ports[2])
    dnsport_v4 = int(ports[3])

    assert (port_v6 >= 0)
    assert (port_v6 <= 65535)
    assert (port_v4 >= 0)
    assert (port_v4 <= 65535)
    assert (dnsport_v6 >= 0)
    assert (dnsport_v6 <= 65535)
    assert (dnsport_v4 >= 0)
    assert (dnsport_v4 <= 65535)

    debug2('Got ports: %d,%d,%d,%d' %
           (port_v6, port_v4, dnsport_v6, dnsport_v4))

    line = stdin.readline(128)
    if not line:
        raise Fatal('expected GO but got %r' % line)
    elif not line.startswith("GO "):
        raise Fatal('expected GO but got %r' % line)

    _, _, args = line.partition(" ")
    udp, user = args.strip().split(" ", 1)
    udp = bool(int(udp))
    if user == '-':
        user = None
    debug2('Got udp: %r, user: %r' % (udp, user))

    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]

    try:
        debug1('setting up.')

        if subnets_v6 or nslist_v6:
            debug2('setting up IPv6.')
            method.setup_firewall(port_v6, dnsport_v6, nslist_v6,
                                  socket.AF_INET6, subnets_v6, udp, user)

        if subnets_v4 or nslist_v4:
            debug2('setting up IPv4.')
            method.setup_firewall(port_v4, dnsport_v4, nslist_v4,
                                  socket.AF_INET, subnets_v4, udp, user)

        stdout.write('STARTED\n')

        try:
            stdout.flush()
        except IOError:
            # the parent process died for some reason; he's surely been loud
            # enough, so no reason to report another error
            return

        # Now we wait until EOF or any other kind of exception.  We need
        # to stay running so that we don't need a *second* password
        # authentication at shutdown time - that cleanup is important!
        while 1:
            line = stdin.readline(128)
            if line.startswith('HOST '):
                (name, ip) = line[5:].strip().split(',', 1)
                hostmap[name] = ip
                debug2('setting up /etc/hosts.')
                rewrite_etc_hosts(hostmap, port_v6 or port_v4)
            elif line:
                if not method.firewall_command(line):
                    raise Fatal('expected command, got %r' % line)
            else:
                break
    finally:
        try:
            debug1('undoing changes.')
        except BaseException:
            debug2('An error occurred, ignoring it.')

        try:
            if subnets_v6 or nslist_v6:
                debug2('undoing IPv6 changes.')
                method.restore_firewall(port_v6, socket.AF_INET6, udp, user)
        except BaseException:
            try:
                debug1("Error trying to undo IPv6 firewall.")
                debug1(traceback.format_exc())
            except BaseException:
                debug2('An error occurred, ignoring it.')

        try:
            if subnets_v4 or nslist_v4:
                debug2('undoing IPv4 changes.')
                method.restore_firewall(port_v4, socket.AF_INET, udp, user)
        except BaseException:
            try:
                debug1("Error trying to undo IPv4 firewall.")
                debug1(traceback.format_exc())
            except BaseException:
                debug2('An error occurred, ignoring it.')

        try:
            # debug2() message printed in restore_etc_hosts() function.
            restore_etc_hosts(hostmap, port_v6 or port_v4)
        except BaseException:
            try:
                debug1("Error trying to undo /etc/hosts changes.")
                debug1(traceback.format_exc())
            except BaseException:
                debug2('An error occurred, ignoring it.')
Exemple #23
0
 def send_udp(self, sock, srcip, dstip, data):
     if srcip is not None:
         Fatal("Method %s send_udp does not support setting srcip to %r"
               % (self.name, srcip))
     sock.sendto(data, dstip)
Exemple #24
0
 def check(self):
     rv = self.p.poll()
     if rv:
         raise Fatal('%r returned %d' % (self.argv, rv))
Exemple #25
0
 def done(self):
     self.pfile.close()
     rv = self.p.wait()
     if rv:
         raise Fatal('cleanup: %r returned %d' % (self.argv, rv))
Exemple #26
0
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):

    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))
    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 tshuttle server started. However, the tshuttle 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 " \
                "tshuttle's server code. Try specifying the python " \
                "executable to user on the server by passing --python " \
                "to tshuttle."

        # 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 tshuttle."

        # 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 tshuttle 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 tshuttle 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/tshuttle/tshuttle/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 'tshuttle -v -r example.com 0/0' to " \
                "redirect all traffic through example.com, then try " \
                "'tshuttle -v -r example.com -x example.com 0/0' to " \
                "exclude redirecting the connection to example.com itself " \
                "(i.e., tshuttle's firewall rules may be breaking the " \
                "ssh connection that it previously established). " \
                "Alternatively, you may be able to use 'tshuttle -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()
Exemple #27
0
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):

    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 tshuttle proxy (version %s).' % __version__)
    helpers.logprefix = 'c : '

    fw = FirewallClient(method_name, sudo_pythonpath)

    # 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 tshuttle.")
        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 tshuttle is not using IPv6. Therefore DNS "
                  "traffic your system sends to the IPv6 DNS server won't "
                  "be redirected via tshuttle 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 tshuttle 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)

    # Add routes to avoid Cico VPN issue
    for subnet_info in subnets_include:
        cidr = subnet_info[1] + "/" + str(subnet_info[2])
        try:
            ssubprocess.run('sudo route -n add -net ' + cidr +
                            ' -interface en0',
                            check=True,
                            shell=True)
        except ssubprocess.CalledProcessError:
            print('Failed to add ' + cidr)

    # 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)
    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()
Exemple #28
0
def connect(ssh_cmd, rhostport, python, stderr, options):
    username, password, port, host = parse_hostport(rhostport)
    if username:
        rhost = "{}@{}".format(username, host)
    else:
        rhost = host

    z = zlib.compressobj(1)
    content = get_module_source('tshuttle.assembler')
    optdata = ''.join("%s=%r\n" % (k, v) for (k, v) in list(options.items()))
    optdata = optdata.encode("UTF8")
    content2 = (empackage(z, 'tshuttle') +
                empackage(z, 'tshuttle.cmdline_options', optdata) +
                empackage(z, 'tshuttle.helpers') +
                empackage(z, 'tshuttle.ssnet') +
                empackage(z, 'tshuttle.hostwatch') +
                empackage(z, 'tshuttle.server') +
                b"\n")

    # If the exec() program calls sys.exit(), it should exit python
    # and the sys.exit(98) call won't be reached (so we try to only
    # exit that way in the server). However, if the code that we
    # exec() simply returns from main, then we will return from
    # exec(). If the server's python process dies, it should stop
    # executing and also won't reach sys.exit(98).
    #
    # So, we shouldn't reach sys.exit(98) and we certainly shouldn't
    # reach it immediately after trying to start the server.
    pyscript = r"""
                import sys, os;
                verbosity=%d;
                sys.stdin = os.fdopen(0, "rb");
                exec(compile(sys.stdin.read(%d), "assembler.py", "exec"));
                sys.exit(98);
                """ % (helpers.verbose or 0, len(content))
    pyscript = re.sub(r'\s+', ' ', pyscript.strip())

    if not rhost:
        # ignore the --python argument when running locally; we already know
        # which python version works.
        argv = [sys.executable, '-c', pyscript]
    else:
        if ssh_cmd:
            sshl = shlex.split(ssh_cmd)
        else:
            sshl = ['ssh']
        if port is not None:
            portl = ["-p", str(port)]
        else:
            portl = []
        if python:
            pycmd = "'%s' -c '%s'" % (python, pyscript)
        else:
            # By default, we run the following code in a shell.
            # However, with restricted shells and other unusual
            # situations, there can be trouble. See the RESTRICTED
            # SHELL section in "man bash" for more information. The
            # code makes many assumptions:
            #
            # (1) That /bin/sh exists and that we can call it.
            # Restricted shells often do *not* allow you to run
            # programs specified with an absolute path like /bin/sh.
            # Either way, if there is trouble with this, it should
            # return error code 127.
            #
            # (2) python3 or python exists in the PATH and is
            # executable. If they aren't, then exec wont work (see (4)
            # below).
            #
            # (3) In /bin/sh, that we can redirect stderr in order to
            # hide the version that "python3 -V" might print (some
            # restricted shells don't allow redirection, see
            # RESTRICTED SHELL section in 'man bash'). However, if we
            # are in a restricted shell, we'd likely have trouble with
            # assumption (1) above.
            #
            # (4) The 'exec' command should work except if we failed
            # to exec python because it doesn't exist or isn't
            # executable OR if exec isn't allowed (some restricted
            # shells don't allow exec). If the exec succeeded, it will
            # not return and not get to the "exit 97" command. If exec
            # does return, we exit with code 97.
            #
            # Specifying the exact python program to run with --python
            # avoids many of the issues above. However, if
            # you have a restricted shell on remote, you may only be
            # able to run python if it is in your PATH (and you can't
            # run programs specified with an absolute path). In that
            # case, tshuttle might not work at all since it is not
            # possible to run python on the remote machine---even if
            # it is present.
            pycmd = ("P=python3; $P -V 2>%s || P=python; "
                     "exec \"$P\" -c %s; exit 97") % \
                     (os.devnull, quote(pyscript))
            pycmd = ("/bin/sh -c {}".format(quote(pycmd)))

        if password is not None:
            os.environ['SSHPASS'] = str(password)
            argv = (["sshpass", "-e"] + sshl +
                    portl +
                    [rhost, pycmd])

        else:
            argv = (sshl +
                    portl +
                    [rhost, pycmd])

    # Our which() function searches for programs in get_path()
    # directories (which include PATH). This step isn't strictly
    # necessary if ssh is already in the user's PATH, but it makes the
    # error message friendlier if the user incorrectly passes in a
    # custom ssh command that we cannot find.
    abs_path = which(argv[0])
    if abs_path is None:
        raise Fatal("Failed to find '%s' in path %s" % (argv[0], get_path()))
    argv[0] = abs_path

    (s1, s2) = socket.socketpair()

    def setup():
        # runs in the child process
        s2.close()
    s1a, s1b = os.dup(s1.fileno()), os.dup(s1.fileno())
    s1.close()

    debug2('executing: %r' % argv)
    p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup,
                          close_fds=True, stderr=stderr)
    os.close(s1a)
    os.close(s1b)
    s2.sendall(content)
    s2.sendall(content2)
    return p, s2