summaryrefslogtreecommitdiffstats
path: root/sshuttle/options.py
diff options
context:
space:
mode:
authorScott Kuhl <kuhl@mtu.edu>2020-10-16 18:29:16 -0400
committerScott Kuhl <kuhl@mtu.edu>2020-10-16 18:29:16 -0400
commit036c49e41262cb22537f4380c39b2fc1cf41ad00 (patch)
tree0db363013ebf4239952d175136ccd729c4808595 /sshuttle/options.py
parentc1cc3911df2881d21ddcd52b2d7b80e3e1acc47f (diff)
When subnets and excludes are specified with hostnames, use all IPs.
The list of subnets to route over VPN and the list of subnets to exclude are parsed in option.py parse_subnetport(). Hostnames or IP addresses are supported. If a hostname was provided, only the first IP address was considered. This could result in some traffic not traversing the VPN that the user might expect should traverse it from the arguments passed to sshuttle. This patch makes the function handle all of the IPs if a hostname is provided. If a user provides a hostname with a CIDR mask, problems can occur and we warn the user about the issue. If the user includes a hostname with both an IPv4 and an IPv6 address, and the underlying method doesn't support IPv6, then this patch will cause sshuttle to fail. I plan to provide a future patch where failure won't occur if the only place IPv6 addresses appear is in the exclude list. In that case it should be safe to ignore the IPv6 address. This patch also changes parse_ipport() which is used by the --to-ns option. If the user provides a hostname here, we just use the first IP from the hostname and warn the user that only one is being used.
Diffstat (limited to 'sshuttle/options.py')
-rw-r--r--sshuttle/options.py75
1 files changed, 62 insertions, 13 deletions
diff --git a/sshuttle/options.py b/sshuttle/options.py
index 12ce55d..e104c79 100644
--- a/sshuttle/options.py
+++ b/sshuttle/options.py
@@ -28,7 +28,14 @@ def parse_subnetport_file(s):
# 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:
@@ -38,19 +45,57 @@ def parse_subnetport(s):
if not m:
raise Fatal('%r is not a valid address/mask:port format' % s)
- addr, width, fport, lport = m.groups()
+ # 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(addr, 0, 0, socket.SOCK_STREAM)
+ addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM)
except socket.gaierror:
- raise Fatal('Unable to resolve address: %s' % addr)
+ raise Fatal('Unable to resolve address: %s' % host)
- family, _, _, _, addr = min(addrinfo)
- max_width = 32 if family == socket.AF_INET else 128
- width = int(width or max_width)
- if not 0 <= width <= max_width:
- raise Fatal('width %d is not between 0 and %d' % (width, max_width))
+ # 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))
- return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
+ 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
@@ -69,16 +114,20 @@ def parse_ipport(s):
if not m:
raise Fatal('%r is not a valid IP:port format' % s)
- ip, port = m.groups()
- ip = ip or '0.0.0.0'
+ host, port = m.groups()
+ host = host or '0.0.0.0'
port = int(port or 0)
try:
- addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
+ addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
except socket.gaierror:
- raise Fatal('%r is not a valid IP:port format' % s)
+ 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]