import sys import re import socket import helpers import options import client import server import firewall import hostwatch from helpers import log, Fatal # 1.2.3.4/5 or just 1.2.3.4 def parse_subnet4(s): m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s) if not m: raise Fatal('%r is not a valid IP subnet format' % s) (a, b, c, d, width) = m.groups() (a, b, c, d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0)) if width is None: width = 32 else: width = int(width) if a > 255 or b > 255 or c > 255 or d > 255: raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d)) if width > 32: raise Fatal('*/%d is greater than the maximum of 32' % width) return(socket.AF_INET, '%d.%d.%d.%d' % (a, b, c, d), width) # 1:2::3/64 or just 1:2::3 def parse_subnet6(s): m = re.match(r'(?:([a-fA-F\d:]+))?(?:/(\d+))?$', s) if not m: raise Fatal('%r is not a valid IP subnet format' % s) (net, width) = m.groups() if width is None: width = 128 else: width = int(width) if width > 128: raise Fatal('*/%d is greater than the maximum of 128' % width) return(socket.AF_INET6, net, width) # Subnet file, supporting empty lines and hash-started comment lines def parse_subnet_file(s): try: handle = open(s, 'r') except OSError: raise Fatal('Unable to open subnet file: %s' % s) raw_config_lines = handle.readlines() config_lines = [] for line_no, line in enumerate(raw_config_lines): line = line.strip() if len(line) == 0: continue if line[0] == '#': continue config_lines.append(line) return config_lines # list of: # 1.2.3.4/5 or just 1.2.3.4 # 1:2::3/64 or just 1:2::3 def parse_subnets(subnets_str): subnets = [] for s in subnets_str: if ':' in s: subnet = parse_subnet6(s) else: subnet = parse_subnet4(s) subnets.append(subnet) return subnets # 1.2.3.4:567 or just 1.2.3.4 or just 567 def parse_ipport4(s): s = str(s) m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s) if not m: raise Fatal('%r is not a valid IP:port format' % s) (a, b, c, d, port) = m.groups() (a, b, c, d, port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0), int(port or 0)) if a > 255 or b > 255 or c > 255 or d > 255: raise Fatal('%d.%d.%d.%d has numbers > 255' % (a, b, c, d)) if port > 65535: raise Fatal('*:%d is greater than the maximum of 65535' % port) if a is None: a = b = c = d = 0 return ('%d.%d.%d.%d' % (a, b, c, d), port) # [1:2::3]:456 or [1:2::3] or 456 def parse_ipport6(s): s = str(s) m = re.match(r'(?:\[([^]]*)])?(?::)?(?:(\d+))?$', s) if not m: raise Fatal('%s is not a valid IP:port format' % s) (ip, port) = m.groups() (ip, port) = (ip or '::', int(port or 0)) return (ip, port) optspec = """ sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] sshuttle --server sshuttle --firewall sshuttle --hostwatch -- l,listen= transproxy to this ip address and port number H,auto-hosts scan for remote hostnames and update local /etc/hosts N,auto-nets automatically determine subnets to route dns capture local DNS requests and forward to the remote DNS server method= auto, nat, tproxy, pf or ipfw python= path to python interpreter on the remote server r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) X,exclude-from= exclude the subnets in a file (whitespace separated) v,verbose increase debug message verbosity e,ssh-cmd= the command to use to connect to the remote [ssh] seed-hosts= with -H, use these hostnames for initial scan (comma-separated) no-latency-control sacrifice latency to improve bandwidth benchmarks wrap= restart counting channel numbers after this number (for testing) D,daemon run in the background as a daemon s,subnets= file where the subnets are stored, instead of on the command line syslog send log messages to syslog (default if you use --daemon) pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] server (internal use only) firewall (internal use only) hostwatch (internal use only) """ o = options.Options(optspec) (opt, flags, extra) = o.parse(sys.argv[2:]) if opt.daemon: opt.syslog = 1 if opt.wrap: import ssnet ssnet.MAX_CHANNEL = int(opt.wrap) helpers.verbose = opt.verbose try: if opt.server: if len(extra) != 0: o.fatal('no arguments expected') server.latency_control = opt.latency_control sys.exit(server.main()) elif opt.firewall: if len(extra) != 6: o.fatal('exactly six arguments expected') sys.exit(firewall.main(int(extra[0]), int(extra[1]), int(extra[2]), int(extra[3]), extra[4], int(extra[5]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: if len(extra) < 1 and not opt.auto_nets and not opt.subnets: o.fatal('at least one subnet, subnet file, or -N expected') includes = extra excludes = ['127.0.0.0/8'] for k, v in flags: if k in ('-x', '--exclude'): excludes.append(v) if k in ('-X', '--exclude-from'): excludes += open(v).read().split() remotename = opt.remote if remotename == '' or remotename == '-': remotename = None if opt.seed_hosts and not opt.auto_hosts: o.fatal('--seed-hosts only works if you also use -H') if opt.seed_hosts: sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip()) elif opt.auto_hosts: sh = [] else: sh = None if opt.subnets: includes = parse_subnet_file(opt.subnets) if not opt.method: method = "auto" elif opt.method in ["auto", "nat", "tproxy", "ipfw", "pf"]: method = opt.method else: o.fatal("method %s not supported" % opt.method) if not opt.listen: if opt.method == "tproxy": ipport_v6 = parse_ipport6('[::1]:0') else: ipport_v6 = None ipport_v4 = parse_ipport4('127.0.0.1:0') else: ipport_v6 = None ipport_v4 = None list = opt.listen.split(",") for ip in list: if '[' in ip and ']' in ip and opt.method == "tproxy": ipport_v6 = parse_ipport6(ip) else: ipport_v4 = parse_ipport4(ip) return_code = client.main(ipport_v6, ipport_v4, opt.ssh_cmd, remotename, opt.python, opt.latency_control, opt.dns, method, sh, opt.auto_nets, parse_subnets(includes), parse_subnets(excludes), opt.syslog, opt.daemon, opt.pidfile) if return_code == 0: log('Normal exit code, exiting...') else: log('Abnormal exit code detected, failing...' % return_code) sys.exit(return_code) except Fatal, e: log('fatal: %s\n' % e) sys.exit(99) except KeyboardInterrupt: log('\n') log('Keyboard interrupt: exiting.\n') sys.exit(1)