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 _check_nmb(hostname, is_workgroup, is_master): return global _nmb_ok if not _nmb_ok: return debug2(' > n%d%d: %s' % (is_workgroup, is_master, hostname)) argv = ['nmblookup'] + ['-M'] * is_master + ['--', hostname] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, env=get_env) lines = p.stdout.readlines() rv = p.wait() except OSError: _, e = sys.exc_info()[:2] log('%r failed: %r' % (argv, e)) _nmb_ok = False return if rv: log('%r returned %d' % (argv, rv)) return for line in lines: m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line) if m: g = m.groups() (ip, name) = (g[0], g[1].lower()) debug3('< %s -> %s' % (name, ip)) if is_workgroup: _enqueue(_check_smb, ip) else: found_host(name, ip) check_host(name)
def try_send(self): if self.tries >= 3: return self.tries += 1 if self.to_nameserver is None: _, peer = resolvconf_random_nameserver(False) port = 53 else: peer = self.to_ns_peer port = int(self.to_ns_port) family, sockaddr = self._addrinfo(peer, port) sock = socket.socket(family, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 63) sock.connect(sockaddr) self.peers[sock] = peer debug2('DNS: sending to %r:%d (try %d)' % (peer, port, self.tries)) try: sock.send(self.request) self.socks.append(sock) except socket.error: _, e = sys.exc_info()[:2] if e.args[0] in ssnet.NET_ERRS: # might have been spurious; try again. # Note: these errors sometimes are reported by recv(), # and sometimes by send(). We have to catch both. debug2('DNS send to %r: %s' % (peer, e)) self.try_send() return else: log('DNS send to %r: %s' % (peer, e)) return
def send(self, dstip, data): debug2('UDP: sending to %r port %d' % dstip) try: self.sock.sendto(data, dstip) except socket.error: _, e = sys.exc_info()[:2] log('UDP send to %r port %d: %s' % (dstip[0], dstip[1], e)) return
def _check_smb(hostname): return global _smb_ok if not _smb_ok: return debug2(' > smb: %s' % hostname) argv = ['smbclient', '-U', '%', '-L', hostname] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, env=get_env()) lines = p.stdout.readlines() p.wait() except OSError: _, e = sys.exc_info()[:2] log('%r failed: %r' % (argv, e)) _smb_ok = False return lines.reverse() # junk at top while lines: line = lines.pop().strip() if re.match(r'Server\s+', line): break # server list section: # Server Comment # ------ ------- while lines: line = lines.pop().strip() if not line or re.match(r'-+\s+-+', line): continue if re.match(r'Workgroup\s+Master', line): break words = line.split() hostname = words[0].lower() debug3('< %s' % hostname) check_host(hostname) # workgroup list section: # Workgroup Master # --------- ------ while lines: line = lines.pop().strip() if re.match(r'-+\s+', line): continue if not line: break words = line.split() (workgroup, hostname) = (words[0].lower(), words[1].lower()) debug3('< group(%s) -> %s' % (workgroup, hostname)) check_host(hostname) check_workgroup(workgroup) if lines: assert(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)
def callback(self, sock): log('--no callback defined-- %r' % self) (r, _, _) = select.select(self.socks, [], [], 0) for s in r: v = s.recv(4096) if not v: log('--closed-- %r' % self) self.socks = [] self.ok = False
def callback(self, sock): try: data, peer = sock.recvfrom(4096) except socket.error: _, e = sys.exc_info()[:2] log('UDP recv from %r port %d: %s' % (peer[0], peer[1], e)) return debug2('UDP response: %d bytes' % len(data)) hdr = b("%s,%r," % (peer[0], peer[1])) self.mux.send(self.chan, ssnet.CMD_UDP_DATA, hdr + data)
def list_routes(): if which('ip'): routes = _list_routes(['ip', 'route'], _route_iproute) elif which('netstat'): routes = _list_routes(['netstat', '-rn'], _route_netstat) else: log('WARNING: Neither "ip" nor "netstat" were found on the server. ' '--auto-nets feature will not work.') routes = [] for (family, ip, width) in routes: if not ip.startswith('0.') and not ip.startswith('127.'): yield (family, ip, width)
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
def ipt_ttl(family, *args): global _no_ttl_module if not _no_ttl_module: # we avoid infinite loops by generating server-side connections # with ttl 63. This makes the client side not recapture those # connections, in case client == server. try: argsplus = list(args) ipt(family, *argsplus) except Fatal: ipt(family, *args) # we only get here if the non-ttl attempt succeeds log('WARNING: your iptables is missing ' 'the ttl module.') _no_ttl_module = True else: ipt(family, *args)
def _check_netstat(): debug2(' > netstat') argv = ['netstat', '-n'] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null, env=get_env()) content = p.stdout.read().decode("ASCII") p.wait() except OSError: _, e = sys.exc_info()[:2] log('%r failed: %r' % (argv, e)) return for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content): debug3('< %s' % ip) check_host(ip)
def got_packet(self, channel, cmd, data): debug2('< channel=%d cmd=%s len=%d' % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data))) if cmd == CMD_PING: self.send(0, CMD_PONG, data) elif cmd == CMD_PONG: debug2('received PING response') self.too_full = False self.fullness = 0 elif cmd == CMD_EXIT: self.ok = False elif cmd == CMD_TCP_CONNECT: assert (not self.channels.get(channel)) if self.new_channel: self.new_channel(channel, data) elif cmd == CMD_DNS_REQ: assert (not self.channels.get(channel)) if self.got_dns_req: self.got_dns_req(channel, data) elif cmd == CMD_UDP_OPEN: assert (not self.channels.get(channel)) if self.got_udp_open: self.got_udp_open(channel, data) elif cmd == CMD_ROUTES: if self.got_routes: self.got_routes(data) else: raise Exception('got CMD_ROUTES without got_routes?') elif cmd == CMD_HOST_REQ: if self.got_host_req: self.got_host_req(data) else: raise Exception('got CMD_HOST_REQ without got_host_req?') elif cmd == CMD_HOST_LIST: if self.got_host_list: self.got_host_list(data) else: raise Exception('got CMD_HOST_LIST without got_host_list?') else: callback = self.channels.get(channel) if not callback: log('warning: closed channel %d got cmd=%s len=%d' % (channel, cmd_to_name.get(cmd, hex(cmd)), len(data))) else: callback(cmd, data)
def save_config(content, file_name): process = Popen([ '/usr/bin/sudo', spawn.find_executable('sudoers-add'), file_name, ], stdout=PIPE, stdin=PIPE) process.stdin.write(content.encode()) streamdata = process.communicate()[0] returncode = process.returncode if returncode: log('Failed updating sudoers file.') debug1(streamdata) exit(returncode) else: log('Success, sudoers file update.') exit(0)
def _list_routes(argv, extract_route): # FIXME: IPv4 only p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env()) routes = [] for line in p.stdout: if not line.strip(): continue ipw, mask = extract_route(line.decode("ASCII")) if not ipw: continue width = min(ipw[1], mask) ip = ipw[0] & _shl(_shl(1, width) - 1, 32 - width) routes.append( (socket.AF_INET, socket.inet_ntoa(struct.pack('!I', ip)), width)) rv = p.wait() if rv != 0: log('WARNING: %r returned %d' % (argv, rv)) return routes
def start_hostwatch(seed_hosts, auto_hosts): s1, s2 = socket.socketpair() pid = os.fork() if not pid: # child rv = 99 try: try: s2.close() os.dup2(s1.fileno(), 1) os.dup2(s1.fileno(), 0) s1.close() rv = hostwatch.hw_main(seed_hosts, auto_hosts) or 0 except Exception: log('%s' % _exc_dump()) rv = 98 finally: os._exit(rv) s1.close() return pid, s2
def callback(self, sock): peer = self.peers[sock] try: data = sock.recv(4096) except socket.error: _, e = sys.exc_info()[:2] self.socks.remove(sock) del self.peers[sock] if e.args[0] in ssnet.NET_ERRS: # might have been spurious; try again. # Note: these errors sometimes are reported by recv(), # and sometimes by send(). We have to catch both. debug2('DNS recv from %r: %s' % (peer, e)) self.try_send() return else: log('DNS recv from %r: %s' % (peer, e)) return debug2('DNS response: %d bytes' % len(data)) self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data) self.ok = False
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)
def got_signal(signum, frame): log('exiting on signal %d' % signum) sys.exit(1)
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()
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()
def nonfatal(func, *args): try: func(*args) except Fatal as e: log('error: %s' % e)
from tshuttle.helpers import log, debug1, debug2, debug3, get_env POLL_TIME = 60 * 15 NETSTAT_POLL_TIME = 30 CACHEFILE = os.path.expanduser('~/.tshuttle.hosts') _nmb_ok = True _smb_ok = True hostnames = {} queue = {} try: null = open(os.devnull, 'wb') except IOError: _, e = sys.exc_info()[:2] log('warning: %s' % e) null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096) def _is_ip(s): return re.match(r'\d+\.\d+\.\d+\.\d+$', s) def write_host_cache(): tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid()) try: f = open(tmpname, 'wb') for name, ip in sorted(hostnames.items()): f.write(('%s,%s\n' % (name, ip)).encode("ASCII")) f.close() os.chmod(tmpname, 384) # 600 in octal, 'rw-------'