summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/manpage.rst9
-rw-r--r--docs/requirements.rst4
-rw-r--r--requirements-tests.txt2
-rw-r--r--requirements.txt1
-rwxr-xr-xsetup.py1
-rw-r--r--sshuttle/client.py5
-rw-r--r--sshuttle/hostwatch.py79
-rw-r--r--sshuttle/methods/nat.py16
-rw-r--r--tests/client/test_methods_nat.py83
9 files changed, 153 insertions, 47 deletions
diff --git a/docs/manpage.rst b/docs/manpage.rst
index ead9a16..8770235 100644
--- a/docs/manpage.rst
+++ b/docs/manpage.rst
@@ -89,6 +89,13 @@ Options
few subnets over the VPN, you probably would prefer to
keep using your local DNS server for everything else.
+ :program:`sshuttle` tries to store a cache of the hostnames in
+ ~/.sshuttle.hosts on the remote host. Similarly, it tries to read
+ the file when you later reconnect to the host with --auto-hosts
+ enabled to quickly populate the host list. When troubleshooting
+ this feature, try removing this file on the remote host when
+ sshuttle is not running.
+
.. option:: -N, --auto-nets
In addition to the subnets provided on the command
@@ -178,7 +185,7 @@ Options
A comma-separated list of hostnames to use to
initialize the :option:`--auto-hosts` scan algorithm.
- :option:`--auto-hosts` does things like poll local SMB servers
+ :option:`--auto-hosts` does things like poll netstat output
for lists of local hostnames, but can speed things up
if you use this option to give it a few names to start
from.
diff --git a/docs/requirements.rst b/docs/requirements.rst
index f4499ea..fb04178 100644
--- a/docs/requirements.rst
+++ b/docs/requirements.rst
@@ -15,10 +15,12 @@ Supports:
* IPv4 TCP
* IPv4 DNS
+* IPv6 TCP
+* IPv6 DNS
Requires:
-* iptables DNAT, REDIRECT, and ttl modules.
+* iptables DNAT, REDIRECT, and ttl modules. ip6tables for IPv6.
Linux with nft method
~~~~~~~~~~~~~~~~~~~~~
diff --git a/requirements-tests.txt b/requirements-tests.txt
index 9bdcb0e..174dadb 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -1,5 +1,5 @@
-r requirements.txt
pytest==6.2.4
-pytest-cov==2.12.0
+pytest-cov==2.12.1
flake8==3.9.2
pyflakes==2.3.1
diff --git a/requirements.txt b/requirements.txt
index 48581a1..e5473dc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1 @@
setuptools-scm==6.0.1
-psutil
diff --git a/setup.py b/setup.py
index 735676c..54b751d 100755
--- a/setup.py
+++ b/setup.py
@@ -63,7 +63,6 @@ setup(
},
python_requires='>=3.6',
install_requires=[
- 'psutil',
],
tests_require=[
'pytest',
diff --git a/sshuttle/client.py b/sshuttle/client.py
index 349ad86..ef4f36b 100644
--- a/sshuttle/client.py
+++ b/sshuttle/client.py
@@ -6,7 +6,6 @@ import subprocess as ssubprocess
import os
import sys
import platform
-import psutil
import sshuttle.helpers as helpers
import sshuttle.ssnet as ssnet
@@ -650,7 +649,9 @@ def _main(tcp_listener, udp_listener, fw, ssh_cmd, remotename,
# poll() won't tell us when process exited since the
# process is no longer our child (it returns 0 all the
# time).
- if not psutil.pid_exists(serverproc.pid):
+ try:
+ os.kill(serverproc.pid, 0)
+ except OSError:
raise Fatal('ssh connection to server (pid %d) exited.' %
serverproc.pid)
else:
diff --git a/sshuttle/hostwatch.py b/sshuttle/hostwatch.py
index 7e4d3c5..a016f4f 100644
--- a/sshuttle/hostwatch.py
+++ b/sshuttle/hostwatch.py
@@ -15,6 +15,8 @@ POLL_TIME = 60 * 15
NETSTAT_POLL_TIME = 30
CACHEFILE = os.path.expanduser('~/.sshuttle.hosts')
+# Have we already failed to write CACHEFILE?
+CACHE_WRITE_FAILED = False
hostnames = {}
queue = {}
@@ -31,7 +33,10 @@ def _is_ip(s):
def write_host_cache():
+ """If possible, write our hosts file to disk so future connections
+ can reuse the hosts that we already found."""
tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid())
+ global CACHE_WRITE_FAILED
try:
f = open(tmpname, 'wb')
for name, ip in sorted(hostnames.items()):
@@ -39,7 +44,15 @@ def write_host_cache():
f.close()
os.chmod(tmpname, 384) # 600 in octal, 'rw-------'
os.rename(tmpname, CACHEFILE)
- finally:
+ CACHE_WRITE_FAILED = False
+ except (OSError, IOError):
+ # Write message if we haven't yet or if we get a failure after
+ # a previous success.
+ if not CACHE_WRITE_FAILED:
+ log("Failed to write host cache to temporary file "
+ "%s and rename it to %s" % (tmpname, CACHEFILE))
+ CACHE_WRITE_FAILED = True
+
try:
os.unlink(tmpname)
except BaseException:
@@ -47,25 +60,34 @@ def write_host_cache():
def read_host_cache():
+ """If possible, read the cache file from disk to populate hosts that
+ were found in a previous sshuttle run."""
try:
f = open(CACHEFILE)
- except IOError:
+ except (OSError, IOError):
_, e = sys.exc_info()[:2]
if e.errno == errno.ENOENT:
return
else:
- raise
+ log("Failed to read existing host cache file %s on remote host"
+ % CACHEFILE)
+ return
for line in f:
words = line.strip().split(',')
if len(words) == 2:
(name, ip) = words
name = re.sub(r'[^-\w\.]', '-', name).strip()
+ # Remove characters that shouldn't be in IP
ip = re.sub(r'[^0-9.]', '', ip).strip()
if name and ip:
found_host(name, ip)
def found_host(name, ip):
+ """The provided name maps to the given IP. Add the host to the
+ hostnames list, send the host to the sshuttle client via
+ stdout, and write the host to the cache file.
+ """
hostname = re.sub(r'\..*', '', name)
hostname = re.sub(r'[^-\w\.]', '_', hostname)
if (ip.startswith('127.') or ip.startswith('255.') or
@@ -84,29 +106,37 @@ def found_host(name, ip):
def _check_etc_hosts():
- debug2(' > hosts')
- for line in open('/etc/hosts'):
- line = re.sub(r'#.*', '', line)
- words = line.strip().split()
- if not words:
- continue
- ip = words[0]
- names = words[1:]
- if _is_ip(ip):
- debug3('< %s %r' % (ip, names))
- for n in names:
- check_host(n)
- found_host(n, ip)
+ """If possible, read /etc/hosts to find hosts."""
+ filename = '/etc/hosts'
+ debug2(' > Reading %s on remote host' % filename)
+ try:
+ for line in open(filename):
+ line = re.sub(r'#.*', '', line) # remove comments
+ words = line.strip().split()
+ if not words:
+ continue
+ ip = words[0]
+ if _is_ip(ip):
+ names = words[1:]
+ debug3('< %s %r' % (ip, names))
+ for n in names:
+ check_host(n)
+ found_host(n, ip)
+ except (OSError, IOError):
+ debug1("Failed to read %s on remote host" % filename)
def _check_revdns(ip):
+ """Use reverse DNS to try to get hostnames from an IP addresses."""
debug2(' > rev: %s' % ip)
try:
r = socket.gethostbyaddr(ip)
debug3('< %s' % r[0])
check_host(r[0])
found_host(r[0], ip)
- except (socket.herror, UnicodeError):
+ except (OSError, socket.error, UnicodeError):
+ # This case is expected to occur regularly.
+ # debug3('< %s gethostbyaddr failed on remote host' % ip)
pass
@@ -134,7 +164,14 @@ def _check_netstat():
log('%r failed: %r' % (argv, e))
return
+ # The same IPs may appear multiple times. Consolidate them so the
+ # debug message doesn't print the same IP repeatedly.
+ ip_list = []
for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content):
+ if ip not in ip_list:
+ ip_list.append(ip)
+
+ for ip in sorted(ip_list):
debug3('< %s' % ip)
check_host(ip)
@@ -179,13 +216,19 @@ def hw_main(seed_hosts, auto_hosts):
while 1:
now = time.time()
+ # For each item in the queue
for t, last_polled in list(queue.items()):
(op, args) = t
if not _stdin_still_ok(0):
break
+
+ # Determine if we need to run.
maxtime = POLL_TIME
+ # netstat runs more often than other jobs
if op == _check_netstat:
maxtime = NETSTAT_POLL_TIME
+
+ # Check if this jobs needs to run.
if now - last_polled > maxtime:
queue[t] = time.time()
op(*args)
@@ -195,5 +238,5 @@ def hw_main(seed_hosts, auto_hosts):
break
# FIXME: use a smarter timeout based on oldest last_polled
- if not _stdin_still_ok(1):
+ if not _stdin_still_ok(1): # sleeps for up to 1 second
break
diff --git a/sshuttle/methods/nat.py b/sshuttle/methods/nat.py
index ac4c56a..a7a661c 100644
--- a/sshuttle/methods/nat.py
+++ b/sshuttle/methods/nat.py
@@ -14,14 +14,12 @@ class Method(BaseMethod):
# "-A OUTPUT").
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
user, ttl, tmark):
- # only ipv4 supported with NAT
- if family != socket.AF_INET:
+ if family != socket.AF_INET and family != socket.AF_INET6:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
if udp:
raise Exception("UDP not supported by nat method_name")
-
table = "nat"
def _ipt(*args):
@@ -53,13 +51,18 @@ class Method(BaseMethod):
# This TTL hack allows the client and server to run on the
# same host. The connections the sshuttle server makes will
# have TTL set to 63.
- _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl', '%s' % ttl)
+ if family == socket.AF_INET:
+ _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'ttl', '--ttl',
+ '%s' % ttl)
+ else: # ipv6, ttl is renamed to 'hop limit'
+ _ipt_ttl('-A', chain, '-j', 'RETURN', '-m', 'hl', '--hl-eq',
+ '%s' % ttl)
# Redirect DNS traffic as requested. This includes routing traffic
# to localhost DNS servers through sshuttle.
for _, ip in [i for i in nslist if i[0] == family]:
_ipt('-A', chain, '-j', 'REDIRECT',
- '--dest', '%s/32' % ip,
+ '--dest', '%s' % ip,
'-p', 'udp',
'--dport', '53',
'--to-ports', str(dnsport))
@@ -87,7 +90,7 @@ class Method(BaseMethod):
def restore_firewall(self, port, family, udp, user):
# only ipv4 supported with NAT
- if family != socket.AF_INET:
+ if family != socket.AF_INET and family != socket.AF_INET6:
raise Exception(
'Address family "%s" unsupported by nat method_name'
% family_to_string(family))
@@ -123,6 +126,7 @@ class Method(BaseMethod):
def get_supported_features(self):
result = super(Method, self).get_supported_features()
result.user = True
+ result.ipv6 = True
return result
def is_supported(self):
diff --git a/tests/client/test_methods_nat.py b/tests/client/test_methods_nat.py
index e0bf2a9..0bd0ec2 100644
--- a/tests/client/test_methods_nat.py
+++ b/tests/client/test_methods_nat.py
@@ -11,7 +11,7 @@ from sshuttle.methods import get_method
def test_get_supported_features():
method = get_method('nat')
features = method.get_supported_features()
- assert not features.ipv6
+ assert features.ipv6
assert not features.udp
assert features.dns
@@ -92,18 +92,51 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
method = get_method('nat')
assert method.name == 'nat'
- with pytest.raises(Exception) as excinfo:
- method.setup_firewall(
- 1024, 1026,
- [(AF_INET6, u'2404:6800:4004:80c::33')],
- AF_INET6,
- [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
- (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
- True,
- None,
- 63, '0x01')
- assert str(excinfo.value) \
- == 'Address family "AF_INET6" unsupported by nat method_name'
+ assert mock_ipt_chain_exists.mock_calls == []
+ assert mock_ipt_ttl.mock_calls == []
+ assert mock_ipt.mock_calls == []
+ method.setup_firewall(
+ 1024, 1026,
+ [(AF_INET6, u'2404:6800:4004:80c::33')],
+ AF_INET6,
+ [(AF_INET6, 64, False, u'2404:6800:4004:80c::', 0, 0),
+ (AF_INET6, 128, True, u'2404:6800:4004:80c::101f', 80, 80)],
+ False,
+ None,
+ 63, '0x01')
+
+ assert mock_ipt_chain_exists.mock_calls == [
+ call(AF_INET6, 'nat', 'sshuttle-1024')
+ ]
+ assert mock_ipt_ttl.mock_calls == [
+ call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN',
+ '-m', 'hl', '--hl-eq', '63')
+ ]
+ assert mock_ipt.mock_calls == [
+ call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-F', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-X', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-N', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-F', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1024'),
+ call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT',
+ '--dest', u'2404:6800:4004:80c::33', '-p', 'udp',
+ '--dport', '53', '--to-ports', '1026'),
+ call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN',
+ '-m', 'addrtype', '--dst-type', 'LOCAL'),
+ call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'RETURN',
+ '--dest', u'2404:6800:4004:80c::101f/128', '-p', 'tcp',
+ '--dport', '80:80'),
+ call(AF_INET6, 'nat', '-A', 'sshuttle-1024', '-j', 'REDIRECT',
+ '--dest', u'2404:6800:4004:80c::/64', '-p', 'tcp',
+ '--to-ports', '1024')
+ ]
+ mock_ipt_chain_exists.reset_mock()
+ mock_ipt_ttl.reset_mock()
+ mock_ipt.reset_mock()
+
assert mock_ipt_chain_exists.mock_calls == []
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == []
@@ -149,7 +182,7 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
call(AF_INET, 'nat', '-I', 'OUTPUT', '1', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-I', 'PREROUTING', '1', '-j', 'sshuttle-1025'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'REDIRECT',
- '--dest', u'1.2.3.33/32', '-p', 'udp',
+ '--dest', u'1.2.3.33', '-p', 'udp',
'--dport', '53', '--to-ports', '1027'),
call(AF_INET, 'nat', '-A', 'sshuttle-1025', '-j', 'RETURN',
'-m', 'addrtype', '--dst-type', 'LOCAL'),
@@ -169,11 +202,29 @@ def test_setup_firewall(mock_ipt_chain_exists, mock_ipt_ttl, mock_ipt):
]
assert mock_ipt_ttl.mock_calls == []
assert mock_ipt.mock_calls == [
- call(AF_INET, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
- call(AF_INET, 'nat', '-D', 'PREROUTING', '-j', 'sshuttle-1025'),
+ call(AF_INET, 'nat', '-D', 'OUTPUT', '-j',
+ 'sshuttle-1025'),
+ call(AF_INET, 'nat', '-D', 'PREROUTING', '-j',
+ 'sshuttle-1025'),
call(AF_INET, 'nat', '-F', 'sshuttle-1025'),
call(AF_INET, 'nat', '-X', 'sshuttle-1025')
]
mock_ipt_chain_exists.reset_mock()
mock_ipt_ttl.reset_mock()
mock_ipt.reset_mock()
+
+ method.restore_firewall(1025, AF_INET6, False, None)
+ assert mock_ipt_chain_exists.mock_calls == [
+ call(AF_INET6, 'nat', 'sshuttle-1025')
+ ]
+ assert mock_ipt_ttl.mock_calls == []
+ assert mock_ipt.mock_calls == [
+ call(AF_INET6, 'nat', '-D', 'OUTPUT', '-j', 'sshuttle-1025'),
+ call(AF_INET6, 'nat', '-D', 'PREROUTING', '-j',
+ 'sshuttle-1025'),
+ call(AF_INET6, 'nat', '-F', 'sshuttle-1025'),
+ call(AF_INET6, 'nat', '-X', 'sshuttle-1025')
+ ]
+ mock_ipt_chain_exists.reset_mock()
+ mock_ipt_ttl.reset_mock()
+ mock_ipt.reset_mock()