mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-05-15 13:46:28 +02:00
Even when --tmark was used, the iptables code always used '1' for the mark. This patch corrects the problem. Previously, it wasn't clear if the tmark should be supplied in hexadecimal or as an integer. This makes it use hexadecimal, checks that the input is hexadecimal, and updates the associated documentation. This patch also makes --ttl information get passed to the firewall in a way that matches how other information gets passed. The ttl and tmark information are passed next to each other in many places and this patch also makes the order consistent.
454 lines
12 KiB
Python
454 lines
12 KiB
Python
import re
|
|
import socket
|
|
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal
|
|
|
|
from sshuttle import __version__
|
|
|
|
|
|
# Subnet file, supporting empty lines and hash-started comment lines
|
|
def parse_subnetport_file(s):
|
|
try:
|
|
handle = open(s, 'r')
|
|
except OSError:
|
|
raise Fatal('Unable to open subnet file: %s' % s)
|
|
|
|
raw_config_lines = handle.readlines()
|
|
subnets = []
|
|
for _, line in enumerate(raw_config_lines):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
if line[0] == '#':
|
|
continue
|
|
subnets.append(parse_subnetport(line))
|
|
|
|
return subnets
|
|
|
|
|
|
# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
|
|
# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
|
|
# example.com:123 or just example.com
|
|
#
|
|
# In addition, the port number can be specified as a range:
|
|
# 1.2.3.4:8000-8080.
|
|
#
|
|
# Can return multiple matches if the domain name used in the request
|
|
# has multiple IP addresses.
|
|
def parse_subnetport(s):
|
|
|
|
if s.count(':') > 1:
|
|
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
|
|
else:
|
|
rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'
|
|
|
|
m = re.match(rx, s)
|
|
if not m:
|
|
raise Fatal('%r is not a valid address/mask:port format' % s)
|
|
|
|
# Ports range from fport to lport. If only one port is specified,
|
|
# fport is defined and lport is None.
|
|
#
|
|
# cidr is the mask defined with the slash notation
|
|
host, cidr, fport, lport = m.groups()
|
|
try:
|
|
addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM)
|
|
except socket.gaierror:
|
|
raise Fatal('Unable to resolve address: %s' % host)
|
|
|
|
# If the address is a domain with multiple IPs and a mask is also
|
|
# provided, proceed cautiously:
|
|
if cidr is not None:
|
|
addr_v6 = [a for a in addrinfo if a[0] == socket.AF_INET6]
|
|
addr_v4 = [a for a in addrinfo if a[0] == socket.AF_INET]
|
|
|
|
# Refuse to proceed if IPv4 and IPv6 addresses are present:
|
|
if len(addr_v6) > 0 and len(addr_v4) > 0:
|
|
raise Fatal("%s has IPv4 and IPv6 addresses, so the mask "
|
|
"of /%s is not supported. Specify the IP "
|
|
"addresses directly if you wish to specify "
|
|
"a mask." % (host, cidr))
|
|
|
|
# Warn if a domain has multiple IPs of the same type (IPv4 vs
|
|
# IPv6) and the mask is applied to all of the IPs.
|
|
if len(addr_v4) > 1 or len(addr_v6) > 1:
|
|
print("WARNING: %s has multiple IP addresses. The "
|
|
"mask of /%s is applied to all of the addresses."
|
|
% (host, cidr))
|
|
|
|
rv = []
|
|
for a in addrinfo:
|
|
family, _, _, _, addr = a
|
|
|
|
# Largest possible slash value we can use with this IP:
|
|
max_cidr = 32 if family == socket.AF_INET else 128
|
|
|
|
if cidr is None: # if no mask, use largest mask
|
|
cidr_to_use = max_cidr
|
|
else: # verify user-provided mask is appropriate
|
|
cidr_to_use = int(cidr)
|
|
if not 0 <= cidr_to_use <= max_cidr:
|
|
raise Fatal('Slash in CIDR notation (/%d) is '
|
|
'not between 0 and %d'
|
|
% (cidr_to_use, max_cidr))
|
|
|
|
rv.append((family, addr[0], cidr_to_use,
|
|
int(fport or 0), int(lport or fport or 0)))
|
|
|
|
return rv
|
|
|
|
|
|
# 1.2.3.4:567 or just 1.2.3.4 or just 567
|
|
# [1:2::3]:456 or [1:2::3] or just [::]:567
|
|
# example.com:123 or just example.com
|
|
def parse_ipport(s):
|
|
s = str(s)
|
|
if s.isdigit():
|
|
rx = r'()(\d+)$'
|
|
elif ']' in s:
|
|
rx = r'(?:\[([^]]+)])(?::(\d+))?$'
|
|
else:
|
|
rx = r'([\w\.\-]+)(?::(\d+))?$'
|
|
|
|
m = re.match(rx, s)
|
|
if not m:
|
|
raise Fatal('%r is not a valid IP:port format' % s)
|
|
|
|
host, port = m.groups()
|
|
host = host or '0.0.0.0'
|
|
port = int(port or 0)
|
|
|
|
try:
|
|
addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
|
|
except socket.gaierror:
|
|
raise Fatal('Unable to resolve address: %s' % host)
|
|
|
|
if len(addrinfo) > 1:
|
|
print("WARNING: Host %s has more than one IP, only using one of them."
|
|
% host)
|
|
|
|
family, _, _, _, addr = min(addrinfo)
|
|
# Note: addr contains (ip, port)
|
|
return (family,) + addr[:2]
|
|
|
|
|
|
def parse_list(lst):
|
|
return re.split(r'[\s,]+', lst.strip()) if lst else []
|
|
|
|
|
|
class Concat(Action):
|
|
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
|
if nargs is not None:
|
|
raise ValueError("nargs not supported")
|
|
super(Concat, self).__init__(option_strings, dest, **kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
curr_value = getattr(namespace, self.dest, None) or []
|
|
setattr(namespace, self.dest, curr_value + values)
|
|
|
|
|
|
# Override one function in the ArgumentParser so that we can have
|
|
# better control for how we parse files containing arguments. We
|
|
# expect one argument per line, but strip whitespace/quotes from the
|
|
# beginning/end of the lines.
|
|
class MyArgumentParser(ArgumentParser):
|
|
def convert_arg_line_to_args(self, arg_line):
|
|
# Ignore comments
|
|
if arg_line.startswith("#"):
|
|
return []
|
|
|
|
# strip whitespace at beginning and end of line
|
|
arg_line = arg_line.strip()
|
|
|
|
# When copying parameters from the command line to a file,
|
|
# some users might copy the quotes they used on the command
|
|
# line into the config file. We ignore these if the line
|
|
# starts and ends with the same quote.
|
|
if arg_line.startswith("'") and arg_line.endswith("'") or \
|
|
arg_line.startswith('"') and arg_line.endswith('"'):
|
|
arg_line = arg_line[1:-1]
|
|
|
|
return [arg_line]
|
|
|
|
|
|
parser = MyArgumentParser(
|
|
prog="sshuttle",
|
|
usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
|
|
fromfile_prefix_chars="@"
|
|
)
|
|
parser.add_argument(
|
|
"subnets",
|
|
metavar="IP/MASK[:PORT[-PORT]]...",
|
|
nargs="*",
|
|
type=parse_subnetport,
|
|
help="""
|
|
capture and forward traffic to these subnets (whitespace separated)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-l", "--listen",
|
|
metavar="[IP:]PORT",
|
|
help="""
|
|
transproxy to this ip address and port number
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-H", "--auto-hosts",
|
|
action="store_true",
|
|
help="""
|
|
continuously scan for remote hostnames and update local /etc/hosts as
|
|
they are found
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-N", "--auto-nets",
|
|
action="store_true",
|
|
help="""
|
|
automatically determine subnets to route
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--dns",
|
|
action="store_true",
|
|
help="""
|
|
capture local DNS requests and forward to the remote DNS server
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--ns-hosts",
|
|
metavar="IP[,IP]",
|
|
default=[],
|
|
type=parse_list,
|
|
help="""
|
|
capture and forward DNS requests made to the following servers
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--to-ns",
|
|
metavar="IP[:PORT]",
|
|
type=parse_ipport,
|
|
help="""
|
|
the DNS server to forward requests to; defaults to servers in
|
|
/etc/resolv.conf on remote side if not given.
|
|
"""
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--method",
|
|
choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
|
|
metavar="TYPE",
|
|
default="auto",
|
|
help="""
|
|
%(choices)s
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--python",
|
|
metavar="PATH",
|
|
help="""
|
|
path to python interpreter on the remote server
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-r", "--remote",
|
|
metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
|
|
help="""
|
|
ssh hostname (and optional username and password) of remote %(prog)s server
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-x", "--exclude",
|
|
metavar="IP/MASK[:PORT[-PORT]]",
|
|
action="append",
|
|
default=[],
|
|
type=parse_subnetport,
|
|
help="""
|
|
exclude this subnet (can be used more than once)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-X", "--exclude-from",
|
|
metavar="PATH",
|
|
action=Concat,
|
|
dest="exclude",
|
|
type=parse_subnetport_file,
|
|
help="""
|
|
exclude the subnets in a file (whitespace separated)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-v", "--verbose",
|
|
action="count",
|
|
default=0,
|
|
help="""
|
|
increase debug message verbosity
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-V", "--version",
|
|
action="version",
|
|
version=__version__,
|
|
help="""
|
|
print the %(prog)s version number and exit
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-e", "--ssh-cmd",
|
|
metavar="CMD",
|
|
default="ssh",
|
|
help="""
|
|
the command to use to connect to the remote [%(default)s]
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--seed-hosts",
|
|
metavar="HOSTNAME[,HOSTNAME]",
|
|
default=[],
|
|
help="""
|
|
comma-separated list of hostnames for initial scan (may be used with
|
|
or without --auto-hosts)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--no-latency-control",
|
|
action="store_false",
|
|
dest="latency_control",
|
|
help="""
|
|
sacrifice latency to improve bandwidth benchmarks
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--latency-buffer-size",
|
|
metavar="SIZE",
|
|
type=int,
|
|
default=32768,
|
|
dest="latency_buffer_size",
|
|
help="""
|
|
size of latency control buffer
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--wrap",
|
|
metavar="NUM",
|
|
type=int,
|
|
help="""
|
|
restart counting channel numbers after this number (for testing)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--disable-ipv6",
|
|
action="store_true",
|
|
help="""
|
|
disable IPv6 support
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-D", "--daemon",
|
|
action="store_true",
|
|
help="""
|
|
run in the background as a daemon
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-s", "--subnets",
|
|
metavar="PATH",
|
|
action=Concat,
|
|
dest="subnets_file",
|
|
default=[],
|
|
type=parse_subnetport_file,
|
|
help="""
|
|
file where the subnets are stored, instead of on the command line
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--syslog",
|
|
action="store_true",
|
|
help="""
|
|
send log messages to syslog (default if you use --daemon)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--pidfile",
|
|
metavar="PATH",
|
|
default="./sshuttle.pid",
|
|
help="""
|
|
pidfile name (only if using --daemon) [%(default)s]
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--user",
|
|
help="""
|
|
apply all the rules only to this linux user
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--firewall",
|
|
action="store_true",
|
|
help="""
|
|
(internal use only)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--ttl",
|
|
type=int,
|
|
default=63,
|
|
help="""
|
|
Override the TTL for the connections made by the sshuttle server.
|
|
Default is 63.
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--hostwatch",
|
|
action="store_true",
|
|
help="""
|
|
(internal use only)
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--sudoers",
|
|
action="store_true",
|
|
help="""
|
|
Add sshuttle to the sudoers for this user
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--sudoers-no-modify",
|
|
action="store_true",
|
|
help="""
|
|
Prints the sudoers config to STDOUT and DOES NOT modify anything.
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--sudoers-user",
|
|
default="",
|
|
help="""
|
|
Set the user name or group with %%group_name for passwordless operation.
|
|
Default is the current user.set ALL for all users. Only works with
|
|
--sudoers or --sudoers-no-modify option.
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--sudoers-filename",
|
|
default="sshuttle_auto",
|
|
help="""
|
|
Set the file name for the sudoers.d file to be added. Default is
|
|
"sshuttle_auto". Only works with --sudoers or --sudoers-no-modify option.
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--no-sudo-pythonpath",
|
|
action="store_false",
|
|
dest="sudo_pythonpath",
|
|
help="""
|
|
do not set PYTHONPATH when invoking sudo
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"-t", "--tmark",
|
|
metavar="[MARK]",
|
|
default="0x01",
|
|
help="""
|
|
tproxy optional traffic mark with provided MARK value in
|
|
hexadecimal (default '0x01')
|
|
"""
|
|
)
|