Skip to content

Commit

Permalink
Merge pull request #11 from gmacon/feature/macos
Browse files Browse the repository at this point in the history
Add macOS support
  • Loading branch information
dlenski authored May 26, 2019
2 parents ad772cf + edfa341 commit 8a061f8
Show file tree
Hide file tree
Showing 7 changed files with 514 additions and 165 deletions.
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

0 comments on commit 8a061f8

Please sign in to comment.