diff --git a/setup.py b/setup.py index 86f2a68..2c4952a 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 -import sys, os +import os +import sys + try: - from setuptools import setup + from setuptools import setup except ImportError: - from distutils.core import setup + from distutils.core import setup -if sys.version_info < (3,3): +if sys.version_info < (3, 3): sys.exit("Python 3.3+ is required; you are using %s" % sys.version) ######################################## @@ -20,20 +22,21 @@ ######################################## -setup(name="vpn-slice", - version=version_pep, - description=("vpnc-script replacement for easy split-tunnel VPN setup"), - long_description=open('description.rst').read(), - author="Daniel Lenski", - author_email="dlenski@gmail.com", - extras_require={ +setup( + name="vpn-slice", + version=version_pep, + description=("vpnc-script replacement for easy split-tunnel VPN setup"), + long_description=open('description.rst').read(), + author="Daniel Lenski", + author_email="dlenski@gmail.com", + extras_require={ "setproctitle": ["setproctitle"], "dnspython": ["dnspython"], - }, - install_requires=["setproctitle", "dnspython"], - license='GPL v3 or later', - url="https://github.com/dlenski/vpn-slice", - packages=["vpn_slice"], - include_package_data = True, - entry_points={ 'console_scripts': [ 'vpn-slice=vpn_slice.__main__:main' ] }, - ) + }, + install_requires=["setproctitle", "dnspython"], + license='GPL v3 or later', + url="https://github.com/dlenski/vpn-slice", + packages=["vpn_slice"], + include_package_data=True, + entry_points={'console_scripts': ['vpn-slice=vpn_slice.__main__:main']}, +) diff --git a/vpn_slice/__main__.py b/vpn_slice/__main__.py index 5cda6c9..56c5235 100755 --- a/vpn_slice/__main__.py +++ b/vpn_slice/__main__.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 from __future__ import print_function -from sys import stderr, platform -import os + import argparse +import os from enum import Enum +from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Interface, IPv6Network, ip_address, ip_network from itertools import chain, zip_longest -from ipaddress import ip_network, ip_address, IPv4Address, IPv4Network, IPv6Address, IPv6Network, IPv6Interface -from time import sleep -from random import randint, choice, shuffle +from random import choice, randint, shuffle from subprocess import CalledProcessError +from sys import platform, stderr +from time import sleep try: from setproctitle import setproctitle @@ -20,19 +21,18 @@ def setproctitle(title): def tagged(iter, tag): return zip_longest(iter, (), fillvalue=tag) -from .version import __version__ from .util import slurpy +from .version import __version__ def get_default_providers(): - global platform try: from .dnspython import DNSPythonProvider except ImportError: DNSPythonProvider = None if platform.startswith('linux'): - from .linux import ProcfsProvider, Iproute2Provider, IptablesProvider, CheckTunDevProvider + from .linux import CheckTunDevProvider, Iproute2Provider, IptablesProvider, ProcfsProvider from .posix import DigProvider, PosixHostsFileProvider return dict( process = ProcfsProvider, @@ -43,11 +43,12 @@ def get_default_providers(): prep = CheckTunDevProvider, ) elif platform.startswith('darwin'): - from platform import release from distutils.version import LooseVersion - from .mac import PsProvider, BSDRouteProvider, MacSplitDNSProvider, PfFirewallProvider - from .posix import PosixHostsFileProvider + from platform import release + from .dnspython import DNSPythonProvider + from .mac import BSDRouteProvider, MacSplitDNSProvider, PfFirewallProvider, PsProvider + from .posix import PosixHostsFileProvider return dict( process=PsProvider, route=BSDRouteProvider, @@ -57,10 +58,10 @@ def get_default_providers(): firewall = PfFirewallProvider if release() >= LooseVersion('10.6') else None, ) elif platform.startswith('freebsd'): - from .mac import BSDRouteProvider + from .dnspython import DNSPythonProvider from .freebsd import ProcfsProvider + from .mac import BSDRouteProvider from .posix import PosixHostsFileProvider - from .dnspython import DNSPythonProvider return dict( process = ProcfsProvider, route = BSDRouteProvider, @@ -269,7 +270,7 @@ def do_post_connect(env, args): providers.dns.configure(dns_servers=(env.dns + env.dns6), search_domains=args.domain, bind_addresses=env.myaddrs) for host in args.hosts: try: - ips = providers.dns.lookup_host(host) + ips = providers.dns.lookup_host(host) except Exception as e: print("WARNING: Lookup for %s on VPN DNS servers failed:\n\t%s" % (host, e), file=stderr) else: @@ -303,7 +304,7 @@ def do_post_connect(env, args): # run DNS queries in background to prevent idle timeout if args.prevent_idle_timeout: - dns = (env.dns + env.dns6) + dns = env.dns + env.dns6 idle_timeout = env.idle_timeout setproctitle('vpn-slice --prevent-idle-timeout --name %s' % args.name) if args.verbose: @@ -323,7 +324,7 @@ def do_post_connect(env, args): # pick random host or IP to look up without leaking any new information # about what we do/don't access within the VPN pool = args.hosts - pool += map(str, chain(env.dns, env.dns6, env.nbns, ((r.network_address) for r in args.subnets if r.prefixlen==r.max_prefixlen))) + pool += map(str, chain(env.dns, env.dns6, env.nbns, ((r.network_address) for r in args.subnets if r.prefixlen == r.max_prefixlen))) dummy = choice(pool) shuffle(dns) if args.verbose > 1: @@ -339,27 +340,27 @@ def do_post_connect(env, args): # into a more Pythonic form (these are take from vpnc-script) reasons = Enum('reasons', 'pre_init connect disconnect reconnect attempt_reconnect') vpncenv = [ - ('reason','reason',lambda x: reasons[x.replace('-','_')]), - ('gateway','VPNGATEWAY',ip_address), - ('tundev','TUNDEV',str), - ('domain','CISCO_DEF_DOMAIN',lambda x: x.split(),[]), - ('splitdns','CISCO_SPLIT_DNS',lambda x: x.split(','),[]), - ('banner','CISCO_BANNER',str), - ('myaddr','INTERNAL_IP4_ADDRESS',IPv4Address), # a.b.c.d - ('mtu','INTERNAL_IP4_MTU',int), - ('netmask','INTERNAL_IP4_NETMASK',IPv4Address), # a.b.c.d - ('netmasklen','INTERNAL_IP4_NETMASKLEN',int), - ('network','INTERNAL_IP4_NETADDR',IPv4Address), # a.b.c.d - ('dns','INTERNAL_IP4_DNS',lambda x: [ip_address(x) for x in x.split()],[]), - ('nbns','INTERNAL_IP4_NBNS',lambda x: [IPv4Address(x) for x in x.split()],[]), - ('myaddr6','INTERNAL_IP6_ADDRESS',IPv6Interface), # x:y::z or x:y::z/p - ('netmask6','INTERNAL_IP6_NETMASK',IPv6Interface), # x:y:z:: or x:y::z/p - ('dns6','INTERNAL_IP6_DNS',lambda x: [ip_address(x) for x in x.split()],[]), - ('nsplitinc','CISCO_SPLIT_INC',int,0), - ('nsplitexc','CISCO_SPLIT_EXC',int,0), - ('nsplitinc6','CISCO_IPV6_SPLIT_INC',int,0), - ('nsplitexc6','CISCO_IPV6_SPLIT_EXC',int,0), - ('idle_timeout','IDLE_TIMEOUT',int,600), + ('reason', 'reason', lambda x: reasons[x.replace('-', '_')]), + ('gateway', 'VPNGATEWAY', ip_address), + ('tundev', 'TUNDEV', str), + ('domain', 'CISCO_DEF_DOMAIN', lambda x: x.split(), []), + ('splitdns', 'CISCO_SPLIT_DNS', lambda x: x.split(','), []), + ('banner', 'CISCO_BANNER', str), + ('myaddr', 'INTERNAL_IP4_ADDRESS', IPv4Address), # a.b.c.d + ('mtu', 'INTERNAL_IP4_MTU', int), + ('netmask', 'INTERNAL_IP4_NETMASK', IPv4Address), # a.b.c.d + ('netmasklen', 'INTERNAL_IP4_NETMASKLEN', int), + ('network', 'INTERNAL_IP4_NETADDR', IPv4Address), # a.b.c.d + ('dns', 'INTERNAL_IP4_DNS', lambda x: [ip_address(x) for x in x.split()], []), + ('nbns', 'INTERNAL_IP4_NBNS', lambda x: [IPv4Address(x) for x in x.split()], []), + ('myaddr6', 'INTERNAL_IP6_ADDRESS', IPv6Interface), # x:y::z or x:y::z/p + ('netmask6', 'INTERNAL_IP6_NETMASK', IPv6Interface), # x:y:z:: or x:y::z/p + ('dns6', 'INTERNAL_IP6_DNS', lambda x: [ip_address(x) for x in x.split()], []), + ('nsplitinc', 'CISCO_SPLIT_INC', int, 0), + ('nsplitexc', 'CISCO_SPLIT_EXC', int, 0), + ('nsplitinc6', 'CISCO_IPV6_SPLIT_INC', int, 0), + ('nsplitexc6', 'CISCO_IPV6_SPLIT_EXC', int, 0), + ('idle_timeout', 'IDLE_TIMEOUT', int, 600), ] def parse_env(environ=os.environ): @@ -381,10 +382,10 @@ def parse_env(environ=os.environ): env.network = IPv4Network(env.network).supernet(new_prefix=env.netmasklen) if env.network.network_address != orig_netaddr: print("WARNING: IPv4 network %s/%d has host bits set, replacing with %s" % (orig_netaddr, env.netmasklen, env.network), file=stderr) - if env.network.netmask!=env.netmask: + if env.network.netmask != env.netmask: raise AssertionError("IPv4 network (INTERNAL_IP4_{{NETADDR,NETMASK}}) {ad}/{nm} does not match INTERNAL_IP4_NETMASKLEN={nml} (implies /{nmi})".format( ad=orig_netaddr, nm=env.netmask, nml=env.netmasklen, nmi=env.network.netmask)) - assert env.network.netmask==env.netmask + assert env.network.netmask == env.netmask # Need to match behavior of original vpnc-script here # Examples: @@ -393,7 +394,7 @@ def parse_env(environ=os.environ): # 3) INTERNAL_IP6_ADDRESS=2000::1, INTERNAL_IP6_NETMASK=unset => interface of 2000::1/128, network of 2000::1/128 if env.myaddr6 or env.netmask6: if not env.netmask6: - env.netmask6 = IPv6Network(env.myaddr6) # case 3 above, /128 + env.netmask6 = IPv6Network(env.myaddr6) # case 3 above, /128 env.myaddr6 = IPv6Interface(env.netmask6) env.network6 = env.myaddr6.network else: @@ -413,10 +414,10 @@ def parse_env(environ=os.environ): net = IPv4Network(ad).supernet(new_prefix=nml) if net.network_address != ad: print("WARNING: IPv4 split network (CISCO_SPLIT_%s_%d_{ADDR,MASK}) %s/%d has host bits set, replacing with %s" % (pfx, n, ad, nml, net), file=stderr) - if net.netmask!=nm: + if net.netmask != nm: raise AssertionError("IPv4 split network (CISCO_SPLIT_{pfx}_{n}_{{ADDR,MASK}}) {ad}/{nm} does not match CISCO_SPLIT_{pfx}_{n}_MASKLEN={nml} (implies /{nmi})".format( pfx=pfx, n=n, ad=ad, nm=nm, nml=nml, nmi=net.netmask)) - env['split'+pfx.lower()].append(net) + env['split' + pfx.lower()].append(net) for pfx, n in chain((('INC', n) for n in range(env.nsplitinc6)), (('EXC', n) for n in range(env.nsplitexc6))): @@ -425,7 +426,7 @@ def parse_env(environ=os.environ): net = IPv6Network(ad).supernet(new_prefix=nml) if net.network_address != ad: print("WARNING: IPv6 split network (CISCO_IPV6_SPLIT_%s_%d_{ADDR,MASKLEN}) %s/%d has host bits set, replacing with %s" % (pfx, n, ad, nml, net), file=stderr) - env['split'+pfx.lower()].append(net) + env['split' + pfx.lower()].append(net) return env @@ -434,16 +435,16 @@ def parse_args_and_env(args=None, environ=os.environ): p = argparse.ArgumentParser() p.add_argument('routes', nargs='*', type=net_or_host_param, help='List of VPN-internal hostnames, included subnets (e.g. 192.168.0.0/24), excluded subnets (e.g. %%8.0.0.0/8), or aliases (e.g. host1=192.168.1.2) to add to routing and /etc/hosts.') g = p.add_argument_group('Subprocess options') - g.add_argument('-k','--kill', default=[], action='append', help='File containing PID to kill before disconnect (may be specified multiple times)') - g.add_argument('-K','--prevent-idle-timeout', action='store_true', help='Prevent idle timeout by doing random DNS lookups (interval set by $IDLE_TIMEOUT, defaulting to 10 minutes)') + p.add_argument('-k', '--kill', default=[], action='append', help='File containing PID to kill before disconnect (may be specified multiple times)') + p.add_argument('-K', '--prevent-idle-timeout', action='store_true', help='Prevent idle timeout by doing random DNS lookups (interval set by $IDLE_TIMEOUT, defaulting to 10 minutes)') g = p.add_argument_group('Informational options') g.add_argument('--banner', action='store_true', help='Print banner message (default is to suppress it)') g = p.add_argument_group('Routing and hostname options') - g.add_argument('-i','--incoming', action='store_true', help='Allow incoming traffic from VPN (default is to block)') - g.add_argument('-n','--name', default=None, help='Name of this VPN (default is $TUNDEV)') - g.add_argument('-d','--domain', action='append', help='Search domain inside the VPN (default is $CISCO_DEF_DOMAIN)') - g.add_argument('-I','--route-internal', action='store_true', help="Add route for VPN's default subnet (passed in as $INTERNAL_IP*_NET*") - g.add_argument('-S','--route-splits', action='store_true', help="Add route for VPN's split-tunnel subnets (passed in via $CISCO_SPLIT_*)") + g.add_argument('-i', '--incoming', action='store_true', help='Allow incoming traffic from VPN (default is to block)') + g.add_argument('-n', '--name', default=None, help='Name of this VPN (default is $TUNDEV)') + g.add_argument('-d', '--domain', action='append', help='Search domain inside the VPN (default is $CISCO_DEF_DOMAIN)') + g.add_argument('-I', '--route-internal', action='store_true', help="Add route for VPN's default subnet (passed in as $INTERNAL_IP*_NET*") + g.add_argument('-S', '--route-splits', action='store_true', help="Add route for VPN's split-tunnel subnets (passed in via $CISCO_SPLIT_*)") g.add_argument('--no-host-names', action='store_false', dest='host_names', default=True, help='Do not add either short or long hostnames to /etc/hosts') g.add_argument('--no-short-names', action='store_false', dest='short_names', default=True, help="Only add long/fully-qualified domain names to /etc/hosts") g = p.add_argument_group('Nameserver options') @@ -452,9 +453,9 @@ def parse_args_and_env(args=None, environ=os.environ): g.add_argument('--domains-vpn-dns', dest='vpn_domains', default=None, help="comma separated domains to query with vpn dns") g = p.add_argument_group('Debugging options') g.add_argument('--self-test', action='store_true', help='Stop after verifying that environment variables and providers are configured properly.') - g.add_argument('-v','--verbose', default=0, action='count', help="Explain what %(prog)s is doing") - p.add_argument('-V','--version', action='version', version='%(prog)s ' + __version__) - g.add_argument('-D','--dump', action='store_true', help='Dump environment variables passed by caller') + g.add_argument('-v', '--verbose', default=0, action='count', help="Explain what %(prog)s is doing") + p.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__) + g.add_argument('-D', '--dump', action='store_true', help='Dump environment variables passed by caller') g.add_argument('--no-fork', action='store_false', dest='fork', help="Don't fork and continue in background on connect") g.add_argument('--ppid', type=int, help='PID of calling process (normally autodetected, when using openconnect or vpnc)') args = p.parse_args(args) @@ -501,9 +502,10 @@ def finalize_args_and_env(args, env): if args.ppid is None: args.ppid = providers.process.ppid_of(None) exe = providers.process.pid2exe(args.ppid) - if exe and os.path.basename(exe) in ('dash','bash','sh','tcsh','csh','ksh','zsh'): + if exe and os.path.basename(exe) in ('dash', 'bash', 'sh', 'tcsh', 'csh', 'ksh', 'zsh'): args.ppid = providers.process.ppid_of(args.ppid) + def main(args=None, environ=os.environ): global providers @@ -561,13 +563,13 @@ def main(args=None, environ=os.environ): print('WARNING: IPv6 address or netmask set. Support for IPv6 in %s should be considered BETA-QUALITY.' % p.prog, file=stderr) if args.dump: exe = providers.process.pid2exe(args.ppid) - caller = '%s (PID %d)'%(exe, args.ppid) if exe else 'PID %d' % args.ppid + caller = '%s (PID %d)' % (exe, args.ppid) if exe else 'PID %d' % args.ppid print('Called by %s with environment variables for vpnc-script:' % caller, file=stderr) width = max((len(envar) for var, envar, *rest in vpncenv if envar in environ), default=0) for var, envar, *rest in vpncenv: if envar in environ: - pyvar = var+'='+repr(env[var]) if var else 'IGNORED' + pyvar = var + '=' + repr(env[var]) if var else 'IGNORED' print(' %-*s => %s' % (width, envar, pyvar), file=stderr) if env.splitinc: print(' %-*s => %s=%r' % (width, 'CISCO_*SPLIT_INC_*', 'splitinc', env.splitinc), file=stderr) @@ -588,9 +590,9 @@ def main(args=None, environ=os.environ): if env.reason is None: raise SystemExit("Must be called as vpnc-script, with $reason set; use --help for more information") - elif env.reason==reasons.pre_init: + elif env.reason == reasons.pre_init: do_pre_init(env, args) - elif env.reason==reasons.disconnect: + elif env.reason == reasons.disconnect: do_disconnect(env, args) elif env.reason in (reasons.reconnect, reasons.attempt_reconnect): # FIXME: is there anything that reconnect or attempt_reconnect /should/ do @@ -604,7 +606,7 @@ def main(args=None, environ=os.environ): if args.verbose: print('WARNING: %s ignores reason=%s' % (p.prog, env.reason.name), file=stderr) - elif env.reason==reasons.connect: + elif env.reason == reasons.connect: do_connect(env, args) # we continue running in a new child process, so the VPN can actually @@ -614,5 +616,6 @@ def main(args=None, environ=os.environ): do_post_connect(env, args) -if __name__=='__main__': + +if __name__ == '__main__': main() diff --git a/vpn_slice/dnspython.py b/vpn_slice/dnspython.py index b5449dd..ef6ddc9 100644 --- a/vpn_slice/dnspython.py +++ b/vpn_slice/dnspython.py @@ -1,10 +1,12 @@ -from sys import stderr from ipaddress import ip_address -from dns.resolver import Resolver, NXDOMAIN, NoAnswer, Timeout -from dns.name import root, from_text +from sys import stderr + +from dns.name import from_text, root +from dns.resolver import NXDOMAIN, NoAnswer, Resolver, Timeout from .provider import DNSProvider + class DNSPythonProvider(DNSProvider): def configure(self, dns_servers, *, bind_addresses=None, search_domains=()): super().configure(dns_servers, bind_addresses=bind_addresses, search_domains=search_domains) diff --git a/vpn_slice/linux.py b/vpn_slice/linux.py index 25cb64e..2082a38 100644 --- a/vpn_slice/linux.py +++ b/vpn_slice/linux.py @@ -1,6 +1,6 @@ import os -import subprocess import stat +import subprocess from .posix import PosixProcessProvider from .provider import FirewallProvider, RouteProvider, TunnelPrepProvider @@ -34,16 +34,16 @@ def _iproute(self, *args, **kwargs): if v is not None: cl.extend((k, str(v))) - if args[:2]==('route','get'): + if args[:2] == ('route', 'get'): output_start, keys = 1, ('via', 'dev', 'src', 'mtu') - elif args[:2]==('link','show'): + 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, universal_newlines=True).split() - if args[:2]==('route','get') and words[0] in ('broadcast', 'multicast', 'local', 'unreachable'): + if args[:2] == ('route', 'get') and words[0] in ('broadcast', 'multicast', 'local', 'unreachable'): output_start += 1 return {words[i]: words[i + 1] for i in range(output_start, len(words), 2) if words[i] in keys} else: @@ -104,7 +104,8 @@ def create_tunnel(self): node = '/dev/net/tun' if not os.path.exists(node): os.makedirs(os.path.dirname(node), exist_ok=True) - os.mknod(node, mode=0o640 | stat.S_IFCHR, device = os.makedev(10, 200)) + os.mknod(node, mode=0o640 | stat.S_IFCHR, device=os.makedev(10, 200)) + 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") diff --git a/vpn_slice/mac.py b/vpn_slice/mac.py index 2514e25..073b212 100644 --- a/vpn_slice/mac.py +++ b/vpn_slice/mac.py @@ -4,7 +4,7 @@ from ipaddress import ip_interface from .posix import PosixProcessProvider -from .provider import RouteProvider, SplitDNSProvider, FirewallProvider +from .provider import FirewallProvider, RouteProvider, SplitDNSProvider from .util import get_executable diff --git a/vpn_slice/posix.py b/vpn_slice/posix.py index 2386af9..457af32 100644 --- a/vpn_slice/posix.py +++ b/vpn_slice/posix.py @@ -19,7 +19,7 @@ def lookup_host(self, hostname, keep_going=True): search_domains = self.search_domains if not bind_addresses: - some_cls = [ self.base_cl + ['@{!s}'.format(dns) for dns in dns_servers] ] + some_cls = [self.base_cl + ['@{!s}'.format(dns) for dns in dns_servers]] field_requests = [hostname, 'A', hostname, 'AAAA'] else: some_cls = [] diff --git a/vpn_slice/version.py b/vpn_slice/version.py index 10c277b..f379281 100644 --- a/vpn_slice/version.py +++ b/vpn_slice/version.py @@ -1 +1 @@ -__version__="0.15" +__version__ = "0.15"