Exemple #1
0
 def __init__(self):
     self.ensure_config_dirs()
     self._active_servers = None
     self.settings = SettingsHandler(paths.SETTINGS)
     self.credentials = CredentialsHandler(paths.CREDENTIALS)
     self.country_blacklist = self.settings.get_blacklist()
     self.country_whitelist = self.settings.get_whitelist()
     self._config_info = None
Exemple #2
0
    def setup(self):
        self.create_directories()

        self.settings = SettingsHandler(paths.SETTINGS)
        self.credentials = CredentialsHandler(paths.CREDENTIALS)

        self.black_list = self.settings.get_blacklist()
        self.white_list = self.settings.get_whitelist()

        if os.path.isfile(paths.ACTIVE_SERVERS):
            self.active_servers = self.load_active_servers(paths.ACTIVE_SERVERS)
Exemple #3
0
class NordNM(object):
    def __init__(self):
        parser = argparse.ArgumentParser()
        subparsers = parser.add_subparsers(title="commands", help="Each command has its own help page, which can be accessed via nordnm <COMMAND> --help", metavar='')

        # Kill-switch and auto-connect are repeated, to allow their use with or without the sync command.
        # TODO: Find out if there's a way to re-use the attributes so they don't need to be manually repeated
        parser.add_argument("-v", "--version", help="Display the package version.", action="store_true")
        parser.add_argument("-k", "--kill-switch", help="Sets a network kill-switch, to disable the active network interface when an active VPN connection disconnects.", action="store_true")
        parser.add_argument("-a", "--auto-connect", nargs=3, metavar=("[COUNTRY_CODE]", "[VPN_CATEGORY]", "[PROTOCOL]"), help="Configure NetworkManager to auto-connect to the chosen server type. Takes country code, category and protocol.")

        remove_parser = subparsers.add_parser("remove", aliases=['r'], help="Remove active connections, auto-connect, kill-switch, data, mac settings or all.")
        remove_parser.add_argument("--all", dest="remove_all", help="Remove all connections, enabled features and local data.", action="store_true")
        remove_parser.add_argument("-c", "--connections", dest="remove_c", help="Remove all active connections and auto-connect.", action="store_true")
        remove_parser.add_argument("-a", "--auto-connect", dest="remove_ac", help="Remove the active auto-connect feature.", action="store_true")
        remove_parser.add_argument("-k", "--kill-switch", dest="remove_ks", help="Remove the active kill-switch feature.", action="store_true")
        remove_parser.add_argument("-d", "--data", dest="remove_d", help="Remove existing local data (VPN Configs, Credentials & Settings).", action="store_true")
        remove_parser.add_argument("-m", "--mac-settings", dest="remove_m", help="Remove existing MAC Address settings configured by nordnm.", action="store_true")
        remove_parser.set_defaults(remove=True)

        update_parser = subparsers.add_parser('update', aliases=['u'], help='Update a specified setting.')
        update_parser.add_argument('-c', '--credentials', help='Update your existing saved credentials.', action='store_true')
        update_parser.add_argument('-s', '--settings', help='Update your existing saved settings.', action='store_true')
        update_parser.set_defaults(update=True)

        list_parser = subparsers.add_parser('list', aliases=['l'], help="List the specified information.")
        list_parser.add_argument('--active-servers', help='Display a list of the active servers currently synchronised.', action='store_true', default=False)
        list_parser.add_argument('--countries', help='Display a list of the available NordVPN countries.', action='store_true', default=False)
        list_parser.add_argument('--categories', help='Display a list of the available NordVPN categories..', action='store_true', default=False)
        list_parser.set_defaults(list=True)

        sync_parser = subparsers.add_parser('sync', aliases=['s'], help="Synchronise the optimal servers (based on load and latency) to NetworkManager.")
        sync_parser.add_argument('-s', '--slow-mode', help="Run benchmarking in 'slow mode'. May increase benchmarking success by pinging servers at a slower rate.", action='store_true')
        sync_parser.add_argument('-p', '--preserve-vpn', help="When provided, synchronising will preserve any active VPN instead of disabling it for more accurate benchmarking.", action='store_true')
        sync_parser.add_argument('-n', '--no-update', help='Do not download the latest OpenVPN configurations from NordVPN.', action='store_true', default=False)
        sync_parser.add_argument("-k", "--kill-switch", help="Sets a network kill-switch, to disable the active network interface when an active VPN connection disconnects.", action="store_true")
        sync_parser.add_argument('-a', '--auto-connect', nargs=3, metavar=('[COUNTRY_CODE]', '[VPN_CATEGORY]', '[PROTOCOL]'), help='Configure NetworkManager to auto-connect to the chosen server type. Takes country code, category and protocol.')
        sync_parser.set_defaults(sync=True)

        import_parser = subparsers.add_parser('import', aliases=['i'], help="Import an OpenVPN config file to NetworkManager.")
        import_parser.add_argument("config_file", metavar='CONFIG_FILE', help="The OpenVPN config file to be imported.")
        import_parser.add_argument("-k", "--kill-switch", help="Sets a network kill-switch, to disable the active network interface when an active VPN connection disconnects.", action="store_true")
        import_parser.add_argument('-a', '--auto-connect', help='Configure NetworkManager to auto-connect to the the imported config.', action="store_true", dest="auto_connect_imported", default=False)
        import_parser.add_argument('-u', '--username', required=True, help="Specify the username used for the OpenVPN config.", metavar="USERNAME")
        import_parser.add_argument('-p', '--password', required=True, help="Specify the password used for the OpenVPN config.", metavar="PASSWORD")
        import_parser.set_defaults(import_config=True)

        # For reference: https://blogs.gnome.org/thaller/category/networkmanager/
        mac_parser = subparsers.add_parser('mac', aliases=['m'], help="Global NetworkManager MAC address preferences. This command will affect ALL NetworkManager connections permanently.")
        mac_parser.add_argument('-r', '--random', help="A randomised MAC addresss will be generated on each connect.", action='store_true')
        mac_parser.add_argument('-s', '--stable', help="Use a stable, hashed MAC address on connect.", action='store_true')
        mac_parser.add_argument('-e', '--explicit', help="Specify a MAC address to use on connect.", nargs=1, metavar='"MAC_ADDRESS"')
        mac_parser.add_argument('--preserve', help="Don't change the current MAC address upon connection.", action='store_true')
        mac_parser.add_argument('--permanent', help="Use the permanent MAC address of the device on connect.", action='store_true')
        mac_parser.set_defaults(mac=True)

        self.logger = logging.getLogger(__name__)
        self.active_servers = {}

        try:
            args = parser.parse_args()
        except Exception:
            parser.print_help()
            sys.exit(1)

        # Count the number of arguments provided
        arg_count = 0
        for arg in vars(args):
            if getattr(args, arg):
                arg_count += 1

        if arg_count == 0:
            parser.print_help()
            sys.exit(1)

        if "version" in args and args.version:
            print(__version__)
            sys.exit(1)

        self.print_splash()

        # Check for commands that should be run on their own
        if "remove" in args and args.remove:
            removed = False

            if not args.remove_c and not args.remove_d and not args.remove_ac and not args.remove_ks and not args.remove_m and not args.remove_all:
                remove_parser.print_help()
                sys.exit(1)

            if args.remove_all:
                # Removing all, so set all args to True
                args.remove_ks = True
                args.remove_ac = True
                args.remove_c = True
                args.remove_d = True
                args.remove_m = True
            elif args.remove_c:
                # We need to remove the auto-connect if we are removing all connections
                args.remove_ac = True

            if args.remove_ks:
                if networkmanager.remove_killswitch():
                    removed = True

            if args.remove_ac:
                if networkmanager.remove_autoconnect():
                    removed = True

            if args.remove_c:
                # Get the active servers, since self.setup() hasn't run
                if os.path.isfile(paths.ACTIVE_SERVERS):
                    self.active_servers = self.load_active_servers(paths.ACTIVE_SERVERS)

                if self.remove_active_connections():
                    removed = True

            if args.remove_d:
                if self.remove_data():
                    removed = True

            if args.remove_m:
                if networkmanager.remove_global_mac_address():
                    removed = True

            if removed:
                networkmanager.reload_connections()
            else:
                self.logger.info("Nothing to remove.")

            sys.exit(0)
        elif "list" in args and args.list:
            if not args.countries and not args.categories and not args.active_servers:
                list_parser.print_help()
                sys.exit(1)

            if args.categories:
                self.print_categories()
            if args.countries:
                self.print_countries()
            if args.active_servers:
                self.print_active_servers()

            sys.exit(0)
        elif "mac" in args and args.mac:
            value = None
            if args.random:
                value = "random"
            elif args.stable:
                value = "stable"
            elif args.explicit:
                value = args.explicit[0]
            elif args.preserve:
                value = "preserve"
            elif args.permanent:
                value = "permanent"

            if value:
                if networkmanager.set_global_mac_address(value):
                    networkmanager.restart()
            else:
                mac_parser.print_help()

        # Now that arguments that don't need to be disturbed by setup() are over, do setup()
        self.setup()

        if "update" in args and args.update:
            if not args.credentials and not args.settings:
                update_parser.print_help()
                sys.exit(1)

            if args.credentials:
                self.credentials.save_new_credentials()
            if args.settings:
                self.settings.save_new_settings()

            sys.exit(0)

        # Now check for commands that can be chained...
        if "sync" in args and args.sync:
            # Take the inverse of no_update arg as update parameter
            self.sync(not args.no_update, args.preserve_vpn, args.slow_mode)

        if "import_config" in args and args.import_config:
            if not self.import_config(args.config_file, args.username, args.password):
                sys.exit(1)

            if args.auto_connect_imported:
                self.enable_auto_connect(*IMPORTED_SERVER_KEY)

        if args.kill_switch:
            networkmanager.set_killswitch()

        if args.auto_connect:
            country_code = args.auto_connect[0]
            category = args.auto_connect[1]
            protocol = args.auto_connect[2]

            self.enable_auto_connect(country_code, category, protocol)

        sys.exit(0)

    def print_splash(self):
        version_string = __version__

        latest_version = utils.get_pypi_package_version(__package__)
        if latest_version:
            if StrictVersion(version_string) < StrictVersion(latest_version):  # There's a new version on PyPi
                version_string = version_string + " (v" + latest_version + " available!)"
            else:
                version_string = version_string + " (Latest)"

        print("     _   _               _ _   _ ___  ___\n"
              "    | \\ | |             | | \\ | ||  \\/  |\n"
              "    |  \\| | ___  _ __ __| |  \\| || .  . |\n"
              "    | . ` |/ _ \\| '__/ _` | . ` || |\\/| |\n"
              "    | |\\  | (_) | | | (_| | |\\  || |  | |\n"
              "    \\_| \\_/\\___/|_|  \\__,_\\_| \\_/\\_|  |_/   v%s\n" % version_string)

    def print_categories(self):
        format_string = "| %-10s | %-20s |"
        print("\n Note: You must use the short name in this tool.\n")
        print(format_string % ("SHORT NAME", "LONG NAME"))
        print("|------------+----------------------|")

        for long_name, short_name in nordapi.VPN_CATEGORIES.items():
            print(format_string % (short_name, long_name))

        print()  # For spacing

    def print_countries(self):
        servers = nordapi.get_server_list(sort_by_country=True)
        if servers:
            format_string = "| %-22s | %-4s |"
            countries = []

            print("\n Note: You must use the country code, NOT the country name in this tool.\n")
            print(format_string % ("NAME", "CODE"))
            print("|------------------------+------|")

            for server in servers:
                country_code = server['flag']
                if country_code not in countries:
                    countries.append(country_code)
                    country_name = server['country']
                    print(format_string % (country_name, country_code))

            print()  # For spacing
        else:
            self.logger.error("Could not get available countries from the NordVPN API.")

    def print_active_servers(self):
        if os.path.isfile(paths.ACTIVE_SERVERS):
            self.active_servers = self.load_active_servers(paths.ACTIVE_SERVERS)

        if self.active_servers:
            print("Note: All metrics below are from the last synchronise.\n")
            format_string = "| %-16s | %-20s | %-8s | %-11s | %-8s |"
            print(format_string % ("PARAMETER", "SERVER", "LOAD (%)", "LATENCY (s)", "SCORE"))
            print("|------------------+----------------------+----------+-------------+----------|")

            for params in self.active_servers:
                parameters = ' '.join(params).lower()
                domain = self.active_servers[params]['domain']
                score = self.active_servers[params]['score']
                load = self.active_servers[params]['load']
                latency = round(self.active_servers[params]['latency'], 2)

                print(format_string % (parameters, domain, load, latency, score))

            print()  # For spacing
        else:
            self.logger.warning("No active servers to display.")

    def setup(self):
        self.create_directories()

        self.settings = SettingsHandler(paths.SETTINGS)
        self.credentials = CredentialsHandler(paths.CREDENTIALS)

        self.black_list = self.settings.get_blacklist()
        self.white_list = self.settings.get_whitelist()

        if os.path.isfile(paths.ACTIVE_SERVERS):
            self.active_servers = self.load_active_servers(paths.ACTIVE_SERVERS)

    def remove_legacy_files(self):
        removed = False
        for file_path in paths.LEGACY_FILES:
            try:
                os.remove(file_path)
                removed = True
            except FileNotFoundError:
                pass
            except Exception as e:
                self.logger.error("Error attempting to remove '%s': %s" % (file_path, e))

        return removed

    def set_config_info(self, etag):
        if os.path.exists(paths.OVPN_CONFIGS):
            with open(paths.CONFIG_INFO, 'w') as f:
                f.write(etag)

            return True
        else:
            return False

    def get_config_info(self):
        if os.path.exists(paths.CONFIG_INFO):
            with open(paths.CONFIG_INFO, 'r') as f:
                info = f.read().replace('\n', '')

            return info
        else:
            return None

    def delete_configs(self):
        def main():
            for f in os.listdir(paths.OVPN_CONFIGS):
                file_path = os.path.join(paths.OVPN_CONFIGS, f)
                try:
                    if os.path.isfile(file_path):
                        os.unlink(file_path)
                    elif os.path.isdir(file_path):
                        shutil.rmtree(file_path)
                except Exception as e:
                    self.logger.error("Could not delete config file: %s" % e)

        # Requires root privilege
        return utils.run_as_root(main)

    def get_configs(self):
        self.logger.info("Downloading latest NordVPN OpenVPN configuration files to '%s'." % paths.OVPN_CONFIGS)

        etag = self.get_config_info()
        config_data = nordapi.get_configs(etag)
        if config_data is False:
            self.logger.error("Failed to retrieve configuration files from NordVPN")
            return False
        elif config_data:
            zip_file, etag = config_data
            if zip_file and etag:
                self.delete_configs()

                if not utils.extract_zip(zip_file, paths.OVPN_CONFIGS):
                    self.logger.error("Failed to extract configuration files")
                    return False

                if not self.set_config_info(etag):
                    return False
            else:
                self.logger.info("Configuration files already up-to-date.")

            return True

    def sync(self, update_config=True, preserve_vpn=False, slow_mode=False):
        if self.remove_legacy_files():
            self.logger.info("Removed legacy files")

        if update_config:
            self.get_configs()

        if self.sync_servers(preserve_vpn, slow_mode):
            networkmanager.reload_connections()

    def import_config(self, file_path: str, username: str, password: str) -> bool:
        updated = False
        imported = False
        if self.remove_legacy_files():
            self.logger.info("Removed legacy files")

        if not os.path.isfile(file_path):
            self.logger.error("Configuration file '%s' does not exist.", file_path)
            return None

        # remove all old connections and any auto-connect, until a better import routine is added
        if self.remove_active_connections():
            updated = True
        if networkmanager.remove_autoconnect():
            updated = True

        dns_list = self.settings.get_custom_dns_servers()
        connection_name = os.path.splitext(os.path.basename(file_path))[0]

        if networkmanager.import_connection(file_path, connection_name, username, password, dns_list):
            updated = True
            imported = True
            self.active_servers[IMPORTED_SERVER_KEY] = {
                'name': connection_name,
                'domain': '<' + connection_name + '>',
                'score': -1,
                'load': -1,
                'latency': -1,
            }
            self.save_active_servers(self.active_servers, paths.ACTIVE_SERVERS)

        if updated:
            networkmanager.reload_connections()

        return imported

    def remove_data(self):
        if os.path.exists(paths.ROOT):
            try:
                shutil.rmtree(paths.ROOT)
            except Exception as e:
                self.logger.error("Could not remove the data directory '%s': %s" % (paths.ROOT, e))
                return False
        else:
            self.logger.info("Data directory does not exist. Nothing to remove.")

        self.logger.info("Data directory '%s' removed successfully!" % paths.ROOT)
        return True

    def create_directories(self):
        if not os.path.exists(paths.ROOT):
            os.mkdir(paths.ROOT)

        if not os.path.exists(paths.OVPN_CONFIGS):
            os.mkdir(paths.OVPN_CONFIGS)

    def get_ovpn_path(self, domain, protocol):
        ovpn_path = None

        try:
            files = glob.glob(paths.OVPN_CONFIGS + '/**/' + domain + '.' + protocol + '*.ovpn')

            if not files:
                return False

            ovpn_path = files[0]
        except Exception as ex:
            self.logger.error(ex)

        return ovpn_path

    def enable_auto_connect(self, country_code: str, category: str = 'normal', protocol: str = 'tcp'):
        enabled = False
        selected_parameters = (country_code.lower(), category.lower(), protocol.lower())

        if selected_parameters in self.active_servers:
            connection_name = self.active_servers[selected_parameters]['name']
            connection_load = self.active_servers[selected_parameters]['load']
            connection_latency = self.active_servers[selected_parameters]['latency']

            if networkmanager.set_auto_connect(connection_name):
                self.logger.info("Auto-connect enabled for '%s' (Load: %i%%, Latency: %0.2fs).", connection_name, connection_load, connection_latency)

                # Temporarily remove the kill-switch if there was one
                kill_switch = networkmanager.remove_killswitch(log=False)

                networkmanager.disconnect_active_vpn(self.active_servers)

                if kill_switch:
                    networkmanager.set_killswitch(log=False)

                if networkmanager.enable_connection(connection_name):
                    enabled = True
        else:
            self.logger.error("Auto-connect not activated: No active server found matching [%s, %s, %s].", country_code, category, protocol)

        return enabled

    def remove_active_connections(self):
        if self.active_servers:
            self.logger.info("Removing all active connections...")
            active_servers = copy.deepcopy(self.active_servers)
            for key in self.active_servers.keys():
                connection_name = self.active_servers[key]['name']
                if self.connection_exists(connection_name):
                    networkmanager.remove_connection(connection_name)

                del active_servers[key]
                self.save_active_servers(active_servers, paths.ACTIVE_SERVERS)  # Save after every successful removal, in case importer is killed abruptly

            self.active_servers = active_servers

            return True
        else:
            self.logger.info("No active connections to remove.")

    def load_active_servers(self, path):
        try:
            with open(paths.ACTIVE_SERVERS, 'rb') as fp:
                active_servers = pickle.load(fp)
            return active_servers
        except Exception as ex:
            self.logger.error(ex)
            return None

    def save_active_servers(self, active_servers, path):
        try:
            with open(paths.ACTIVE_SERVERS, 'wb') as fp:
                pickle.dump(active_servers, fp)

        except Exception as ex:
            self.logger.error(ex)

    def country_is_selected(self, country_code):
        # If (there is a whitelist and the country code is whitelisted) or (there is no whitelist, but there is a blacklist and it's not in the blacklist) or (there is no whitelist or blacklist)
        if (self.white_list and country_code in self.white_list) or (not self.white_list and self.black_list and country_code not in self.black_list) or (not self.white_list and not self.black_list):
            return True
        else:
            return False

    def has_valid_categories(self, server):
        valid_categories = self.settings.get_categories()

        # If the server has a category that is valid, return true
        for category in server['categories']:
            if category['name'] in valid_categories:
                return True

        return False

    def has_valid_protocol(self, server):
        valid_protocols = self.settings.get_protocols()
        has_openvpn_tcp = server['features']['openvpn_tcp']
        has_openvpn_udp = server['features']['openvpn_udp']

        if ('tcp' in valid_protocols and has_openvpn_tcp) or ('udp' in valid_protocols and has_openvpn_udp):
            return True
        else:
            return False

    def get_valid_servers(self, servers):
        valid_server_list = []

        for server in servers:
            country_code = server['flag'].lower()

            # If the server country has been selected, it has a selected protocol and selected categories
            if self.country_is_selected(country_code) and self.has_valid_protocol(server) and self.has_valid_categories(server):
                valid_server_list.append(server)

        return valid_server_list

    def connection_exists(self, connection_name):
        vpn_connections = networkmanager.get_vpn_connections()

        if vpn_connections and connection_name in vpn_connections:
            return True
        else:
            return False

    def configs_exist(self):
        configs = os.listdir(paths.OVPN_CONFIGS)
        if configs:
            return True
        else:
            return False

    def sync_servers(self, preserve_vpn, slow_mode):
        updated = False

        username = self.credentials.get_username()
        password = self.credentials.get_password()

        # Check if there are custom DNS servers specified in the settings before loading the defaults
        dns_list = self.settings.get_custom_dns_servers()

        if not self.configs_exist():
            self.logger.warning("No OpenVPN configuration files found.")
            if not self.get_configs():
                sys.exit(1)

        self.logger.info("Checking for new connections to import...")

        server_list = nordapi.get_server_list(sort_by_load=True)
        if server_list:

            valid_server_list = self.get_valid_servers(server_list)
            if valid_server_list:

                if not preserve_vpn:
                    # If there's a kill-switch in place, we need to temporarily remove it, otherwise it will kill out network when disabling an active VPN below
                    # Disconnect active Nord VPNs, so we get a more reliable benchmark
                    show_warning = False
                    if networkmanager.remove_killswitch():
                        show_warning = True
                        warning_string = "Kill-switch"
                    if networkmanager.disconnect_active_vpn(self.active_servers):
                        if show_warning:
                            warning_string = "Active VPN(s) and " + warning_string
                        else:
                            show_warning = True
                            warning_string = "Active VPN(s)"

                    if show_warning:
                        self.logger.warning("%s disabled for accurate benchmarking. Your connection is not secure until these are re-enabled.", warning_string)
                else:
                    self.logger.warning("Active VPN preserved. This may give unreliable results!")

                if slow_mode:
                    self.logger.info("Benchmarking slow mode enabled.")

                num_servers = len(valid_server_list)
                self.logger.info("Benchmarking %i servers...", num_servers)

                start = timer()
                ping_attempts = self.settings.get_ping_attempts()  # We are going to be multiprocessing within a class instance, so this needs getting outside of the multiprocessing
                valid_protocols = self.settings.get_protocols()
                valid_categories = self.settings.get_categories()
                best_servers, num_success = benchmarking.get_best_servers(valid_server_list, ping_attempts, valid_protocols, valid_categories, slow_mode)

                end = timer()

                if num_success == 0:
                    self.logger.error("Benchmarking failed to test any servers. Your network may be blocking large-scale ICMP requests. Exiting.")
                    sys.exit(1)
                else:
                    percent_success = round(num_success / num_servers * 100, 2)
                    self.logger.info("Benchmarked %i servers successfully (%0.2f%%). Took %0.2f seconds.", num_success, percent_success, end - start)

                    if percent_success < 90.0:
                        self.logger.warning("A large quantity of tests failed. Your network may be unreliable, or blocking large-scale ICMP requests. Syncing in slow mode (-s) may fix this.")

                # remove all old connections and any auto-connect, until a better sync routine is added
                if self.remove_active_connections():
                    updated = True
                if networkmanager.remove_autoconnect():
                    updated = True

                self.logger.info("Adding new connections...")

                new_connections = 0
                for key in best_servers.keys():
                    imported = True
                    name = best_servers[key]['name']

                    if not self.connection_exists(name):
                        domain = best_servers[key]['domain']
                        protocol = key[2]

                        file_path = self.get_ovpn_path(domain, protocol)
                        if file_path:
                            if networkmanager.import_connection(file_path, name, username, password, dns_list):
                                updated = True
                                new_connections += 1
                            else:
                                imported = False
                        else:
                            self.logger.warning("Could not find a configuration file for %s. Skipping.", name)

                    # If the connection already existed, or the import was successful, add the server combination to the active servers
                    if imported:
                        self.active_servers[key] = best_servers[key]
                        self.save_active_servers(self.active_servers, paths.ACTIVE_SERVERS)

                if new_connections > 0:
                    self.logger.info("%i new connections added.", new_connections)
                else:
                    self.logger.info("No new connections added.")

                return updated
            else:
                self.logger.error("No servers found matching your settings. Review your settings and try again.")
                sys.exit(1)
        else:
            self.logger.error("Could not fetch the server list from NordVPN. Check your Internet connectivity.")
            sys.exit(1)
Exemple #4
0
class NordNM(object):
    def __init__(self):
        parser = argparse.ArgumentParser()
        parser.add_argument(
            '-u',
            '--update',
            help='Download the latest OpenVPN configurations from NordVPN',
            action='store_true')
        parser.add_argument(
            '-s',
            '--sync',
            help=
            "Synchronise the optimal servers (based on load and latency) to NetworkManager",
            action="store_true")
        parser.add_argument(
            '-a',
            '--auto-connect',
            nargs=3,
            metavar=('[COUNTRY_CODE]', '[VPN_CATEGORY]', '[PROTOCOL]'),
            help=
            'Configure NetworkManager to auto-connect to the chosen server type. Takes country code, category and protocol'
        )
        parser.add_argument(
            '-k',
            '--kill-switch',
            help=
            'Sets a network kill-switch, to disable the active network interface when an active VPN connection disconnects',
            action='store_true')
        parser.add_argument(
            '-p',
            '--purge',
            help=
            'Remove all active connections, auto-connect and kill-switch (if configured)',
            action='store_true')
        parser.add_argument('--countries',
                            help='Display a list of the available countries',
                            action='store_true')
        parser.add_argument(
            '--categories',
            help='Display a list of the available VPN categories',
            action='store_true')
        parser.add_argument('--credentials',
                            help='Change the existing saved credentials',
                            action='store_true')
        parser.add_argument('--settings',
                            help='Change the existing saved settings',
                            action='store_true')

        try:
            args = parser.parse_args()
        except Exception:
            sys.exit(1)

        self.logger = logging.getLogger(__name__)

        # Count the number of arguments provided
        arg_count = 0
        for arg in vars(args):
            if getattr(args, arg):
                arg_count += 1

        if args.purge and arg_count > 1:
            print("Error: --purge should be used on its own.")
            sys.exit(1)
        elif args.categories and arg_count == 1:
            self.print_categories()
        elif args.countries and arg_count == 1:
            self.print_countries()
        elif args.credentials or args.settings or args.update or args.sync or args.purge or args.auto_connect or args.kill_switch:
            self.print_splash()

            self.run(args.credentials, args.settings, args.update, args.sync,
                     args.purge, args.auto_connect, args.kill_switch)
        else:
            parser.print_help()

    def print_splash(self):
        version_string = __version__

        latest_version = utils.get_pypi_package_version(__package__)
        if latest_version and version_string != latest_version:  # There's a new version on PyPi
            version_string = version_string + " (v" + latest_version + " available!)"
        elif latest_version and version_string == latest_version:
            version_string = version_string + " (Latest)"

        print("""     _   _               _ _   _ ___  ___
    | \ | |             | | \ | ||  \/  |
    |  \| | ___  _ __ __| |  \| || .  . |
    | . ` |/ _ \| '__/ _` | . ` || |\/| |
    | |\  | (_) | | | (_| | |\  || |  | |
    \_| \_/\___/|_|  \__,_\_| \_/\_|  |_/   v%s\n""" % version_string)

    def print_categories(self):
        for long_name, short_name in nordapi.VPN_CATEGORIES.items():
            print("%-9s (%s)" % (short_name, long_name))

    def print_countries(self):
        servers = nordapi.get_server_list(sort_by_country=True)
        if servers:
            format_string = "| %-14s | %-4s |"
            countries = []

            print(
                "\n Note: You must use the country code, NOT the country name in this tool.\n"
            )
            print(format_string % ("NAME", "CODE"))
            print("|----------------+------|")

            for server in servers:
                country_code = server['flag']
                if country_code not in countries:
                    countries.append(country_code)
                    country_name = server['country']
                    print(format_string % (country_name, country_code))
        else:
            self.logger.error(
                "Could not get available countries from the NordVPN API.")

    def setup(self):
        self.create_directories()

        self.settings = SettingsHandler(paths.SETTINGS)
        self.credentials = CredentialsHandler(paths.CREDENTIALS)

        self.black_list = self.settings.get_blacklist()
        self.white_list = self.settings.get_whitelist()

        self.active_servers = {}
        if os.path.isfile(paths.ACTIVE_SERVERS):
            self.active_servers = self.load_active_servers(
                paths.ACTIVE_SERVERS)

    def run(self, credentials, settings, update, sync, purge, auto_connect,
            kill_switch):
        self.setup()

        if credentials:
            self.credentials.save_new_credentials()
        if settings:
            self.settings.save_new_settings()
        if update:
            self.get_configs()

        if sync:
            if self.sync_servers():
                networkmanager.reload_connections()
        elif purge:
            networkmanager.remove_autoconnect()
            networkmanager.remove_killswitch(paths.KILLSWITCH)

            self.purge_active_connections()

        if auto_connect:
            self.enable_auto_connect(auto_connect[0], auto_connect[1],
                                     auto_connect[2])

        if kill_switch:
            networkmanager.set_killswitch(paths.KILLSWITCH)

    def create_directories(self):
        if not os.path.exists(paths.DIR_ROOT):
            os.mkdir(paths.DIR_ROOT)
            utils.chown_path_to_user(paths.DIR_ROOT)

        if not os.path.exists(paths.DIR_OVPN):
            os.mkdir(paths.DIR_OVPN)
            utils.chown_path_to_user(paths.DIR_OVPN)

    def get_configs(self):
        self.logger.info(
            "Downloading latest NordVPN OpenVPN configuration files to '%s'." %
            paths.DIR_OVPN)

        configs = nordapi.get_configs()
        if configs:
            if not utils.extract_zip(configs, paths.DIR_OVPN):
                self.logger.error("Failed to extract configuration files")
        else:
            self.logger.error(
                "Failed to retrieve configuration files from NordVPN")

    def get_ovpn_path(self, domain, protocol):
        wildcard = domain + '.' + protocol + '*'
        ovpn_path = None

        try:
            for f in os.listdir(paths.DIR_OVPN):
                file_path = os.path.join(paths.DIR_OVPN, f)
                if os.path.isfile(file_path):
                    if fnmatch(f, wildcard):
                        ovpn_path = os.path.join(paths.DIR_OVPN, f)

        except Exception as ex:
            self.logger.error(ex)

        return ovpn_path

    def enable_auto_connect(self,
                            country_code,
                            category='normal',
                            protocol='tcp'):
        enabled = False
        selected_parameters = (country_code.upper(), category, protocol)

        if selected_parameters in self.active_servers:
            connection_name = self.active_servers[selected_parameters]['name']

            if networkmanager.set_auto_connect(connection_name):
                networkmanager.disconnect_active_vpn(self.active_servers)
                if networkmanager.enable_connection(connection_name):
                    enabled = True
        else:
            self.logger.error(
                "Auto-connect not activated: No active server found matching [%s, %s, %s].",
                country_code, category, protocol)

        return enabled

    def purge_active_connections(self):
        if self.active_servers:
            self.logger.info("Removing all active connections...")
            active_servers = copy.deepcopy(self.active_servers)
            for key in self.active_servers.keys():
                connection_name = self.active_servers[key]['name']
                if self.connection_exists(connection_name):
                    networkmanager.remove_connection(connection_name)

                del active_servers[key]
                self.save_active_servers(
                    active_servers, paths.ACTIVE_SERVERS
                )  # Save after every successful removal, in case importer is killed abruptly

            self.active_servers = active_servers

            return True
        else:
            self.logger.info("No active connections to remove.")

    def load_active_servers(self, path):
        try:
            with open(paths.ACTIVE_SERVERS, 'rb') as fp:
                active_servers = pickle.load(fp)
            return active_servers
        except Exception as ex:
            self.logger.error(ex)
            return None

    def save_active_servers(self, active_servers, path):
        try:
            with open(paths.ACTIVE_SERVERS, 'wb') as fp:
                pickle.dump(active_servers, fp)
            utils.chown_path_to_user(paths.ACTIVE_SERVERS)
        except Exception as ex:
            self.logger.error(ex)

    def country_is_selected(self, country_code):
        # If (there is a whitelist and the country code is whitelisted) or (there is no whitelist, but there is a blacklist and it's not in the blacklist) or (there is no whitelist or blacklist)
        if (self.white_list and country_code in self.white_list) or (
                not self.white_list and self.black_list and country_code
                not in self.black_list) or (not self.white_list
                                            and not self.black_list):
            return True
        else:
            return False

    def has_valid_categories(self, server):
        valid_categories = self.settings.get_categories()

        # If the server has a category that is valid, return true
        for category in server['categories']:
            if category['name'] in valid_categories:
                return True

        return False

    def has_valid_protocol(self, server):
        valid_protocols = self.settings.get_protocols()
        has_openvpn_tcp = server['features']['openvpn_tcp']
        has_openvpn_udp = server['features']['openvpn_udp']

        if ('tcp' in valid_protocols
                and has_openvpn_tcp) or ('udp' in valid_protocols
                                         and has_openvpn_udp):
            return True
        else:
            return False

    def get_valid_servers(self, servers):
        valid_server_list = []

        for server in servers:
            country_code = server['flag']

            # If the server country has been selected, it has a selected protocol and selected categories
            if self.country_is_selected(
                    country_code) and self.has_valid_protocol(
                        server) and self.has_valid_categories(server):
                valid_server_list.append(server)

        return valid_server_list

    def connection_exists(self, connection_name):
        vpn_connections = networkmanager.get_vpn_connections()

        if vpn_connections and connection_name in vpn_connections:
            return True
        else:
            return False

    def configs_exist(self):
        configs = os.listdir(paths.DIR_OVPN)
        if configs:
            return True
        else:
            return False

    def sync_servers(self):
        updated = False

        username = self.credentials.get_username()
        password = self.credentials.get_password()
        dns_list = nordapi.get_nameservers()

        self.logger.info("Checking for new connections to import...")

        if self.configs_exist():

            server_list = nordapi.get_server_list(sort_by_load=True)
            if server_list:

                valid_server_list = self.get_valid_servers(server_list)
                if valid_server_list:

                    # If there's a kill-switch in place, we need to temporarily remove it, otherwise it will kill out network when disabling an active VPN below
                    # Disconnect active Nord VPNs, so we get a more reliable benchmark
                    show_warning = False
                    if networkmanager.remove_killswitch(paths.KILLSWITCH):
                        show_warning = True
                        warning_string = "Kill-switch"
                    if networkmanager.disconnect_active_vpn(
                            self.active_servers):
                        if show_warning:
                            warning_string = "Active VPN(s) and " + warning_string
                        else:
                            show_warning = True
                            warning_string = "Active VPN(s)"

                    if show_warning:
                        self.logger.warning(
                            "%s disabled for accurate benchmarking. Your connection is not secure until these are re-enabled.",
                            warning_string)

                    self.logger.info("Benchmarking servers...")

                    start = timer()
                    ping_attempts = self.settings.get_ping_attempts(
                    )  # We are going to be multiprocessing within a class instance, so this needs getting outside of the multiprocessing
                    valid_protocols = self.settings.get_protocols()
                    best_servers = benchmarking.get_best_servers(
                        valid_server_list, ping_attempts, valid_protocols)

                    end = timer()
                    self.logger.info(
                        "Benchmarking complete. Took %0.2f seconds.",
                        end - start)

                    # Purge all old connections and any auto-connect, until a better sync routine is added
                    if self.purge_active_connections():
                        updated = True
                    if networkmanager.remove_autoconnect():
                        updated = True

                    self.logger.info("Adding new connections...")

                    new_connections = 0
                    for key in best_servers.keys():
                        imported = True
                        name = best_servers[key]['name']

                        if not self.connection_exists(name):
                            domain = best_servers[key]['domain']
                            protocol = key[2]

                            file_path = self.get_ovpn_path(domain, protocol)
                            if file_path:
                                if networkmanager.import_connection(
                                        file_path, name, username, password,
                                        dns_list):
                                    updated = True
                                    new_connections += 1
                                else:
                                    imported = False
                            else:
                                self.logger.warning(
                                    "Could not find a configuration file for %s. Skipping.",
                                    name)

                        # If the connection already existed, or the import was successful, add the server combination to the active servers
                        if imported:
                            self.active_servers[key] = best_servers[key]
                            self.save_active_servers(self.active_servers,
                                                     paths.ACTIVE_SERVERS)

                    if new_connections > 0:
                        self.logger.info("%i new connections added.",
                                         new_connections)
                    else:
                        self.logger.info("No new connections added.")

                    return updated
                else:
                    self.logger.error(
                        "No servers found matching your settings. Review your settings and try again."
                    )
                    sys.exit(1)
            else:
                self.logger.error(
                    "Could not fetch the server list from NordVPN. Check your Internet connectivity."
                )
                sys.exit(1)
        else:
            self.logger.error(
                "Can't find any OpenVPN configuration files. Please run --update before syncing."
            )
            sys.exit(1)
Exemple #5
0
class NordNM(object):
    def __init__(self):
        self.ensure_config_dirs()
        self._active_servers = None
        self.settings = SettingsHandler(paths.SETTINGS)
        self.credentials = CredentialsHandler(paths.CREDENTIALS)
        self.country_blacklist = self.settings.get_blacklist()
        self.country_whitelist = self.settings.get_whitelist()
        self._config_info = None

    @property
    def config_info(self):
        if not self._config_info:
            with open(paths.CONFIG_INFO, 'r') as f:
                self._config_info = f.read().replace('\n', '')
        return self._config_info

    @config_info.setter
    def config_info(self, value):
        with open(paths.CONFIG_INFO, 'w') as f:
            f.write(value)
        self._config_info = value

    @property
    def active_servers(self):
        if self._active_servers:
            return self._active_servers
        try:
            with open(paths.ACTIVE_SERVERS, 'rb') as fp:
                self._active_servers = pickle.load(fp)
        except FileNotFoundError:
            log.error(
                'No active servers found, use sync command to setup servers')
        return self._active_servers

    @active_servers.setter
    def active_servers(self, value):
        with open(paths.ACTIVE_SERVERS, 'wb') as fp:
            pickle.dump(value, fp)
        utils.chown_path_to_user(paths.ACTIVE_SERVERS)

    def delete_configs(self):
        for f in os.listdir(paths.OVPN_CONFIGS):
            file_path = os.path.join(paths.OVPN_CONFIGS, f)
            if os.path.isfile(file_path):
                os.unlink(file_path)
            elif os.path.isdir(file_path):
                shutil.rmtree(file_path)

    def download_configs(self):
        log.info(
            f"Downloading latest NordVPN OpenVPN configuration files to '{paths.OVPN_CONFIGS}'."
        )
        try:
            zip_file, etag = nordapi.get_ovpn_configs()
        except ValueError:
            log.info("Configuration files already up-to-date.")
            return
        self.delete_configs()
        utils.extract_zip(zip_file, paths.OVPN_CONFIGS)
        self.config_info = etag

    def sync(self, preserve_vpn=False):
        # remove legacy files
        if self.sync_servers(preserve_vpn):
            networkmanager.reload_connections()

    def ensure_config_dirs(self):
        try:
            os.mkdir(paths.ROOT)
            utils.chown_path_to_user(paths.ROOT)
            os.mkdir(paths.OVPN_CONFIGS)
            utils.chown_path_to_user(paths.OVPN_CONFIGS)
        except FileExistsError:
            pass

    def get_ovpn_path(self, domain, protocol):
        files = glob.glob(paths.OVPN_CONFIGS + '/**/' + domain + '.' +
                          protocol + '*.ovpn')
        return (files or [None])[0]

    def enable_auto_connect(self,
                            country_code: str,
                            category: str = 'normal',
                            protocol: str = 'tcp'):
        enabled = False
        selected_parameters = (country_code.upper(), category, protocol)

        if selected_parameters in self.active_servers:
            connection_name = self.active_servers[selected_parameters]['name']
            connection_load = self.active_servers[selected_parameters]['load']
            connection_latency = self.active_servers[selected_parameters][
                'latency']

            if networkmanager.set_auto_connect(connection_name):
                log.info(
                    f"Auto-connect enabled for '{connection_name}' "
                    f"(Load: {connection_load}%, Latency: {connection_latency:.2f})."
                )
                networkmanager.disconnect_active_vpn(self.active_servers)
                if networkmanager.enable_connection(connection_name):
                    enabled = True
        else:
            log.error(f"Auto-connect not activated: No active server found "
                      f"matching [{country_code}, {category}, {protocol}].")

        return enabled

    def is_valid_server(self, server):
        def server_has_valid_country():
            country_code = server['flag']
            if not self.country_blacklist and not self.country_whitelist:
                return True
            if self.country_whitelist and country_code and self.country_whitelist:
                return True
            if not self.country_whitelist and self.country_blacklist and country_code not in self.country_blacklist:
                return True
            return False

        def server_has_valid_categories():
            valid_categories = self.settings.get_categories()

            # If the server has a category that is valid, return true
            for category in server['categories']:
                if category['name'] in valid_categories:
                    return True

            return False

        def server_has_valid_protocol():
            valid_protocols = self.settings.get_protocols()
            has_openvpn_tcp = server['features']['openvpn_tcp']
            has_openvpn_udp = server['features']['openvpn_udp']

            if ('tcp' in valid_protocols
                    and has_openvpn_tcp) or ('udp' in valid_protocols
                                             and has_openvpn_udp):
                return True
            else:
                return False

        return server_has_valid_country() and server_has_valid_protocol(
        ) and server_has_valid_categories()

    def connection_exists(self, connection_name):
        return connection_name in networkmanager.get_vpn_connections()

    def configs_exist(self):
        return bool(os.listdir(paths.OVPN_CONFIGS))

    def get_best_servers(self, servers):
        log.info("Benchmarking servers...")
        start = timer()
        best_servers = benchmarking.get_best_servers(
            servers, self.settings.get_ping_attempts(),
            self.settings.get_protocols())
        end = timer()
        log.info(f"Benchmarking complete. Took {end - start:.2f} seconds.")
        return best_servers

    def sync_servers(self, preserve_vpn):
        # remove legacy
        for file_path in paths.LEGACY_FILES:
            try:
                os.remove(file_path)
            except FileNotFoundError:
                pass
        log.info("Checking for new connections to import...")
        server_list = nordapi.get_server_list(sort_by_load=True)
        if not server_list:
            log.error(
                "Could not fetch the server list from NordVPN. Check your Internet connectivity."
            )
            sys.exit(1)
        server_list = [s for s in server_list if self.is_valid_server(s)]
        if not server_list:
            log.error(
                "No servers found matching your settings. Review your settings and try again."
            )
            sys.exit(1)
        if preserve_vpn:
            log.warning(
                "Active VPN preserved. This may give unreliable results!")
        else:
            # If there's a kill-switch in place, we need to temporarily remove it,
            # otherwise it will kill out network when disabling an active VPN below
            # Disconnect active Nord VPNs, so we get a more reliable benchmark
            warnings = {
                'Kill-switch':
                networkmanager.remove_killswitch(),
                'Active VPN(s)':
                networkmanager.disconnect_active_vpn(self.active_servers),
            }
            if any(warnings.values()):
                log.warning(
                    f"{', '.join(warnings.keys())} disabled for accurate benchmarking. "
                    f"Your connection is not secure until these are re-enabled."
                )

        # remove all old connections and any auto-connect, until a better sync routine is added
        self.active_servers = {}
        networkmanager.remove_autoconnect()

        log.info("Adding new connections...")
        new_servers = {}
        for key, server in self.get_best_servers(server_list).items():
            if self.connection_exists(server['name']):
                new_servers[key] = server
                continue
            file_path = self.get_ovpn_path(server['domain'], key[2])
            if not file_path:
                log.warning(
                    f"Could not find a configuration file for {server['name']}. Skipping."
                )
                continue
            networkmanager.import_connection(file_path, server['name'],
                                             self.credentials.get_username(),
                                             self.credentials.get_password(),
                                             nordapi.get_nameservers())
            new_servers[key] = server
        if len(new_servers) > 0:
            self.active_servers = {**self.active_servers, **new_servers}
            log.info(f"{len(new_servers)} new connections added.")
        else:
            log.info("No new connections added.")
Exemple #6
0
def settings():
    SettingsHandler(paths.SETTINGS).save_new_settings()