sshuttle/sshuttle/options.py
Scott Kuhl a7df12cd68 Fix --tmark option
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.
2021-05-27 21:48:43 -04:00

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')
"""
)