mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-05-11 19:44:40 +02:00
Add an "is_supported()" function to the different methods so that each method can include whatever logic they wish to indicate if they are supported on a particular machine. Previously, methods/__init__.py contained all of the logic for selecting individual methods. Now, it iterates through a list of possible options and stops on the first method that it finds that is_supported(). Currently, the decision is made based on the presence of programs in the PATH. In the future, things such as the platform sshuttle is running on could be considered.
263 lines
8.5 KiB
Python
263 lines
8.5 KiB
Python
import os
|
|
import subprocess as ssubprocess
|
|
from sshuttle.methods import BaseMethod
|
|
from sshuttle.helpers import log, debug1, debug2, debug3, \
|
|
Fatal, family_to_string, get_env, which
|
|
|
|
recvmsg = None
|
|
try:
|
|
# try getting recvmsg from python
|
|
import socket as pythonsocket
|
|
getattr(pythonsocket.socket, "recvmsg")
|
|
socket = pythonsocket
|
|
recvmsg = "python"
|
|
except AttributeError:
|
|
# try getting recvmsg from socket_ext library
|
|
try:
|
|
import socket_ext
|
|
getattr(socket_ext.socket, "recvmsg")
|
|
socket = socket_ext
|
|
recvmsg = "socket_ext"
|
|
except ImportError:
|
|
import socket
|
|
|
|
IP_BINDANY = 24
|
|
IP_RECVDSTADDR = 7
|
|
SOL_IPV6 = 41
|
|
IPV6_RECVDSTADDR = 74
|
|
|
|
if recvmsg == "python":
|
|
def recv_udp(listener, bufsize):
|
|
debug3('Accept UDP python using recvmsg.\n')
|
|
data, ancdata, _, srcip = listener.recvmsg(4096,
|
|
socket.CMSG_SPACE(4))
|
|
dstip = None
|
|
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
|
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVDSTADDR:
|
|
port = 53
|
|
ip = socket.inet_ntop(socket.AF_INET, cmsg_data[0:4])
|
|
dstip = (ip, port)
|
|
break
|
|
return (srcip, dstip, data)
|
|
elif recvmsg == "socket_ext":
|
|
def recv_udp(listener, bufsize):
|
|
debug3('Accept UDP using socket_ext recvmsg.\n')
|
|
srcip, data, adata, _ = listener.recvmsg((bufsize,),
|
|
socket.CMSG_SPACE(4))
|
|
dstip = None
|
|
for a in adata:
|
|
if a.cmsg_level == socket.SOL_IP and a.cmsg_type == IP_RECVDSTADDR:
|
|
port = 53
|
|
ip = socket.inet_ntop(socket.AF_INET, a.cmsg_data[0:4])
|
|
dstip = (ip, port)
|
|
break
|
|
return (srcip, dstip, data[0])
|
|
else:
|
|
def recv_udp(listener, bufsize):
|
|
debug3('Accept UDP using recvfrom.\n')
|
|
data, srcip = listener.recvfrom(bufsize)
|
|
return (srcip, None, data)
|
|
|
|
|
|
def ipfw_rule_exists(n):
|
|
argv = ['ipfw', 'list']
|
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
|
|
|
found = False
|
|
for line in p.stdout:
|
|
if line.startswith(b'%05d ' % n):
|
|
if not ('ipttl 63' in line or 'check-state' in line):
|
|
log('non-sshuttle ipfw rule: %r\n' % line.strip())
|
|
raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n)
|
|
found = True
|
|
rv = p.wait()
|
|
if rv:
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
|
return found
|
|
|
|
|
|
_oldctls = {}
|
|
|
|
|
|
def _fill_oldctls(prefix):
|
|
argv = ['sysctl', prefix]
|
|
p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, env=get_env())
|
|
for line in p.stdout:
|
|
line = line.decode()
|
|
assert(line[-1] == '\n')
|
|
(k, v) = line[:-1].split(': ', 1)
|
|
_oldctls[k] = v.strip()
|
|
rv = p.wait()
|
|
if rv:
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
|
if not line:
|
|
raise Fatal('%r returned no data' % (argv,))
|
|
|
|
|
|
def _sysctl_set(name, val):
|
|
argv = ['sysctl', '-w', '%s=%s' % (name, val)]
|
|
debug1('>> %s\n' % ' '.join(argv))
|
|
return ssubprocess.call(argv, stdout=open(os.devnull, 'w'), env=get_env())
|
|
# No env: No output. (Or error that won't be parsed.)
|
|
|
|
|
|
_changedctls = []
|
|
|
|
|
|
def sysctl_set(name, val, permanent=False):
|
|
PREFIX = 'net.inet.ip'
|
|
assert(name.startswith(PREFIX + '.'))
|
|
val = str(val)
|
|
if not _oldctls:
|
|
_fill_oldctls(PREFIX)
|
|
if not (name in _oldctls):
|
|
debug1('>> No such sysctl: %r\n' % name)
|
|
return False
|
|
oldval = _oldctls[name]
|
|
if val != oldval:
|
|
rv = _sysctl_set(name, val)
|
|
if rv == 0 and permanent:
|
|
debug1('>> ...saving permanently in /etc/sysctl.conf\n')
|
|
f = open('/etc/sysctl.conf', 'a')
|
|
f.write('\n'
|
|
'# Added by sshuttle\n'
|
|
'%s=%s\n' % (name, val))
|
|
f.close()
|
|
else:
|
|
_changedctls.append(name)
|
|
return True
|
|
|
|
|
|
def ipfw(*args):
|
|
argv = ['ipfw', '-q'] + list(args)
|
|
debug1('>> %s\n' % ' '.join(argv))
|
|
rv = ssubprocess.call(argv, env=get_env())
|
|
# No env: No output. (Or error that won't be parsed.)
|
|
if rv:
|
|
raise Fatal('%r returned %d' % (argv, rv))
|
|
|
|
|
|
def ipfw_noexit(*args):
|
|
argv = ['ipfw', '-q'] + list(args)
|
|
debug1('>> %s\n' % ' '.join(argv))
|
|
ssubprocess.call(argv, env=get_env())
|
|
# No env: No output. (Or error that won't be parsed.)
|
|
|
|
|
|
class Method(BaseMethod):
|
|
|
|
def get_supported_features(self):
|
|
result = super(Method, self).get_supported_features()
|
|
result.ipv6 = False
|
|
result.udp = False # NOTE: Almost there, kernel patch needed
|
|
result.dns = True
|
|
return result
|
|
|
|
def get_tcp_dstip(self, sock):
|
|
return sock.getsockname()
|
|
|
|
def recv_udp(self, udp_listener, bufsize):
|
|
srcip, dstip, data = recv_udp(udp_listener, bufsize)
|
|
if not dstip:
|
|
debug1(
|
|
"-- ignored UDP from %r: "
|
|
"couldn't determine destination IP address\n" % (srcip,))
|
|
return None
|
|
return srcip, dstip, data
|
|
|
|
def send_udp(self, sock, srcip, dstip, data):
|
|
if not srcip:
|
|
debug1(
|
|
"-- ignored UDP to %r: "
|
|
"couldn't determine source IP address\n" % (dstip,))
|
|
return
|
|
|
|
# debug3('Sending SRC: %r DST: %r\n' % (srcip, dstip))
|
|
sender = socket.socket(sock.family, socket.SOCK_DGRAM)
|
|
sender.setsockopt(socket.SOL_IP, IP_BINDANY, 1)
|
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
sender.setsockopt(socket.SOL_IP, socket.IP_TTL, 63)
|
|
sender.bind(srcip)
|
|
sender.sendto(data, dstip)
|
|
sender.close()
|
|
|
|
def setup_udp_listener(self, udp_listener):
|
|
if udp_listener.v4 is not None:
|
|
udp_listener.v4.setsockopt(socket.SOL_IP, IP_RECVDSTADDR, 1)
|
|
# if udp_listener.v6 is not None:
|
|
# udp_listener.v6.setsockopt(SOL_IPV6, IPV6_RECVDSTADDR, 1)
|
|
|
|
def setup_firewall(self, port, dnsport, nslist, family, subnets, udp,
|
|
user):
|
|
# IPv6 not supported
|
|
if family not in [socket.AF_INET]:
|
|
raise Exception(
|
|
'Address family "%s" unsupported by ipfw method_name'
|
|
% family_to_string(family))
|
|
|
|
# XXX: Any risk from this?
|
|
ipfw_noexit('delete', '1')
|
|
|
|
while _changedctls:
|
|
name = _changedctls.pop()
|
|
oldval = _oldctls[name]
|
|
_sysctl_set(name, oldval)
|
|
|
|
if subnets or dnsport:
|
|
sysctl_set('net.inet.ip.fw.enable', 1)
|
|
|
|
ipfw('add', '1', 'check-state', 'ip',
|
|
'from', 'any', 'to', 'any')
|
|
|
|
ipfw('add', '1', 'skipto', '2',
|
|
'tcp',
|
|
'from', 'any', 'to', 'table(125)')
|
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % port,
|
|
'tcp',
|
|
'from', 'any', 'to', 'table(126)',
|
|
'not', 'ipttl', '63', 'keep-state', 'setup')
|
|
|
|
ipfw_noexit('table', '124', 'flush')
|
|
dnscount = 0
|
|
for _, ip in [i for i in nslist if i[0] == family]:
|
|
ipfw('table', '124', 'add', '%s' % (ip))
|
|
dnscount += 1
|
|
if dnscount > 0:
|
|
ipfw('add', '1', 'fwd', '127.0.0.1,%d' % dnsport,
|
|
'udp',
|
|
'from', 'any', 'to', 'table(124)',
|
|
'not', 'ipttl', '63')
|
|
ipfw('add', '1', 'allow',
|
|
'udp',
|
|
'from', 'any', 'to', 'any',
|
|
'ipttl', '63')
|
|
|
|
if subnets:
|
|
# create new subnet entries
|
|
for _, swidth, sexclude, snet in sorted(subnets,
|
|
key=lambda s: s[1],
|
|
reverse=True):
|
|
if sexclude:
|
|
ipfw('table', '125', 'add', '%s/%s' % (snet, swidth))
|
|
else:
|
|
ipfw('table', '126', 'add', '%s/%s' % (snet, swidth))
|
|
|
|
def restore_firewall(self, port, family, udp, user):
|
|
if family not in [socket.AF_INET]:
|
|
raise Exception(
|
|
'Address family "%s" unsupported by tproxy method'
|
|
% family_to_string(family))
|
|
|
|
ipfw_noexit('delete', '1')
|
|
ipfw_noexit('table', '124', 'flush')
|
|
ipfw_noexit('table', '125', 'flush')
|
|
ipfw_noexit('table', '126', 'flush')
|
|
|
|
def is_supported(self):
|
|
if which("ipfw"):
|
|
return True
|
|
debug2("ipfw method not supported because 'ipfw' command is "
|
|
"missing.\n")
|
|
return False
|