Update/document client's handling of IPv4 and IPv6.

Additional comments, checks, warning messages, and diagnostic
information is printed out when the client starts.

We assume IPv4 is always present and enabled. We assume IPv6 is not
supported when it is disabled at the command line or when it is not
supported by the firewall method. Warn if IPv6 is disabled but the
user specified IPv6 subnets, IPv6 DNS servers, or IPv6 excludes that
are effectively ignored.

Instead of indicating which features are on/off, we also indicate if
features are available in the verbose output.

We also more clearly print the subnets that we forward, excludes, and
any redirected DNS servers to the terminal output.

These changes should help handling bug reports and make it clearer to
users what is happening. It should also make it more graceful when a
user specifies a subnet/exclude with hostname that resolves to both
IPv4 and IPv6 (but IPv6 is disabled in sshuttle).
This commit is contained in:
Scott Kuhl 2020-10-18 16:30:29 -04:00
parent c2b10465e7
commit b7a29acab7
2 changed files with 124 additions and 31 deletions

View File

@ -586,31 +586,76 @@ def main(listenip_v6, listenip_v4,
fw = FirewallClient(method_name, sudo_pythonpath) fw = FirewallClient(method_name, sudo_pythonpath)
# Get family specific subnet lists # If --dns is used, store the IP addresses that the client
# normally uses for DNS lookups in nslist. The firewall needs to
# redirect packets outgoing to this server to the remote host
# instead.
if dns: if dns:
nslist += resolvconf_nameservers() nslist += resolvconf_nameservers()
if to_nameserver is not None: if to_nameserver is not None:
to_nameserver = "%s@%s" % tuple(to_nameserver[1:]) to_nameserver = "%s@%s" % tuple(to_nameserver[1:])
else: else:
# option doesn't make sense if we aren't proxying dns # option doesn't make sense if we aren't proxying dns
if to_nameserver and len(to_nameserver) > 0:
print("WARNING: --to-ns option is ignored because --dns was not "
"used.")
to_nameserver = None to_nameserver = None
subnets = subnets_include + subnets_exclude # we don't care here # Get family specific subnet lists. Also, the user may not specify
subnets_v6 = [i for i in subnets if i[0] == socket.AF_INET6] # any subnets if they use --auto-nets. In this case, our subnets
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6] # list will be empty and the forwarded subnets will be determined
subnets_v4 = [i for i in subnets if i[0] == socket.AF_INET] # later by the server.
subnets_v4 = [i for i in subnets_include if i[0] == socket.AF_INET]
subnets_v6 = [i for i in subnets_include if i[0] == socket.AF_INET6]
nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET] nslist_v4 = [i for i in nslist if i[0] == socket.AF_INET]
nslist_v6 = [i for i in nslist if i[0] == socket.AF_INET6]
# Check features available # Get available features from the firewall method
avail = fw.method.get_supported_features() avail = fw.method.get_supported_features()
# A feature is "required" if the user supplies us parameters which
# implies that the feature is needed.
required = Features() required = Features()
# Select the default addresses to bind to / listen to.
# Assume IPv4 is always available and should always be enabled. If
# a method doesn't provide IPv4 support or if we wish to run
# ipv6-only, changes to this code are required.
assert avail.ipv4
required.ipv4 = True
# listenip_v4 contains user specified value or it is set to "auto".
if listenip_v4 == "auto":
listenip_v4 = ('127.0.0.1', 0)
# listenip_v6 is...
# None when IPv6 is disabled.
# "auto" when listen address is unspecified.
# The user specified address if provided by user
if listenip_v6 is None:
debug1("IPv6 disabled by --disable-ipv6\n")
if listenip_v6 == "auto": if listenip_v6 == "auto":
if avail.ipv6: if avail.ipv6:
debug1("IPv6 enabled: Using default IPv6 listen address ::1\n")
listenip_v6 = ('::1', 0) listenip_v6 = ('::1', 0)
else: else:
debug1("IPv6 disabled since it isn't supported by method "
"%s.\n" % fw.method.name)
listenip_v6 = None listenip_v6 = None
# Make final decision about enabling IPv6:
required.ipv6 = False
if listenip_v6:
required.ipv6 = True
# If we get here, it is possible that listenip_v6 was user
# specified but not supported by the current method.
if required.ipv6 and not avail.ipv6:
raise Fatal("An IPv6 listen address was supplied, but IPv6 is "
"disabled at your request or is unsupported by the %s "
"method." % fw.method.name)
if user is not None: if user is not None:
if getpwnam is None: if getpwnam is None:
raise Fatal("Routing by user not available on this system.") raise Fatal("Routing by user not available on this system.")
@ -618,38 +663,66 @@ def main(listenip_v6, listenip_v4,
user = getpwnam(user).pw_uid user = getpwnam(user).pw_uid
except KeyError: except KeyError:
raise Fatal("User %s does not exist." % user) raise Fatal("User %s does not exist." % user)
if fw.method.name != 'nat':
required.ipv6 = len(subnets_v6) > 0 or listenip_v6 is not None
required.ipv4 = len(subnets_v4) > 0 or listenip_v4 is not None
else:
required.ipv6 = None
required.ipv4 = None
required.udp = avail.udp
required.dns = len(nslist) > 0
required.user = False if user is None else True required.user = False if user is None else True
# if IPv6 not supported, ignore IPv6 DNS servers if not required.ipv6 and len(subnets_v6) > 0:
if not required.ipv6: print("WARNING: IPv6 subnets were ignored because IPv6 is disabled "
"in sshuttle.")
subnets_v6 = []
subnets_include = subnets_v4
required.udp = avail.udp # automatically enable UDP if it is available
required.dns = len(nslist) > 0
# Remove DNS servers using IPv6.
if required.dns:
if not required.ipv6 and len(nslist_v6) > 0:
print("WARNING: Your system is configured to use an IPv6 DNS "
"server but sshuttle is not using IPv6. Therefore DNS "
"traffic your system sends to the IPv6 DNS server won't "
"be redirected via sshuttle to the remote machine.")
nslist_v6 = [] nslist_v6 = []
nslist = nslist_v4 nslist = nslist_v4
if len(nslist) == 0:
raise Fatal("Can't redirect DNS traffic since IPv6 is not "
"enabled in sshuttle and all of the system DNS "
"servers are IPv6.")
# If we aren't using IPv6, we can safely ignore excluded IPv6 subnets.
if not required.ipv6:
orig_len = len(subnets_exclude)
subnets_exclude = [i for i in subnets_exclude
if i[0] == socket.AF_INET]
if len(subnets_exclude) < orig_len:
print("WARNING: Ignoring one or more excluded IPv6 subnets "
"because IPv6 is not enabled.")
# This will print error messages if we required a feature that
# isn't available by the current method.
fw.method.assert_features(required) fw.method.assert_features(required)
if required.ipv6 and listenip_v6 is None:
raise Fatal("IPv6 required but not listening.")
# display features enabled # display features enabled
debug1("IPv6 enabled: %r\n" % required.ipv6) def feature_status(label, enabled, available):
debug1("UDP enabled: %r\n" % required.udp) msg = label + ": "
debug1("DNS enabled: %r\n" % required.dns) if enabled:
debug1("User enabled: %r\n" % required.user) msg += "on"
else:
msg += "off "
if available:
msg += "(available)"
else:
msg += "(not available with %s method)" % fw.method.name
debug1(msg + "\n")
# bind to required ports debug1("Method: %s\n" % fw.method.name)
if listenip_v4 == "auto": feature_status("IPv4", required.ipv4, avail.ipv4)
listenip_v4 = ('127.0.0.1', 0) feature_status("IPv6", required.ipv6, avail.ipv6)
feature_status("UDP ", required.udp, avail.udp)
feature_status("DNS ", required.dns, avail.dns)
feature_status("User", required.user, avail.user)
# Exclude traffic destined to our listen addresses.
if required.ipv4 and \ if required.ipv4 and \
not any(listenip_v4[0] == sex[1] for sex in subnets_v4): not any(listenip_v4[0] == sex[1] for sex in subnets_v4):
subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0)) subnets_exclude.append((socket.AF_INET, listenip_v4[0], 32, 0, 0))
@ -658,6 +731,25 @@ def main(listenip_v6, listenip_v4,
not any(listenip_v6[0] == sex[1] for sex in subnets_v6): not any(listenip_v6[0] == sex[1] for sex in subnets_v6):
subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0)) subnets_exclude.append((socket.AF_INET6, listenip_v6[0], 128, 0, 0))
# We don't print the IP+port of where we are listening here
# because we do that below when we have identified the ports to
# listen on.
debug1("Subnets to forward through remote host (type, IP, cidr mask "
"width, startPort, endPort):\n")
for i in subnets_include:
print(" "+str(i))
if auto_nets:
debug1("NOTE: Additional subnets to forward may be added below by "
"--auto-nets.\n")
debug1("Subnets to exclude from forwarding:\n")
for i in subnets_exclude:
print(" "+str(i))
if required.dns:
debug1("DNS requests normally directed at these servers will be "
"redirected to remote:\n")
for i in nslist:
print(" "+str(i))
if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]: if listenip_v6 and listenip_v6[1] and listenip_v4 and listenip_v4[1]:
# if both ports given, no need to search for a spare port # if both ports given, no need to search for a spare port
ports = [0, ] ports = [0, ]

View File

@ -38,6 +38,7 @@ class BaseMethod(object):
@staticmethod @staticmethod
def get_supported_features(): def get_supported_features():
result = Features() result = Features()
result.ipv4 = True
result.ipv6 = False result.ipv6 = False
result.udp = False result.udp = False
result.dns = True result.dns = True
@ -68,7 +69,7 @@ class BaseMethod(object):
def assert_features(self, features): def assert_features(self, features):
avail = self.get_supported_features() avail = self.get_supported_features()
for key in ["udp", "dns", "ipv6", "user"]: for key in ["udp", "dns", "ipv6", "ipv4", "user"]:
if getattr(features, key) and not getattr(avail, key): if getattr(features, key) and not getattr(avail, key):
raise Fatal( raise Fatal(
"Feature %s not supported with method %s.\n" % "Feature %s not supported with method %s.\n" %