Skip to content

Commit c3016f2

Browse files
authored
Merge pull request sshuttle#541 from skuhl/use-all-ips
When subnets and excludes are specified with hostnames, use all IPs.
2 parents a266e7a + b7a29ac commit c3016f2

File tree

5 files changed

+212
-61
lines changed

5 files changed

+212
-61
lines changed

sshuttle/client.py

Lines changed: 120 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -586,70 +586,143 @@ def main(listenip_v6, listenip_v4,
586586

587587
fw = FirewallClient(method_name, sudo_pythonpath)
588588

589-
# Get family specific subnet lists
589+
# If --dns is used, store the IP addresses that the client
590+
# normally uses for DNS lookups in nslist. The firewall needs to
591+
# redirect packets outgoing to this server to the remote host
592+
# instead.
590593
if dns:
591594
nslist += resolvconf_nameservers()
592595
if to_nameserver is not None:
593596
to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
594597
else:
595598
# option doesn't make sense if we aren't proxying dns
599+
if to_nameserver and len(to_nameserver) > 0:
600+
print("WARNING: --to-ns option is ignored because --dns was not "
601+
"used.")
596602
to_nameserver = None
597603

598-
subnets = subnets_include + subnets_exclude # we don't care here
599-
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6]
600-
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
601-
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET]
604+
# Get family specific subnet lists. Also, the user may not specify
605+
# any subnets if they use --auto-nets. In this case, our subnets
606+
# list will be empty and the forwarded subnets will be determined
607+
# later by the server.
608+
subnets_v4 = [i for i in subnets_include if i[0] == socket.AF_INET]
609+
subnets_v6 = [i for i in subnets_include if i[0] == socket.AF_INET6]
602610
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
611+
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
603612

604-
# Check features available
613+
# Get available features from the firewall method
605614
avail = fw.method.get_supported_features()
615+
616+
# A feature is "required" if the user supplies us parameters which
617+
# implies that the feature is needed.
606618
required = Features()
607619

620+
# Select the default addresses to bind to / listen to.
621+
622+
# Assume IPv4 is always available and should always be enabled. If
623+
# a method doesn't provide IPv4 support or if we wish to run
624+
# ipv6-only, changes to this code are required.
625+
assert avail.ipv4
626+
required.ipv4 = True
627+
628+
# listenip_v4 contains user specified value or it is set to "auto".
629+
if listenip_v4 == "auto":
630+
listenip_v4 = ('127.0.0.1', 0)
631+
632+
# listenip_v6 is...
633+
# None when IPv6 is disabled.
634+
# "auto" when listen address is unspecified.
635+
# The user specified address if provided by user
636+
if listenip_v6 is None:
637+
debug1("IPv6 disabled by --disable-ipv6\n")
608638
if listenip_v6 == "auto":
609639
if avail.ipv6:
640+
debug1("IPv6 enabled: Using default IPv6 listen address ::1\n")
610641
listenip_v6 = ('::1', 0)
611642
else:
643+
debug1("IPv6 disabled since it isn't supported by method "
644+
"%s.\n" % fw.method.name)
612645
listenip_v6 = None
613646

647+
# Make final decision about enabling IPv6:
648+
required.ipv6 = False
649+
if listenip_v6:
650+
required.ipv6 = True
651+
652+
# If we get here, it is possible that listenip_v6 was user
653+
# specified but not supported by the current method.
654+
if required.ipv6 and not avail.ipv6:
655+
raise Fatal("An IPv6 listen address was supplied, but IPv6 is "
656+
"disabled at your request or is unsupported by the %s "
657+
"method." % fw.method.name)
658+
614659
if user is not None:
615660
if getpwnam is None:
616661
raise Fatal("Routing by user not available on this system.")
617662
try:
618663
user = getpwnam(user).pw_uid
619664
except KeyError:
620665
raise Fatal("User %s does not exist." % user)
666+
required.user = False if user is None else True
621667

622-
if fw.method.name != 'nat':
623-
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
624-
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
625-
else:
626-
required.ipv6 = None
627-
required.ipv4 = None
668+
if not required.ipv6 and len(subnets_v6) > 0:
669+
print("WARNING: IPv6 subnets were ignored because IPv6 is disabled "
670+
"in sshuttle.")
671+
subnets_v6 = []
672+
subnets_include = subnets_v4
628673

629-
required.udp = avail.udp
674+
required.udp = avail.udp # automatically enable UDP if it is available
630675
required.dns = len(nslist) > 0
631-
required.user = False if user is None else True
632676

633-
# if IPv6 not supported, ignore IPv6 DNS servers
677+
# Remove DNS servers using IPv6.
678+
if required.dns:
679+
if not required.ipv6 and len(nslist_v6) > 0:
680+
print("WARNING: Your system is configured to use an IPv6 DNS "
681+
"server but sshuttle is not using IPv6. Therefore DNS "
682+
"traffic your system sends to the IPv6 DNS server won't "
683+
"be redirected via sshuttle to the remote machine.")
684+
nslist_v6 = []
685+
nslist = nslist_v4
686+
687+
if len(nslist) == 0:
688+
raise Fatal("Can't redirect DNS traffic since IPv6 is not "
689+
"enabled in sshuttle and all of the system DNS "
690+
"servers are IPv6.")
691+
692+
# If we aren't using IPv6, we can safely ignore excluded IPv6 subnets.
634693
if not required.ipv6:
635-
nslist_v6 = []
636-
nslist = nslist_v4
637-
694+
orig_len = len(subnets_exclude)
695+
subnets_exclude = [i for i in subnets_exclude
696+
if i[0] == socket.AF_INET]
697+
if len(subnets_exclude) < orig_len:
698+
print("WARNING: Ignoring one or more excluded IPv6 subnets "
699+
"because IPv6 is not enabled.")
700+
701+
# This will print error messages if we required a feature that
702+
# isn't available by the current method.
638703
fw.method.assert_features(required)
639704

640-
if required.ipv6 and listenip_v6 is None:
641-
raise Fatal("IPv6 required but not listening.")
642-
643705
# display features enabled
644-
debug1("IPv6 enabled: %r\n" % required.ipv6)
645-
debug1("UDP enabled: %r\n" % required.udp)
646-
debug1("DNS enabled: %r\n" % required.dns)
647-
debug1("User enabled: %r\n" % required.user)
706+
def feature_status(label, enabled, available):
707+
msg = label + ": "
708+
if enabled:
709+
msg += "on"
710+
else:
711+
msg += "off "
712+
if available:
713+
msg += "(available)"
714+
else:
715+
msg += "(not available with %s method)" % fw.method.name
716+
debug1(msg + "\n")
648717

649-
# bind to required ports
650-
if listenip_v4 == "auto":
651-
listenip_v4 = ('127.0.0.1', 0)
718+
debug1("Method: %s\n" % fw.method.name)
719+
feature_status("IPv4", required.ipv4, avail.ipv4)
720+
feature_status("IPv6", required.ipv6, avail.ipv6)
721+
feature_status("UDP ", required.udp, avail.udp)
722+
feature_status("DNS ", required.dns, avail.dns)
723+
feature_status("User", required.user, avail.user)
652724

725+
# Exclude traffic destined to our listen addresses.
653726
if required.ipv4 and \
654727
not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
655728
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0))
@@ -658,6 +731,25 @@ def main(listenip_v6, listenip_v4,
658731
not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
659732
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
660733

734+
# We don't print the IP+port of where we are listening here
735+
# because we do that below when we have identified the ports to
736+
# listen on.
737+
debug1("Subnets to forward through remote host (type, IP, cidr mask "
738+
"width, startPort, endPort):\n")
739+
for i in subnets_include:
740+
print(" "+str(i))
741+
if auto_nets:
742+
debug1("NOTE: Additional subnets to forward may be added below by "
743+
"--auto-nets.\n")
744+
debug1("Subnets to exclude from forwarding:\n")
745+
for i in subnets_exclude:
746+
print(" "+str(i))
747+
if required.dns:
748+
debug1("DNS requests normally directed at these servers will be "
749+
"redirected to remote:\n")
750+
for i in nslist:
751+
print(" "+str(i))
752+
661753
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
662754
# if both ports given, no need to search for a spare port
663755
ports = [0, ]

sshuttle/cmdline.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,16 @@ def main():
4747
elif opt.hostwatch:
4848
return hostwatch.hw_main(opt.subnets, opt.auto_hosts)
4949
else:
50-
includes = opt.subnets + opt.subnets_file
51-
excludes = opt.exclude
50+
# parse_subnetports() is used to create a list of includes
51+
# and excludes. It is called once for each parameter and
52+
# returns a list of one or more items for each subnet (it
53+
# can return more than one item when a hostname in the
54+
# parameter resolves to multiple IP addresses. Here, we
55+
# flatten these lists.
56+
includes = [item for sublist in opt.subnets+opt.subnets_file
57+
for item in sublist]
58+
excludes = [item for sublist in opt.exclude for item in sublist]
59+
5260
if not includes and not opt.auto_nets:
5361
parser.error('at least one subnet, subnet file, '
5462
'or -N expected')

sshuttle/methods/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def set_firewall(self, firewall):
3838
@staticmethod
3939
def get_supported_features():
4040
result = Features()
41+
result.ipv4 = True
4142
result.ipv6 = False
4243
result.udp = False
4344
result.dns = True
@@ -68,7 +69,7 @@ def setup_udp_listener(self, udp_listener):
6869

6970
def assert_features(self, features):
7071
avail = self.get_supported_features()
71-
for key in ["udp", "dns", "ipv6", "user"]:
72+
for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
7273
if getattr(features, key) and not getattr(avail, key):
7374
raise Fatal(
7475
"Feature %s not supported with method %s.\n" %

sshuttle/options.py

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ def parse_subnetport_file(s):
2828
# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
2929
# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
3030
# example.com:123 or just example.com
31+
#
32+
# In addition, the port number can be specified as a range:
33+
# 1.2.3.4:8000-8080.
34+
#
35+
# Can return multiple matches if the domain name used in the request
36+
# has multiple IP addresses.
3137
def parse_subnetport(s):
38+
3239
if s.count(':') > 1:
3340
rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
3441
else:
@@ -38,19 +45,56 @@ def parse_subnetport(s):
3845
if not m:
3946
raise Fatal('%r is not a valid address/mask:port format' % s)
4047

41-
addr, width, fport, lport = m.groups()
48+
# Ports range from fport to lport. If only one port is specified,
49+
# fport is defined and lport is None.
50+
#
51+
# cidr is the mask defined with the slash notation
52+
host, cidr, fport, lport = m.groups()
4253
try:
43-
addrinfo = socket.getaddrinfo(addr, 0, 0, socket.SOCK_STREAM)
54+
addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM)
4455
except socket.gaierror:
45-
raise Fatal('Unable to resolve address: %s' % addr)
56+
raise Fatal('Unable to resolve address: %s' % host)
4657

47-
family, _, _, _, addr = min(addrinfo)
48-
max_width = 32 if family == socket.AF_INET else 128
49-
width = int(width or max_width)
50-
if not 0 <= width <= max_width:
51-
raise Fatal('width %d is not between 0 and %d' % (width, max_width))
58+
# If the address is a domain with multiple IPs and a mask is also
59+
# provided, proceed cautiously:
60+
if cidr is not None:
61+
addr_v6 = [a for a in addrinfo if a[0] == socket.AF_INET6]
62+
addr_v4 = [a for a in addrinfo if a[0] == socket.AF_INET]
63+
64+
# Refuse to proceed if IPv4 and IPv6 addresses are present:
65+
if len(addr_v6) > 0 and len(addr_v4) > 0:
66+
raise Fatal("%s has IPv4 and IPv6 addresses, so the mask "
67+
"of /%s is not supported. Specify the IP "
68+
"addresses directly if you wish to specify "
69+
"a mask." % (host, cidr))
70+
71+
# Warn if a domain has multiple IPs of the same type (IPv4 vs
72+
# IPv6) and the mask is applied to all of the IPs.
73+
if len(addr_v4) > 1 or len(addr_v6) > 1:
74+
print("WARNING: %s has multiple IP addresses. The "
75+
"mask of /%s is applied to all of the addresses."
76+
% (host, cidr))
77+
78+
rv = []
79+
for a in addrinfo:
80+
family, _, _, _, addr = a
5281

53-
return (family, addr[0], width, int(fport or 0), int(lport or fport or 0))
82+
# Largest possible slash value we can use with this IP:
83+
max_cidr = 32 if family == socket.AF_INET else 128
84+
85+
if cidr is None: # if no mask, use largest mask
86+
cidr_to_use = max_cidr
87+
else: # verify user-provided mask is appropriate
88+
cidr_to_use = int(cidr)
89+
if not 0 <= cidr_to_use <= max_cidr:
90+
raise Fatal('Slash in CIDR notation (/%d) is '
91+
'not between 0 and %d'
92+
% (cidr_to_use, max_cidr))
93+
94+
rv.append((family, addr[0], cidr_to_use,
95+
int(fport or 0), int(lport or fport or 0)))
96+
97+
return rv
5498

5599

56100
# 1.2.3.4:567 or just 1.2.3.4 or just 567
@@ -69,16 +113,21 @@ def parse_ipport(s):
69113
if not m:
70114
raise Fatal('%r is not a valid IP:port format' % s)
71115

72-
ip, port = m.groups()
73-
ip = ip or '0.0.0.0'
116+
host, port = m.groups()
117+
host = host or '0.0.0.0'
74118
port = int(port or 0)
75119

76120
try:
77-
addrinfo = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM)
121+
addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
78122
except socket.gaierror:
79-
raise Fatal('%r is not a valid IP:port format' % s)
123+
raise Fatal('Unable to resolve address: %s' % host)
124+
125+
if len(addrinfo) > 1:
126+
print("WARNING: Host %s has more than one IP, only using one of them."
127+
% host)
80128

81129
family, _, _, _, addr = min(addrinfo)
130+
# Note: addr contains (ip, port)
82131
return (family,) + addr[:2]
83132

84133

0 commit comments

Comments
 (0)