mirror of
https://github.com/sshuttle/sshuttle.git
synced 2025-04-21 01:38:48 +02:00
Even when --tmark was used, the iptables code always used '1' for the mark. This patch corrects the problem. Previously, it wasn't clear if the tmark should be supplied in hexadecimal or as an integer. This makes it use hexadecimal, checks that the input is hexadecimal, and updates the associated documentation. This patch also makes --ttl information get passed to the firewall in a way that matches how other information gets passed. The ttl and tmark information are passed next to each other in many places and this patch also makes the order consistent.
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.')
|
|
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.')
|
|
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.')
|
|
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' % 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' % ' '.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' % 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')
|
|
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' % ' '.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' % ' '.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" % (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" % (dstip,))
|
|
return
|
|
|
|
# debug3('Sending SRC: %r DST: %r' % (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, ttl, tmark):
|
|
# 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', ttl, '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', ttl)
|
|
ipfw('add', '1', 'allow',
|
|
'udp',
|
|
'from', 'any', 'to', 'any',
|
|
'ipttl', ttl)
|
|
|
|
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.")
|
|
return False
|