Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add macOS support #11

Merged
merged 12 commits into from
May 26, 2019
14 changes: 14 additions & 0 deletions vpn_slice/generic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .provider import FirewallProvider, TunnelPrepProvider


class NoFirewallProvider(FirewallProvider):
def configure_firewall(self, device):
pass

def deconfigure_firewall(self, device):
pass


class NoTunnelPrepProvider(TunnelPrepProvider):
def prepare_tunnel(self):
pass
198 changes: 97 additions & 101 deletions vpn_slice/linux.py
Original file line number Diff line number Diff line change
@@ -1,102 +1,98 @@
import os, fcntl
import subprocess as sp
from shutil import which
from ipaddress import ip_address

def find_paths():
global DIG, IPROUTE, HOSTS, IPTABLES
DIG = which('dig') or '/usr/bin/dig'
IPROUTE = which('ip') or '/sbin/ip'
IPTABLES = which('iptables') or '/sbin/iptables'
HOSTS = '/etc/hosts'

for binary in (DIG, IPROUTE, IPTABLES):
if not os.access(binary, os.X_OK):
raise OSError("cannot execute %s" % binary)
if not os.access(HOSTS, os.R_OK | os.W_OK):
raise OSError("cannot read/write %s" % HOSTS)

def pid2exe(pid):
try:
return os.readlink('/proc/%d/exe' % pid)
except (OSError, IOError):
return None

def ppidof(pid):
try:
return int(next(open('/proc/%d/stat'%pid)).split()[3])
except (OSError, ValueError, IOError):
pass

def check_tun():
if not os.access('/dev/net/tun', os.R_OK|os.W_OK):
raise OSError("can't read and write /dev/net/tun")

def write_hosts(host_map, tag):
global HOSTS
with open(HOSTS,'r+') as hostf:
fcntl.flock(hostf, fcntl.LOCK_EX) # POSIX only, obviously
lines = hostf.readlines()
keeplines = [l for l in lines if not l.endswith('# %s\n'%tag)]
hostf.seek(0,0)
hostf.writelines(keeplines)
for ip, names in host_map:
print('%s %s\t\t# %s' % (ip, ' '.join(names), tag), file=hostf)
hostf.truncate()
return len(host_map) or len(lines)-len(keeplines)

def dig(bind, host, dns, domains=None, reverse=False):
global DIG
host, dns = str(host), map(str, dns)
basecl = [DIG,'+short','+noedns']+(['-b'+str(bind)] if bind else [])+['@'+s for s in dns]
if reverse:
extras = (['-x'],)
if domains is None:
extras = ([],)
elif isinstance(domains, str):
extras = (['+domain=' + d],)
else:
extras = (['+domain=' + d] for d in domains)

out = set()
for extra in extras:
#print cl
p = sp.Popen(basecl + extra + [host], stdout=sp.PIPE)
lines = [l.strip() for l in p.communicate()[0].decode().splitlines()]
if lines and p.wait()==0:
for line in lines:
line = line.rstrip('\n.')
if reverse:
n, d = line.split('.',1)
out.add(n if d in domains else line)
else:
try:
out.add(ip_address(line))
except ValueError:
pass # didn't return an IP address!
return out or None

def iproute(*args):
global IPROUTE
cl = [IPROUTE]
for arg in args:
if isinstance(arg, dict):
for k,v in arg.items():
cl += [k] if v is None else [k, str(v)]
import os
import subprocess
from signal import SIGTERM

from .provider import FirewallProvider, ProcessProvider, RouteProvider, TunnelPrepProvider
from .util import get_executable


class ProcfsProvider(ProcessProvider):
def pid2exe(self, pid):
try:
return os.readlink('/proc/%d/exe' % pid)
except (OSError, IOError):
return None

def ppid_of(self, pid=None):
if pid is None:
return os.getppid()
try:
return int(next(open('/proc/%d/stat' % pid)).split()[3])
except (OSError, ValueError, IOError):
return None

def kill(self, pid, signal=SIGTERM):
os.kill(pid, signal)


class Iproute2Provider(RouteProvider):
def __init__(self):
self.iproute = get_executable('/sbin/ip')

def _iproute(self, *args, **kwargs):
cl = [self.iproute]
cl.extend(str(v) for v in args if v is not None)
for k, v in kwargs.items():
if v is not None:
cl.extend((k, str(v)))

if args[:2]==('route','get'):
output_start, keys = 1, ('via', 'dev', 'src', 'mtu')
elif args[:2]==('link','show'):
output_start, keys = 3, ('state', 'mtu')
else:
output_start = None

if output_start is not None:
words = subprocess.check_output(cl).decode().split()
return {words[i]: words[i + 1] for i in range(output_start, len(words), 2) if words[i] in keys}
else:
cl.append(str(arg))

if args[:2]==('route','get'): get, start, keys = 'route', 1, ('via','dev','src','mtu')
elif args[:2]==('link','show'): get, start, keys = 'link', 3, ('mtu','state')
else: get = None

if get is None:
sp.check_call(cl)
else:
w = sp.check_output(cl).decode().split()
return {w[ii]:w[ii+1] for ii in range(start, len(w), 2) if w[ii] in keys}

def iptables(*args):
global IPTABLES
cl = [IPTABLES] + list(args)
sp.check_call(cl)
subprocess.check_call(cl)

def add_route(self, destination, *, via=None, dev=None, src=None, mtu=None):
self._iproute('route', 'add', destination, via=via, dev=dev, src=src, mtu=mtu)

def replace_route(self, destination, *, via=None, dev=None, src=None, mtu=None):
self._iproute('route', 'replace', destination, via=via, dev=dev, src=src, mtu=mtu)

def remove_route(self, destination):
self._iproute('route', 'del', destination)

def get_route(self, destination):
return self._iproute('route', 'get', destination)

def flush_cache(self):
self._iproute('route', 'flush', 'cache')

def get_link_info(self, device):
return self._iproute('link', 'show', device)

def set_link_info(self, device, state, mtu=None):
self._iproute('link', 'set', state, dev=device, mtu=mtu)

def add_address(self, device, address):
self._iproute('address', 'add', address, dev=device)


class IptablesProvider(FirewallProvider):
def __init__(self):
self.iptables = get_executable('/sbin/iptables')

def _iptables(self, *args):
cl = [self.iptables]
cl.extend(args)
subprocess.check_call(cl)

def configure_firewall(self, device):
self._iptables('-A', 'INPUT', '-i', device, '-m', 'state', '--state', 'RELATED,ESTABLISHED', '-j', 'ACCEPT')
self._iptables('-A', 'INPUT', '-i', device, '-j', 'DROP')

def deconfigure_firewall(self, device):
self._iptables('-D', 'INPUT', '-i', device, '-j', 'DROP')
self._iptables('-D', 'INPUT', '-i', device, '-m', 'state', '--state', 'RELATED,ESTABLISHED', '-j', 'ACCEPT')


class CheckTunDevProvider(TunnelPrepProvider):
def prepare_tunnel(self):
if not os.access('/dev/net/tun', os.R_OK | os.W_OK):
raise OSError("can't read and write /dev/net/tun")
109 changes: 109 additions & 0 deletions vpn_slice/mac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import os
import re
import subprocess
from ipaddress import ip_network
from signal import SIGTERM

from .provider import ProcessProvider, RouteProvider
from .util import get_executable


class PsProvider(ProcessProvider):
def __init__(self):
self.lsof = get_executable('/usr/sbin/lsof')
self.ps = get_executable('/bin/ps')

def pid2exe(self, pid):
info = subprocess.check_output([self.lsof, '-p', str(pid)]).decode()
for line in info.splitlines():
parts = line.split()
if parts[3] == 'txt':
return parts[8]

def ppid_of(self, pid=None):
if pid is None:
return os.getppid()
try:
return int(subprocess.check_output([self.ps, '-p', str(pid), '-o', 'ppid=']).decode().strip())
except ValueError:
return None

def kill(self, pid, signal=SIGTERM):
os.kill(pid, signal)


class BSDRouteProvider(RouteProvider):
def __init__(self):
self.route = get_executable('/sbin/route')
self.ifconfig = get_executable('/sbin/ifconfig')

def _route(self, *args):
return subprocess.check_output([self.route, '-n'] + list(map(str, args))).decode()

def _ifconfig(self, *args):
return subprocess.check_output([self.ifconfig] + list(map(str, args))).decode()

def add_route(self, destination, *, via=None, dev=None, src=None, mtu=None):
args = ['add']
if mtu is not None:
args.extend(('-mtu', str(mtu)))
if via is not None:
args.extend((destination, via))
elif dev is not None:
args.extend(('-interface', destination, dev))
self._route(*args)

replace_route = add_route

def remove_route(self, destination):
self._route('delete', destination)

def get_route(self, destination):
info = self._route('get', destination)
lines = iter(info.splitlines())
info_d = {}
for line in lines:
if ':' not in line:
break
key, _, val = line.partition(':')
info_d[key.strip()] = val.strip()
keys = line.split()
vals = next(lines).split()
info_d.update(zip(keys, vals))
return {
'via': info_d['gateway'],
'dev': info_d['interface'],
'mtu': info_d['mtu'],
}

def flush_cache(self):
pass

_LINK_INFO_RE = re.compile(r'flags=\d<(.*?)>\smtu\s(\d+)$')

def get_link_info(self, device):
info = self._ifconfig(device)
match = self._LINK_INFO_RE.search(info)
if match:
flags = match.group(1).split(',')
mtu = int(match.group(2))
return {
'state': 'up' if 'UP' in flags else 'down',
'mtu': mtu,
}
return None

def set_link_info(self, device, state, mtu=None):
args = [device]
if state is not None:
args.append(state)
if mtu is not None:
args.extend(('mtu', str(mtu)))
self._ifconfig(*args)

def add_address(self, device, address):
if address.version == 6:
family = 'inet6'
else:
family = 'inet'
self._ifconfig(device, family, ip_network(address), address)
Loading