diff --git a/CHANGELOG.md b/CHANGELOG.md index e73c294..6e44331 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ ## Changelog +version 0.8.2: +- [new] added AzireVPN +- [new] option to specify key for Airvpn +- [change] fast/random option added to profiles +- [change] order of latency checks now based on previous results +- [change] tidied up directory structure +- [change] checks if ipv6 is available +- [change] auto-updates for different providers won't run concurrently +- [change] check if IPv6 is available before setting iptables rules +- [change] don't generate new WireGuard keys on auto-update +- [change] allow importing new servers if firewall is active but VPN is not +- [bugfix] firewall not deactivating after gui exit (if the respective option is set) +- [bugfix] detection of service crashes is not reliable +- [bugfix] Windscribe auto-update fails because of authentication error +- [bugfix] compatibility with older Qt5 versions +- [bugfix] previous iptables rules not always restored +- [bugfix] crashes if ipv6 stack not available +- [bugfix] OpenVPN config changes overwritten on update +- [bugfix] Debian packages remove /usr/share/qomui directory on update + +##### Additional notes: +- Re-importing config files from supported providers is strongly recommended as they are now saved in a different location. +- If you are using Debian/Ubuntu packages Qomui must be removed (sudo apt purge qomui) and then installed again due to a bug in the postrm script. + version 0.8.1: - [change] option to restart qomui-service from gui if it crashes - [change] added exceptions for all DBus calls diff --git a/README.md b/README.md index b89892a..5d1a886 100755 --- a/README.md +++ b/README.md @@ -4,18 +4,15 @@ Qomui (Qt OpenVPN Management UI) is an easy-to-use OpenVPN/WireGuard gui for GNU/Linux with some unique features such as provider-independent support for double-hop connections. Qomui supports multiple providers with added convenience when using AirVPN, PIA, ProtonVPN, Windscribe or Mullvad. ### Features -- should work with all VPN providers that offer OpenVPN config files -- automatic download function for Mullvad, Private Internet Access, Windscribe, ProtonVPN and AirVPN -- support for OpenVPN over SSL and SSH for AirVPN and OpenVPN over SSL for Windscribe (Stealth Mode) +- works with all VPN providers that offer OpenVPN/WireGuard config files +- easy-to-use gui written in PyQt5 +- automatic download function for Mullvad, Private Internet Access, Windscribe, ProtonVPN and AirVPN including support for OpenVPN over SSL and SSH for AirVPN and OpenVPN over SSL for Windscribe (Stealth Mode) - allows double-hop VPN connections (VPN chains) between different providers -- gui written in PyQt including option to minimize application to system tray +- killswitch & leak protection via an iptables-based, configurable firewall that blocks all outgoing network traffic in case the VPN connection breaks down +- provides the possibility to allow applications to bypass the VPN tunnel, open a second VPN tunnel or use the VPN only for specific applications +- supports WireGuard +- command-line interface - security-conscious separation of the gui and a D-Bus service that handles commands that require root privileges -- protection against DNS leaks/ipv6 leaks -- iptables-based, configurable firewall that blocks all outgoing network traffic in case the VPN connection breaks down -- allow applications to bypass the VPN tunnel, open a second VPN tunnel or use the VPN only for specific applications -- experimental support for WireGuard -- command-line interface -- automatic weekly updates of server configurations for supported providers - experimental ### Screenshots Screenshots were taken on Arch Linux/Plasma Arc Dark Theme - Qomui will adapt to your theme.
@@ -43,9 +40,22 @@ Qomui contains two components: qomui-gui and qomui-service (and qomui-cli: see b Current configurations for AirVPN, Mullvad, ProtonVPN, PIA and Windscribe can be automatically downloaded via the provider tab. Qomui will update these once a week if you choose to enable the respective setting in the options tab. For all other providers you can conveniently add a config file folder. Qomui will automatically resolve host names, determine the location of servers (using geoip-database) and save your username and password (in a file readable only by root). -Once you added server configurations, you can browse and filter them in the server tab. Furthermore, you can mark servers as favourites and connect to one of them randomly. To see a list of all favourited servers click on the star in the upper right. - -### Firewall - Network lock +Once you added server configurations, you can browse and filter them in the server tab. Furthermore, you can mark servers as favourites and connect to one of them randomly. To see a list of all favourited servers click on the star in the upper right. There is also an option to create connection profiles in the respective tab. Profiles will select a server automatically based on the criteria you set. Criteria include protocol (OpenVPN or WireGuard), countries, providers and one of the following selection modes: +- ***Random:*** Chooses a random server among all servers matching the profile +- ***Fastest:*** Chooses the fastest server matching the profile based on latency. For this option to work properly the "Perform latency checks" option needs to be ticked. +- ***Fast/Random:*** Chooses a random server among the fastest twenty percent. If you profile includes more than one country, the algorithm also increases the chance to select a server from a different country next. + +### Options +- ***Autoconnect/reconnect:*** Automatically connect to the last server/last profile once a new internet connection has been detected or after the OpenVPN process has died unexpectedly. +- ***Start minimized:*** Hides the application window on startup. This only works if your desktop environment supports tray icons. +- ***Auto-update:*** Updates server configurations for supported providers automatically every five days. +- ***Perform latency check:*** Checks server latency and sorts servers accordingly. +- ***Disable IPv6:*** Completely disables the IPv6 stack systemwide. This is not recommended unless you know what you are doing. +- ***Allow OpenVPN bypass:*** See bypass section below. +- ***Activate Firewall:*** See firewall section below. +- ***Alternative DNS Servers***: Enforces the usage of custom DNS servers instead of those by your provider. The DNS servers set here will also be used for bypass mode if you don't launch a secondary VPN tunnel. + +### Firewall (Killswitch) It is highly recommended to activate the firewall to prevent against ipv6 and DNS leaks. By default, once qomui-service has been started, all internet connectivity outside the VPN tunnel will be blocked whether or not the gui is running. Hence, your system will be always protected if you enable qomui-service via systemd. Depending on your distribution, it might be necessary to disable preinstalled firewall services such as ufw or firewalld to avoid conflicts. Alternatively, the "Edit firewall" dialog in the options tab offers a setting to enable/disable the firewall only if you start/quit the gui. You can also add custom iptables rules there. ### Double-Hop @@ -67,7 +77,7 @@ The bypass feature also allows you to open a second OpenVPN tunnel (this does cu You can add WireGuard config files from any provider as easily as OpenVPN files. WireGuard configs for Mullvad are now downloaded automatically alongside their OpenVPN configs as long as WireGuard is installed. If you choose to manually import WireGuard config files, Qomui will automatically recognize the type of file. As of now, WireGuard will not be installed automatically with DEB and RPM packages. You can find the official installation guidelines for different distributions [here](https://www.wireguard.com/install/). ### Cli -The cli interface is still experimental and missing some features, e.g. automatic reconnects. Avoid using the cli and the Gui concurrently. +The cli interface is still experimental and missing some features, e.g. automatic reconnects. Avoid using the cli and the gui concurrently. #### Example usage @@ -97,23 +107,33 @@ Qomui has been my first ever programming experience and a practical challenge fo ### Changelog +#### version 0.8.2: +- [new] added AzireVPN +- [new] option to specify key for Airvpn +- [change] fast/random option added to profiles +- [change] order of latency checks now based on previous results +- [change] tidied up directory structure +- [change] checks if ipv6 is available +- [change] auto-updates for different providers won't run concurrently +- [change] check if IPv6 is available before setting iptables rules +- [change] don't generate new WireGuard keys on auto-update +- [change] allow importing new servers if firewall is active but VPN is not +- [bugfix] firewall not deactivating after gui exit (if the respective option is set) +- [bugfix] detection of service crashes is not reliable +- [bugfix] Windscribe auto-update fails because of authentication error +- [bugfix] compatibility with older Qt5 versions +- [bugfix] previous iptables rules not always restored +- [bugfix] crashes if ipv6 stack not available +- [bugfix] OpenVPN config changes overwritten on update +- [bugfix] Debian packages remove /usr/share/qomui directory on update + +##### Additional notes: +- Re-importing config files from supported providers is strongly recommended as they are now saved in a different location. +- If you are using Debian/Ubuntu packages Qomui must be removed (sudo apt purge qomui) and then installed again due to a bug in the postrm script. + #### version 0.8.1: - [change] option to restart qomui-service from gui if it crashes - [change] added exceptions for all DBus calls - [change] improved support for non-systemd distributions - [change] detecting and closing simultaneously running instances - [bugfix] Airvpn auto-download fixed - -#### version 0.8.0: -- [new] connection profiles -- [new] support for custom scripts -- [change] configurations for Airvpn are now downloaded directly -- [change] removed minimize option if system tray not available -- [change] download new Mullvad config/certificates -- [change] added scroll areas to some tabs -- [change] added options for profiles to tray menu -- [change] window state now recognized correctly -- [bugfix] improved stability and reliability of network detection -- [bugfix] manually imported WireGuard servers don't connect -- [bugfix] Qomui crashes when downloading Airvpn configs -- [bugfix] fixed Mullvad & Windscribe configs diff --git a/VERSION b/VERSION index c18d72b..53a48a1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.1 \ No newline at end of file +0.8.2 \ No newline at end of file diff --git a/qomui/bypass.py b/qomui/bypass.py index 23def17..bb90b19 100755 --- a/qomui/bypass.py +++ b/qomui/bypass.py @@ -45,12 +45,9 @@ def create_cgroup(user, group, interface, gw=None, gw_6=None, default_int=None) rt_tables.write("11 bypass_qomui\n") logging.debug("Bypass: Created new routing table") - for rule in cgroup_iptables: - firewall.add_rule(rule) - + firewall.batch_rule(cgroup_iptables) if gw_6 != "None" and default_int == interface: - for rule in cgroup_iptables: - firewall.add_rule_6(rule) + firewall.batch_rule_6(cgroup_iptables) else: logging.debug("Blocking ipv6 via bypass_qomui") @@ -61,9 +58,7 @@ def create_cgroup(user, group, interface, gw=None, gw_6=None, default_int=None) cgroup_iptables.insert(2, ["-I", "OUTPUT", "1", "-m", "cgroup", "--cgroup", "0x00110011", "-j", "DROP"]) cgroup_iptables.pop(3) cgroup_iptables.insert(3, ["-I", "INPUT", "1", "-m", "cgroup", "--cgroup", "0x00110011", "-j", "DROP"]) - - for rule in cgroup_iptables: - firewall.add_rule_6(rule) + firewall.batch_rule_6(cgroup_iptables) try: check_call(["ip", "rule", "add", "fwmark", "11", "table", "bypass_qomui"]) @@ -126,11 +121,8 @@ def delete_cgroup(interface): except CalledProcessError: pass - for rule in cgroup_iptables_del: - firewall.add_rule(rule) - - for rule in cgroup_iptables_del: - firewall.add_rule_6(rule) + firewall.batch_rule(cgroup_iptables_del) + firewall.batch_rule_6(cgroup_iptables_del) try: os.rmdir(cgroup_path) @@ -152,8 +144,7 @@ def set_bypass_vpn(interface, interface_cmd, tun, tun_cmd): "-o", interface, "-j", "MASQUERADE" ]] - for rule in postroutes: - firewall.add_rule(rule) - firewall.add_rule_6(rule) + firewall.batch_rule(postroutes) + firewall.batch_rule_6(postroutes) diff --git a/qomui/dns_manager.py b/qomui/dns_manager.py index 851517e..2d149db 100755 --- a/qomui/dns_manager.py +++ b/qomui/dns_manager.py @@ -56,12 +56,12 @@ def dns_request_exception(action, dns_1, dns_2, port): logging.info("iptables: removing exception for DNS requests") for p in protocols: - rules.append([action, 'OUTPUT', '1', '-p', p, '--dport', port, '-j', 'ACCEPT']) - rules.append([action, 'INPUT', '1', '-p', p, '--sport', port, '-j', 'ACCEPT']) + rules.append([action, 'OUTPUT', '-p', p, '--dport', port, '-j', 'ACCEPT']) + rules.append([action, 'INPUT', '-p', p, '--sport', port, '-j', 'ACCEPT']) for rule in rules: firewall.add_rule(rule) - firewall.add_rule_6(rule) + firewall.add_rule(rule, ipt="ip6") #set_dns(dns_1, server_2=dns_2) diff --git a/qomui/firewall.py b/qomui/firewall.py index df0d1b6..f6103e1 100755 --- a/qomui/firewall.py +++ b/qomui/firewall.py @@ -9,145 +9,130 @@ ROOTDIR = "/usr/share/qomui" saved_rules = [] saved_rules_6 = [] -ip_cmd = ["iptables", "--wait",] -ip6_cmd = ["ip6tables", "--wait",] +#ip_cmd = ["iptables", "--wait",] +#ip6_cmd = ["ip6tables", "--wait",] devnull = open(os.devnull, 'w') +ip6_available = True + -def add_rule(rule): - a = 1 - try: - check = rule[:] - if "-D" not in check: - if check[0] == "-A": - check[0] = "-C" - elif check[0] == "-I": - check[0] = "-C" - check.pop(2) - elif check[2] == "-A": - check[2] = "-C" - check_call(ip_cmd + check, stdout=devnull, stderr=devnull) - logging.debug("iptables: {} already exists".format(rule)) - a = 0 - except (IndexError, CalledProcessError): - pass +def check_ipv6(): try: - if a == 1: - check_call(ip_cmd + rule, stdout=devnull, stderr=devnull) - logging.debug("iptables: applied {}".format(rule)) + ipv6_info = open("/proc/net/if_inet6" , "r").read() + if ipv6_info: + return True - except CalledProcessError: - if "-D" not in rule: - logging.warning("iptables: failed to apply {}".format(rule)) + else: + logging.info("ipv6 stack not available") + return False -def add_rule_6(rule): - a = 1 - try: - check = rule[:] - if "-D" not in check: - if check[0] == "-A": - check[0] = "-C" - elif check[0] == "-I": - check[0] = "-C" - check.pop(2) - elif check[2] == "-A": - check[2] = "-C" - check_call(ip6_cmd + check, stdout=devnull, stderr=devnull) - logging.debug("ipt6ables: {} already exists".format(rule)) - a = 0 - except (IndexError, CalledProcessError): - pass + except (OSError, FileNotFoundError) as e: + logging.debug("Unable to determine whether ipv6 is available") + return True - try: - if a == 1: - check_call(ip6_cmd + rule, stdout=devnull, stderr=devnull) - logging.debug("ip6tables: applied {}".format(rule)) +def add_rule(rule, check=0, ipt="ip4"): + if ipt == "ip4": + ip_cmd = ["iptables", "--wait",] + else: + ip_cmd = ["ip6tables", "--wait",] - except CalledProcessError: - if "-D" not in rule: - logging.warning("ip6tables: failed to apply {}".format(rule)) + if ipt == "ip4" or check == 1 or check_ipv6() is True: + #check if rule already exists and only set it otherwise + try: + if len(rule) > 3 or "-D" not in rule: + check = ["-C" if x == "-A" or x == "-I" else x for x in rule] + check_call(ip_cmd + check, stdout=devnull, stderr=devnull) + logging.debug("iptables: {} already exists".format(rule)) -def apply_rules(opt, block_lan=0, preserve=0): - fw_rules = get_config() - save_existing_rules(fw_rules) - save_existing_rules_6(fw_rules) + else: + raise IndexError - for rule in fw_rules["flush"]: - add_rule(rule) + except (IndexError, CalledProcessError): + try: + check_call(ip_cmd + rule, stdout=devnull, stderr=devnull) + logging.debug("iptables: applied {}".format(rule)) - for rule in fw_rules["flushv6"]: - add_rule_6(rule) + except CalledProcessError: + logging.warning("iptables: failed to apply {}".format(rule)) - logging.info("iptables: flushed existing rules") +def apply_rules(opt, block_lan=0, preserve=0): + fw_rules = get_config() + if opt != 2: + batch_rule(fw_rules["flush"]) + batch_rule_6(fw_rules["flushv6"]) - for rule in saved_rules: - if preserve == 1: - add_rule(rule) + logging.info("iptables: flushed existing rules") - for rule in saved_rules_6: - if preserve == 1: - add_rule_6(rule) + if preserve == 1: + save_existing_rules(fw_rules) + save_existing_rules_6(fw_rules) + batch_rule(saved_rules) + batch_rule_6(saved_rules_6) if opt == 1: - for rule in fw_rules["defaults"]: - add_rule(rule) + batch_rule(fw_rules["defaults"]) + batch_rule_6(fw_rules["defaultsv6"]) if block_lan == 0: - for rule in fw_rules["ipv4local"]: - add_rule(rule) - - for rule in fw_rules["ipv6local"]: - add_rule_6(rule) - - for rule in fw_rules["defaultsv6"]: - add_rule_6(rule) - - for rule in fw_rules["ipv4rules"]: - add_rule(rule) - - for rule in fw_rules["ipv6rules"]: - add_rule_6(rule) + batch_rule(fw_rules["ipv4local"]) + batch_rule_6(fw_rules["ipv6local"]) + batch_rule(fw_rules["ipv4rules"]) + batch_rule_6(fw_rules["ipv6rules"]) logging.info("iptables: activated firewall") elif opt == 0: - for rule in fw_rules["unsecure"]: - add_rule(rule) - - for rule in fw_rules["unsecurev6"]: - add_rule_6(rule) - + batch_rule(fw_rules["unsecure"]) + batch_rule_6(fw_rules["unsecurev6"]) logging.info("iptables: deactivated firewall") +def batch_rule(rules): + for rule in rules: + add_rule(rule) + +def batch_rule_6(rules): + if check_ipv6() is True: + for rule in rules: + add_rule(rule, check=1, ipt="ip6") + def save_existing_rules(fw_rules): - existing_rules = check_output(["iptables", "-S"]).decode("utf-8") - for line in existing_rules.split('\n'): - rpl = line.replace("/32", "") - rule = shlex.split(rpl) - if len(rule) != 0: - match = 0 - omit = fw_rules["ipv4rules"] + fw_rules["flush"] + fw_rules["ipv4local"] - for x in omit: - if Counter(x) == Counter(rule): - match = 1 - if match == 0 and rule not in saved_rules: - saved_rules.append(rule) - match = 0 + try: + existing_rules = check_output(["iptables", "-S"]).decode("utf-8") + for line in existing_rules.split('\n'): + rpl = line.replace("/32", "") + rule = shlex.split(rpl) + if len(rule) != 0: + match = 0 + omit = fw_rules["ipv4rules"] + fw_rules["flush"] + fw_rules["ipv4local"] + for x in omit: + if Counter(x) == Counter(rule): + match = 1 + if match == 0 and rule not in saved_rules: + saved_rules.append(rule) + match = 0 + + except (CalledProcessError, FileNotFoundError) as e: + logging.error("ip4tables: Could not read active rules - {}".format(e)) def save_existing_rules_6(fw_rules): - existing_rules = check_output(["ip6tables", "-S"]).decode("utf-8") - for line in existing_rules.split('\n'): - rpl = line.replace("/32", "") - rule = shlex.split(rpl) - if len(rule) != 0: - match = 0 - omit = fw_rules["ipv6rules"] + fw_rules["flushv6"] + fw_rules["ipv6local"] - for x in omit: - if Counter(x) == Counter(rule): - match = 1 - if match == 0 and rule not in saved_rules_6: - saved_rules_6.append(rule) - match = 0 + if check_ipv6() is True: + try: + existing_rules = check_output(["ip6tables", "-S"]).decode("utf-8") + for line in existing_rules.split('\n'): + rpl = line.replace("/32", "") + rule = shlex.split(rpl) + if len(rule) != 0: + match = 0 + omit = fw_rules["ipv6rules"] + fw_rules["flushv6"] + fw_rules["ipv6local"] + for x in omit: + if Counter(x) == Counter(rule): + match = 1 + if match == 0 and rule not in saved_rules_6: + saved_rules_6.append(rule) + match = 0 + + except (CalledProcessError, FileNotFoundError) as e: + logging.error("ip6tables: Could not read active rules - {}".format(e)) def allow_dest_ip(ip, action): rule = [action, 'OUTPUT', '-d', ip, '-j', 'ACCEPT'] @@ -157,16 +142,17 @@ def allow_dest_ip(ip, action): add_rule(rule) elif len(ip.split(":")) >= 4: - add_rule_6(rule) + if check_ipv6() is True: + add_rule(rule, ipt="ip6") except: - pass + logging.error("{} is not a valid ip address".format(ip)) def get_config(): try: with open("{}/firewall.json".format(ROOTDIR), "r") as f: return json.load(f) except (FileNotFoundError, json.decoder.JSONDecodeError) as e: - logging.debug("Loading default firewall configuration") + logging.info("Loading default firewall configuration") try: with open("{}/firewall_default.json".format(ROOTDIR), "r") as f: return json.load(f) @@ -196,27 +182,46 @@ def check_firewall_services(): return detected_firewall def save_iptables(): - outfile = open("{}/iptables_before.rules".format(ROOTDIR), "w") - save = Popen(["iptables-save"], stdout=outfile, stderr=PIPE) - save.wait() - outfile.flush() + try: + outfile = open("{}/iptables_before.rules".format(ROOTDIR), "w") + save = Popen(["iptables-save"], stdout=outfile, stderr=PIPE) + save.wait() + outfile.flush() + logging.debug("Saved iptables rules") - if save.stderr: + except (CalledProcessError, FileNotFoundError): logging.debug("Failed to save current iptables rules") - else: - logging.debug("Saved iptables rule") + if check_ipv6() is True: + + try: + outfile6 = open("{}/ip6tables_before.rules".format(ROOTDIR), "w") + save6 = Popen(["ip6tables-save"], stdout=outfile, stderr=PIPE) + save6.wait() + outfile6.flush() + logging.debug("Saved ip6tables rules") + + except (CalledProcessError, FileNotFoundError): + logging.debug("Failed to save current ip6tables rules") def restore_iptables(): - infile = open("{}/iptables_before.rules".format(ROOTDIR), "r") - restore = Popen(["iptables-restore"], stdin=infile, stderr=PIPE) - save.wait() + try: + #infile = open("{}/iptables_before.rules".format(ROOTDIR), "r") + restore = Popen(["iptables-restore", "{}/iptables_before.rules".format(ROOTDIR)], stderr=PIPE) + logging.debug("Restored previous iptables rules") - if restore.stderr: - logging.debug("Failed to restore iptables rules") + except (CalledProcessError, FileNotFoundError): + logging.debug("FileNotFoundError: Failed to restore iptables rules") - else: - logging.debug("restored previous iptables rules") + if check_ipv6() is True: + + try: + #infile = open("{}/iptables_before.rules".format(ROOTDIR), "r") + restore = Popen(["ip6tables-restore", "{}/ip6tables_before.rules".format(ROOTDIR)], stderr=PIPE) + logging.debug("Restored previous ip6tables rules") + + except (CalledProcessError, FileNotFoundError): + logging.debug("FileNotFoundError: Failed to restore ip6tables rules") diff --git a/qomui/latency.py b/qomui/latency.py index 9779f51..8240df4 100755 --- a/qomui/latency.py +++ b/qomui/latency.py @@ -15,9 +15,15 @@ def __init__(self, server_dict, interface): self.server_dict = server_dict self.interface = interface + def sort_by_latency(self, server): + try: + return float(server[1]["latency"]) + except KeyError: + return 999 + def run(self): try: - for k,v in self.server_dict.items(): + for k,v in sorted(self.server_dict.items(), key=self.sort_by_latency): try: ip = v["ip"] except KeyError: diff --git a/qomui/monitor.py b/qomui/monitor.py old mode 100644 new mode 100755 diff --git a/qomui/profiles.py b/qomui/profiles.py index 6c3d9d1..d2d4d68 100755 --- a/qomui/profiles.py +++ b/qomui/profiles.py @@ -101,6 +101,7 @@ def popBoxes(self): self.modeBox.addItem("Random") self.modeBox.addItem("Fastest") + self.modeBox.addItem("Fast/Random") if self.selected != 0: self.countries_selected = self.selected["countries"] @@ -124,7 +125,7 @@ def popchoiceTable(self): if cols == 0: cols = 1 - n = n + len(self.providers) + n = n + len(self.providers) rows = int(n / cols) + (n % cols> 0) + int(np / cols) + (np % cols> 0) + 3 self.choiceTable.setRowCount(rows) self.choiceTable.setColumnCount(cols) diff --git a/qomui/qomui_gui.py b/qomui/qomui_gui.py index 943489c..b240bee 100755 --- a/qomui/qomui_gui.py +++ b/qomui/qomui_gui.py @@ -38,7 +38,7 @@ def _translate(context, text, disambig): ROOTDIR = "/usr/share/qomui" HOMEDIR = "{}/.qomui".format(os.path.expanduser("~")) -SUPPORTED_PROVIDERS = ["Airvpn", "Mullvad", "ProtonVPN", "PIA", "Windscribe"] +SUPPORTED_PROVIDERS = ["Airvpn", "AzireVPN", "Mullvad", "PIA", "ProtonVPN", "Windscribe"] JSON_FILE_LIST = [("config_dict", "{}/config.json".format(ROOTDIR)), ("server_dict", "{}/server.json".format(HOMEDIR)), ("protocol_dict", "{}/protocol.json".format(HOMEDIR)), @@ -74,6 +74,7 @@ class QomuiGui(QtWidgets.QWidget): bypass_dict = {} config_dict = {} packetmanager = None + queue = [] tunnel_list = ["OpenVPN", "WireGuard"] config_list = [ "firewall", @@ -86,7 +87,7 @@ class QomuiGui(QtWidgets.QWidget): "auto_update" ] - routes = { + routes = { "gateway" : "None", "gateway_6" : "None", "interface" : "None", @@ -104,7 +105,6 @@ def __init__(self, parent = None): try: self.qomui_dbus = self.dbus.get_object('org.qomui.service', '/org/qomui/service') - self.logger.debug('Successfully connected to qomui-service via DBus') except dbus.exceptions.DBusException: self.logger.error('DBus Error: Qomui-Service is currently not available') @@ -139,13 +139,13 @@ def __init__(self, parent = None): 600, 750 )) + self.logger.debug('Successfully connected to qomui-service via DBus') self.dbus_call("disconnect", "main") self.dbus_call("disconnect", "bypass") self.dbus_call("save_default_dns") self.check_other_instance() self.load_saved_files() self.systemtray() - self.net_mon_thread = monitor.NetMon() self.net_mon_thread.log.connect(self.log_from_thread) self.net_mon_thread.net_state_change.connect(self.network_change) @@ -170,34 +170,43 @@ def dbus_call(self, cmd, *args): return call except dbus.exceptions.DBusException as e: - self.logger.error("Dbus Error: {}".format(e)) - self.notify("Qomui: Dbus Error", "No reply from qomui-service. It may have crashed.", icon="Warning") - ret = self.messageBox( - "Error: Qomui-service is not available", - "Do you want restart it or quit Qomui?", - buttons = [ - ("Quit", "NoRole"), - ("Restart", "YesRole") - ], - icon = "Question" - ) + print(e) + if e.get_dbus_name() == "org.freedesktop.DBus.Error.ServiceUnknown": + self.notify("Qomui: Dbus Error", "No reply from qomui-service. It may have crashed.", icon="Warning") + ret = self.messageBox( + "Error: Qomui-service is not available", + "Do you want restart it or quit Qomui?", + buttons = [ + ("Quit", "NoRole"), + ("Restart", "YesRole") + ], + icon = "Question" + ) + + if ret == 0: + sys.exit(1) + + elif ret == 1: + self.initialize_service("restart") + time.sleep(3) - if ret == 0: - sys.exit(1) + try: + if self.config_dict["bypass"] == 1: + self.dbus_call("bypass", {**self.routes, **utils.get_user_group()}) - elif ret == 1: - self.initialize_service("restart") - time.sleep(3) + except KeyError: + pass - try: - if self.config_dict["bypass"] == 1: - self.dbus_call("bypass", {**self.routes, **utils.get_user_group()}) - - except KeyError: - pass - - retry = self.dbus_call(cmd, *args) - return retry + retry = self.dbus_call(cmd, *args) + return retry + + else: + self.logger.error("Dbus Error: {}".format(e)) + self.notify("Qomui: Dbus Error", "An error occured. See log for details", icon="Warning") + + except Exception as e: + self.logger.error("Dbus Error: {}".format(e)) + self.notify("Qomui: Dbus Error", "An unknown error occured. See log for details", icon="Warning") def initialize_service(self, *args): try: @@ -561,6 +570,9 @@ def setupUi(self, Form): self.addProviderPassEdit.setEchoMode(QtWidgets.QLineEdit.Password) self.addProviderPassEdit.setObjectName(_fromUtf8("addProviderPassEdit")) self.gridLayout_3.addWidget(self.addProviderPassEdit, 1, 0, 1, 2) + self.airvpnKeyEdit = QtWidgets.QLineEdit(self.providerTab) + self.airvpnKeyEdit.setObjectName(_fromUtf8("airvpnKeyEdit")) + self.gridLayout_3.addWidget(self.airvpnKeyEdit, 2, 0, 1, 2) self.verticalLayout_30.addLayout(self.gridLayout_3) self.delProviderLabel = QtWidgets.QLabel(self.providerTab) self.delProviderLabel.setFont(bold_font) @@ -689,7 +701,7 @@ def setupUi(self, Form): self.horizontalLayout_10.addWidget(self.delBypassAppBt) self.verticalLayout_8.addLayout(self.horizontalLayout_10) self.tabWidget.addWidget(self.bypassTab) - + self.aboutTab = QtWidgets.QWidget() self.aboutTab.setObjectName(_fromUtf8("aboutTab")) self.tabWidget.addWidget(self.aboutTab) @@ -962,7 +974,7 @@ def notify(self, header, text, icon="Question"): except (CalledProcessError, FileNotFoundError): self.logger.warning("Desktop notifications not available") - + def messageBox(self, header, text, buttons=[], icon="Question"): box = QtWidgets.QMessageBox(self) box.setText(header) @@ -1083,6 +1095,7 @@ def tab_switch(self): self.tabWidget.setCurrentIndex(3) elif button == "Provider": self.tabWidget.setCurrentIndex(4) + self.providerChosen() elif button == "Bypass": self.tabWidget.setCurrentIndex(5) self.bypassVpnBox.clear() @@ -1120,7 +1133,7 @@ def systemtray(self): def pop_tray_menu(self): self.trayMenu.clear() - self.visibility_action = QtWidgets.QAction() + self.visibility_action = QtWidgets.QAction(self) self.visibility_action.setText("Hide") self.trayMenu.addAction(self.visibility_action) self.trayMenu.addSeparator() @@ -1149,6 +1162,10 @@ def activate_window(self): def shutdown(self): self.tray.hide() self.kill() + self.disconnect_bypass() + self.dbus_call("load_firewall", 2) + with open ("{}/server.json".format(HOMEDIR), "w") as s: + json.dump(self.server_dict, s) sys.exit() def restoreUi(self, reason): @@ -1186,12 +1203,7 @@ def closeEvent(self, event): self.hide() elif ret == 0: - self.tray.hide() - self.kill() - self.disconnect_bypass() - self.dbus_call("load_firewall", 2) - with open ("{}/server.json".format(HOMEDIR), "w") as s: - json.dump(self.server_dict, s) + self.shutdown() self.exit_event.accept() def change_timeout(self): @@ -1235,12 +1247,12 @@ def connect_last_server(self): try: if self.ovpn_dict["random"] == "on": self.choose_random_server() - + except KeyError: if "profile" in self.ovpn_dict.keys(): self.connect_profile(self.ovpn_dict["profile"]) - + else: self.establish_connection(self.ovpn_dict) @@ -1454,11 +1466,13 @@ def providerChosen(self): "Airvpn" : ("Username", "Password"), "PIA" : ("Username", "Password"), "Windscribe" : ("Username", "Password"), + "AzireVPN" : ("Username", "Password"), "Mullvad" : ("Account Number", "N.A. - Leave empty"), "ProtonVPN" : ("OpenVPN username", "OpenVPN password") } if provider in SUPPORTED_PROVIDERS: + self.airvpnKeyEdit.setVisible(False) self.addProviderEdit.setVisible(False) self.addProviderUserEdit.setPlaceholderText(_translate("Form", p_txt[provider][0], None)) self.addProviderPassEdit.setPlaceholderText(_translate("Form", p_txt[provider][1], None)) @@ -1466,8 +1480,12 @@ def providerChosen(self): self.addProviderDownloadBt.setText(_translate("Form", "Update", None)) else: self.addProviderDownloadBt.setText(_translate("Form", "Download", None)) + if provider == "Airvpn": + self.airvpnKeyEdit.setPlaceholderText(_translate("Form", "Key/Device: Default", None)) + self.airvpnKeyEdit.setVisible(True) else: + self.airvpnKeyEdit.setVisible(False) self.addProviderEdit.setVisible(True) self.addProviderEdit.setPlaceholderText(_translate("Form", "Specify name of provider", @@ -1517,9 +1535,16 @@ def add_server_configs(self): "username" : username, "password" : password, "folderpath" : folderpath, - "homedir" : HOMEDIR + "homedir" : HOMEDIR, + "update" : "1" } + if provider == "Airvpn": + if self.airvpnKeyEdit.text() != "": + credentials["key"] = self.airvpnKeyEdit.text() + else: + credentials["key"] = "Default" + self.addProviderUserEdit.setText("") self.addProviderPassEdit.setText("") self.dbus_call("import_thread", credentials) @@ -1610,7 +1635,7 @@ def display_profile(self, number): getattr(self, "{}_widget".format(number)).connect_profile.connect(self.connect_profile) self.verticalLayout_58.insertWidget(0, getattr(self, "{}_widget".format(number))) name = self.profile_dict[number]["name"] - + def connect_profile(self, p): result = None profile = self.profile_dict[p] @@ -1644,6 +1669,32 @@ def connect_profile(self, p): fastest = lat result = s + elif profile["mode"] == "Fast/Random": + l_list = [] + s_list = [] + counter = {} + max_length = int(len(temp_list) * 0.20) + for s in temp_list: + country = self.server_dict[s]["country"] + try: + lat = float(self.server_dict[s]["latency"]) + except KeyError: + lat = 1000 + + bisect.insort(l_list, lat) + s_list.insert(l_list.index(lat), (s, country)) + + s_list = s_list[:max_length + 1] + from collections import Counter + occs = [v for k, v in Counter(e[1] for e in s_list).items()] + max_occ = sum(occs) / len(occs) + for s,c in s_list: + counter[c] = counter.get(c, 0) + 1 + if counter[c] <= max_occ: + s_list.remove((s,c)) + + result = random.choice(s_list)[0] + elif profile["mode"] == "Random": result = random.choice(temp_list) @@ -2427,31 +2478,38 @@ def timeout(self, tunnel, name): def update_check(self): QtWidgets.QApplication.restoreOverrideCursor() - for provider in SUPPORTED_PROVIDERS: - if provider in self.provider_list: - try: - get_last = self.config_dict["{}_last".format(provider)] - last_update = datetime.strptime(get_last, '%Y-%m-%d %H:%M:%S.%f') - time_now = datetime.utcnow() - delta = time_now.date() - last_update.date() - days_since = delta.days - self.logger.info("Last {} update: {} days ago".format(provider, days_since)) - - if days_since >= 5: - credentials = { - "provider" : provider, - "credentials" : "unknown", - "folderpath" : "None", - "homedir" : HOMEDIR - } - - if self.config_dict["auto_update"] == 1: - self.logger.info("Updating {}".format(provider)) - self.dbus_call("import_thread", credentials) + if not self.queue: + self.queue = [p for p in SUPPORTED_PROVIDERS if p in self.provider_list] - except KeyError: - self.logger.debug("Update timestamp for {} not found".format(provider)) + provider = self.queue[0] + + try: + get_last = self.config_dict["{}_last".format(provider)] + last_update = datetime.strptime(get_last, '%Y-%m-%d %H:%M:%S.%f') + time_now = datetime.utcnow() + delta = time_now.date() - last_update.date() + days_since = delta.days + self.logger.info("Last {} update: {} days ago".format(provider, days_since)) + + if days_since >= 5: + credentials = { + "provider" : provider, + "credentials" : "unknown", + "folderpath" : "None", + "homedir" : HOMEDIR, + "update" : "0" + } + + if self.config_dict["auto_update"] == 1: + self.logger.info("Updating {}".format(provider)) + self.dbus_call("import_thread", credentials) + + except KeyError: + self.logger.debug("Update timestamp for {} not found".format(provider)) + + finally: + self.queue.remove(provider) def reconnect(self): if self.tunnel_active == 1: diff --git a/qomui/qomui_service.py b/qomui/qomui_service.py index 446b12f..0db6f8b 100755 --- a/qomui/qomui_service.py +++ b/qomui/qomui_service.py @@ -24,10 +24,11 @@ from qomui import firewall, bypass, update, dns_manager, tunnel ROOTDIR = "/usr/share/qomui" +LOGDIR = "/usr/share/qomui/logs" OPATH = "/org/qomui/service" IFACE = "org.qomui.service" BUS_NAME = "org.qomui.service" -SUPPORTED_PROVIDERS = ["Airvpn", "Mullvad", "ProtonVPN", "PIA", "Windscribe"] +SUPPORTED_PROVIDERS = ["Airvpn", "AzireVPN", "Mullvad", "PIA", "ProtonVPN", "Windscribe"] class GuiLogHandler(logging.Handler): def __init__(self, send_log, parent=None): @@ -54,6 +55,10 @@ class QomuiDbus(dbus.service.Object): interface = "eth0" def __init__(self): + + if not os.path.exists(LOGDIR): + os.makedirs(LOGDIR) + self.sys_bus = dbus.SystemBus() self.bus_name = dbus.service.BusName(BUS_NAME, bus=self.sys_bus) dbus.service.Object.__init__(self, self.bus_name, OPATH) @@ -61,14 +66,14 @@ def __init__(self): self.gui_handler = GuiLogHandler(self.send_log) self.gui_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) self.logger.addHandler(self.gui_handler) - self.filehandler = logging.handlers.RotatingFileHandler("{}/qomui.log".format(ROOTDIR), - maxBytes=2*1024*1024, backupCount=1) + self.filehandler = logging.handlers.RotatingFileHandler("{}/qomui.log".format(LOGDIR), + maxBytes=2*1024*1024, backupCount=3) self.logger.addHandler(self.filehandler) self.filehandler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) self.logger.setLevel(logging.DEBUG) self.logger.info("Dbus-service successfully initialized") - - #Clean slate after (re-)starting + + #Clean slate after (re-)starting try: check_call(["killall", "openvpn"]) self.logger.debug("Killed all running instances of OpenVPN") @@ -149,7 +154,7 @@ def add_pid(self, pid): #get fw configuration - might be called from gui after config change @dbus.service.method(BUS_NAME, in_signature='i', out_signature='') - def load_firewall(self, activate): + def load_firewall(self, stage): try: with open('{}/config.json'.format(ROOTDIR), 'r') as c: self.config = json.load(c) @@ -161,55 +166,34 @@ def load_firewall(self, activate): try: self.logger.setLevel(self.config["log_level"].upper()) - - except KeyError: - pass - - try: - if self.config["fw_gui_only"] == 0: - activate = 1 - - except KeyError: - activate = 1 - - try: - if self.config["preserve_rules"] == 1: - preserve = 1 - else: - preserve = 0 - - except KeyError: - preserve = 0 - - try: - if self.config["block_lan"] == 1: - block_lan = 1 - else: - block_lan = 0 - - except KeyError: - block_lan = 0 - - try: - if activate == 1: - firewall.save_iptables() - firewall.apply_rules(self.config["firewall"], block_lan=block_lan, preserve=preserve) - - elif activate == 2: - if self.config["fw_gui_only"] == 1: - firewall.restore_iptables() - firewall.apply_rules(0, block_lan=0, preserve=preserve) - - try: - bypass.delete_cgroup(self.default_interface_4, self.default_interface_6) - - except AttributeError: - pass - self.disable_ipv6(self.config["ipv6_disable"]) - + fw = self.config["firewall"] + gui_only = self.config["fw_gui_only"] + block_lan=self.config["block_lan"] + preserve=self.config["preserve_rules"] + + if fw == 1 and gui_only == 0: + opt = 1 + elif gui_only == 1 and stage == 1: + firewall.save_iptables() + opt = fw + elif gui_only == 1 and stage == 2: + firewall.restore_iptables() + opt = 2 + elif fw == 0 and stage == 1: + opt = 0 + firewall.restore_iptables() + else: + opt = 2 + + if opt < 2: + firewall.apply_rules( + opt, + block_lan=block_lan, + preserve=preserve + ) except KeyError: - self.logger.warning('Could not read all values from config file') + self.logger.warning('Malformed config file') #default dns is always set to the alternative servers self.dns = self.config["alt_dns1"] @@ -262,13 +246,9 @@ def disconnect(self, env): ["-D", "INPUT", "-i", "wg_qomui", "-j", "ACCEPT"], ["-D", "OUTPUT", "-o", "wg_qomui", "-j", "ACCEPT"] ] - - for rule in wg_rules: - firewall.add_rule_6(rule) - firewall.add_rule(rule) - + firewall.batch_rule_6(wg_rules) + firewall.batch_rule(wg_rules) tunnel.exe_custom_scripts("down", self.wg_provider, self.config) - self.wg_connect = 0 elif env == "bypass": @@ -286,7 +266,9 @@ def kill_pid(self, i): except CalledProcessError: self.logger.debug("OS: process {} does not exist anymore".format(i)) + #OBSOLETE - moved to update.py #allow downloading from provider api/site even if firewall is activated and no connection is active + """ def allow_provider_ip(self, provider): server = [] @@ -322,6 +304,7 @@ def allow_provider_ip(self, provider): except CalledProcessError as e: self.logger.error("{}: Could not resolve {}".format(e, s)) + """ #save and restore content of /etc/resolv.conf @dbus.service.method(BUS_NAME, in_signature='', out_signature='') @@ -345,7 +328,7 @@ def change_ovpn_config(self, provider, certpath): f_source = "{}/{}".format(certpath, f) if provider in SUPPORTED_PROVIDERS: - f_dest = "{}/{}".format(ROOTDIR, f) + f_dest = "{}/{}/openvpn.conf".format(ROOTDIR, provider) else: f_dest = "{}/{}/{}".format(ROOTDIR, provider, f) @@ -356,13 +339,11 @@ def change_ovpn_config(self, provider, certpath): def import_thread(self, credentials): provider = credentials["provider"] self.homedir = credentials["homedir"] - self.allow_provider_ip(provider) - try: if credentials["credentials"] == "unknown": try: - auth_file = "{}/certs/{}-auth.txt".format(ROOTDIR, provider) + auth_file = "{}/{}/{}-auth.txt".format(ROOTDIR, provider, provider) with open(auth_file, "r") as auth: up = auth.read().split("\n") @@ -372,6 +353,9 @@ def import_thread(self, credentials): except FileNotFoundError: self.logger.error("Could not find {} - Aborting update".format(auth_file)) + if provider == "Airvpn": + credentials["key"] = self.config["airvpn_key"] + except KeyError: pass @@ -403,6 +387,8 @@ def downloaded(self, content): if provider in SUPPORTED_PROVIDERS: with open('{}/config.json'.format(ROOTDIR), 'w') as save_config: self.config["{}_last".format(provider)] = str(datetime.utcnow()) + if provider == "Airvpn": + self.config["airvpn_key"] = content["airvpn_key"] json.dump(self.config, save_config) with open('{}/{}.json'.format(self.homedir, provider), 'w') as p: diff --git a/qomui/tunnel.py b/qomui/tunnel.py index e7e4a20..610ce1a 100755 --- a/qomui/tunnel.py +++ b/qomui/tunnel.py @@ -12,7 +12,7 @@ from qomui import firewall, dns_manager ROOTDIR = "/usr/share/qomui" -SUPPORTED_PROVIDERS = ["Airvpn", "Mullvad", "ProtonVPN", "PIA", "Windscribe"] +SUPPORTED_PROVIDERS = ["Airvpn", "Mullvad", "ProtonVPN", "PIA", "Windscribe", "AzireVPN"] class TunnelThread(QtCore.QThread): log = QtCore.pyqtSignal(tuple) @@ -52,7 +52,7 @@ def wireguard(self): oldmask = os.umask(0o077) path = "{}/wg_qomui.conf".format(ROOTDIR) if self.server_dict["provider"] == "Mullvad": - with open("{}/certs/mullvad_wg.conf".format(ROOTDIR), "r") as wg: + with open("{}/Mullvad/mullvad_wg.conf".format(ROOTDIR), "r") as wg: conf = wg.readlines() conf.insert(8, "PublicKey = {}\n".format(self.server_dict["public_key"])) conf.insert(9, "Endpoint = {}:{}\n".format(self.server_dict["ip"], self.server_dict["port"])) @@ -100,7 +100,7 @@ def openvpn(self): elif value.startswith("accept") is True: ssl_config[line] = "accept = 127.0.0.1:{}\n".format(self.air_ssl_port) ssl_config.append("verify = 3\n") - ssl_config.append("CAfile = /usr/share/qomui/certs/stunnel.crt") + ssl_config.append("CAfile = /usr/share/qomui/Airvpn/stunnel.crt") with open("{}/temp.ssl".format(ROOTDIR), "w") as ssl_dump: ssl_dump.writelines(ssl_config) ssl_dump.close() @@ -151,6 +151,9 @@ def openvpn(self): elif provider == "ProtonVPN": self.write_config(self.server_dict) + elif provider == "AzireVPN": + self.write_config(self.server_dict) + else: config_file = "{}/{}".format(ROOTDIR, self.server_dict["path"]) @@ -200,9 +203,15 @@ def write_config(self, ovpn_dict, edit="temp", path=None): ip = ovpn_dict["ip"] port = ovpn_dict["port"] protocol = ovpn_dict["protocol"] + compat = 1 if path is None: - ovpn_file = "{}/{}_config".format(ROOTDIR, provider) + if os.path.exists("{}/{}/openvpn.conf".format(ROOTDIR, provider)): + ovpn_file = "{}/{}/openvpn.conf".format(ROOTDIR, provider) + else: + #Ensure compatibility with older versions + ovpn_file = "{}/{}_config_old".format(ROOTDIR, provider) + compat = 0 else: ovpn_file = path @@ -232,9 +241,9 @@ def write_config(self, ovpn_dict, edit="temp", path=None): if "bypass" in ovpn_dict: edit = "bypass" if ovpn_dict["bypass"] == "1": - config.append("iproute /usr/share/qomui/bypass_route.sh\n") + config.append("iproute {}/scripts/bypass_route.sh\n".format(ROOTDIR)) config.append("script-security 2\n") - config.append("route-up /usr/share/qomui/bypass_up.sh\n") + config.append("route-up {}/scripts/bypass_up.sh\n".format(ROOTDIR)) for line, value in enumerate(config): if value.startswith("proto ") is True: @@ -259,14 +268,26 @@ def write_config(self, ovpn_dict, edit="temp", path=None): try: if ovpn_dict["tlscrypt"] == "on": - config.append("tls-crypt {}/certs/tls-crypt.key \n".format(ROOTDIR)) + config.append("tls-crypt {}/Airvpn/tls-crypt.key \n".format(ROOTDIR)) config.append("auth sha512") else: - config.append("tls-auth {}/certs/ta.key 1 \n".format(ROOTDIR)) + config.append("tls-auth {}/Airvpn/ta.key 1 \n".format(ROOTDIR)) except KeyError: - config.append("tls-auth {}/certs/ta.key 1 \n".format(ROOTDIR)) + config.append("tls-auth {}/Airvpn/ta.key 1 \n".format(ROOTDIR)) + + elif provider == "AzireVPN": + ca = "ca {}/{}/{}.crt\n".format(ROOTDIR, provider, ovpn_dict["name"]) + tls = "tls-auth {}/{}/{}.key 1\n".format(ROOTDIR, provider, ovpn_dict["name"]) + config.append(ca) + config.append(tls) + + #Ensure compatibility with older versions: + if compat == 0: + for i, l in enumerate(config): + config[i] = l.replace("{}/".format(provider), "certs/") + with open("{}/{}.ovpn".format(ROOTDIR, edit), "w") as ovpn_dump: ovpn_dump.writelines(config) @@ -287,10 +308,8 @@ def wg(self, wg_file): ["-I", "OUTPUT", "2", "-o", "wg_qomui", "-j", "ACCEPT"] ] - for rule in wg_rules: - firewall.add_rule_6(rule) - firewall.add_rule(rule) - + firewall.batch_rule_6(wg_rules) + firewall.batch_rule(wg_rules) time.sleep(1) try: @@ -363,10 +382,12 @@ def ovpn(self, ovpn_file, h, cwd_ovpn): '--config', '{}'.format(ovpn_file), '--route-nopull', '--script-security', '2', - '--up', '/usr/share/qomui/hop.sh -f {} {}'.format(self.hop_dict["ip"], - self.server_dict["ip"] + '--up', '{}/scripts/hop.sh -f {} {}'.format( + ROOTDIR, + self.hop_dict["ip"], + self.server_dict["ip"] ), - '--down', '/usr/share/qomui/hop_down.sh {}'.format(self.hop_dict["ip"]) + '--down', '{}/scripts/hop_down.sh {}'.format(ROOTDIR, self.hop_dict["ip"]) ] elif h == "2": @@ -522,7 +543,7 @@ def ssl(self, ip): #using pexpect instead of subprocess to accept SHA fingerprint def ssh(self, ip, port): - cmd_ssh = "ssh -i {}/certs/sshtunnel.key -L 1412:127.0.0.1:2018 sshtunnel@{} -p {} -N -T -v".format(ROOTDIR, ip, port) + cmd_ssh = "ssh -i {}/Airvpn/sshtunnel.key -L 1412:127.0.0.1:2018 sshtunnel@{} -p {} -N -T -v".format(ROOTDIR, ip, port) ssh_exe = pexpect.spawn(cmd_ssh) ssh_newkey = b'Are you sure you want to continue connecting' ssh_success = 'Forced command' diff --git a/qomui/update.py b/qomui/update.py index a5c2fbf..02bbab6 100755 --- a/qomui/update.py +++ b/qomui/update.py @@ -17,6 +17,7 @@ from bs4 import BeautifulSoup from subprocess import PIPE, Popen, check_output, CalledProcessError, run +from qomui import firewall try: _fromUtf8 = QtCore.QString.fromUtf8 @@ -26,8 +27,7 @@ def _fromUtf8(s): ROOTDIR = "/usr/share/qomui" TEMPDIR = "/usr/share/qomui/temp" -CERTDIR = "/usr/share/qomui/certs" -SUPPORTED_PROVIDERS = ["Airvpn", "Mullvad", "ProtonVPN", "PIA", "Windscribe"] +SUPPORTED_PROVIDERS = ["Airvpn", "AzireVPN", "Mullvad", "PIA", "ProtonVPN", "Windscribe"] def country_translate(cc): try: @@ -54,24 +54,24 @@ def __init__(self, credentials, folderpath=None): self.password = credentials["password"] self.provider = credentials["provider"] self.folderpath = credentials["folderpath"] + self.update = credentials["update"] self.temp_path = "{}/{}".format(TEMPDIR, self.provider) + self.allowed_ips = [] + + try: + self.key = credentials["key"] + except KeyError: + self.key = "Default" def run(self): self.started.emit(self.provider) self.log.emit(("debug", "Started new thread to import {}".format(self.provider))) - if os.path.exists("{}/{}".format(TEMPDIR, self.provider)): - shutil.rmtree("{}/{}".format(TEMPDIR, self.provider)) - os.makedirs("{}/{}".format(TEMPDIR, self.provider)) - - else: - os.makedirs("{}/{}".format(TEMPDIR, self.provider)) - - if not os.path.exists(CERTDIR): - os.makedirs(CERTDIR) + if os.path.exists(self.temp_path): + shutil.rmtree(self.temp_path) + os.makedirs(self.temp_path) if self.provider in SUPPORTED_PROVIDERS: getattr(self, self.provider.lower())() - else: self.add_folder() @@ -84,6 +84,9 @@ def airvpn(self): from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization, hashes, asymmetric, ciphers + self.log.emit(("info", "Creating temporary rule to access Airvpn API")) + firewall.allow_dest_ip("54.93.175.114", "-I") + self.allowed_ips.append("54.93.175.114") self.airvpn_servers = {} self.airvpn_protocols = {} self.backend = default_backend() @@ -96,6 +99,16 @@ def airvpn(self): "version" : "999" } + certificates = { + "ssh_key": "sshtunnel.key", + "ssl_crt": "stunnel.crt", + "ca" : "ca.crt", + "ta" : "ta.key", + "key" : "user.key", + "crt" : "user.crt", + "tls_crypt" : "tls-crypt.key" + } + #Loading public RSA key with open("{}/airvpn_api.pem".format(ROOTDIR), "rb") as pem: rsa_pub_key = serialization.load_pem_public_key( @@ -129,19 +142,29 @@ def airvpn(self): cert_xml = decrypt_user.update(cert_xml.content) + decrypt_user.finalize() parser = etree.XMLParser(recover=True) cert_xml_root = et.fromstring(cert_xml, parser=parser) - self.temp_path = "{}/{}".format(TEMPDIR, self.provider) for a in cert_xml_root.attrib: if cert_xml_root.attrib[a] == "Wrong login/password.": raise ValueError("Wrong credentials") if a != "login" and a != "expirationdate": - with open("{}/{}".format(self.temp_path, a), "w") as c: - c.write(cert_xml_root.attrib[a]) + try: + with open("{}/{}".format(self.temp_path, certificates[a]), "w") as c: + c.write(cert_xml_root.attrib[a]) + except KeyError: + pass + + key_index = 0 + keys_available = len(cert_xml_root[0]) + for k in range(keys_available): + for n in cert_xml_root[0][k].attrib: + if n == "name" and cert_xml_root[0][k].attrib[n] == self.key: + key_index = k + + user_key = cert_xml_root[0][key_index] - user_key = cert_xml_root[0][0] for a in user_key.attrib: if a == "crt" or a == "key": - with open("{}/{}".format(self.temp_path, a), "w") as c: + with open("{}/{}".format(self.temp_path, certificates[a]), "w") as c: c.write(user_key.attrib[a]) data_params["act"] = "manifest" @@ -205,20 +228,22 @@ def airvpn(self): airvpn_data = { "server" : self.airvpn_servers, "protocol" : self.airvpn_protocols, - "provider" : "Airvpn" + "provider" : "Airvpn", + "airvpn_key" : self.key } self.copy_certs(self.provider) self.finished.emit(airvpn_data) except ValueError as e: - self.log.emit(("debug", e)) + self.log.emit(("debug", e.args)) m = "Airvpn download failed&Perhaps the credentials you entered are wrong&{}".format(self.provider) self.remove_temp_dir(self.provider) self.failed.emit(m) + except Exception as e: - self.log.emit(("debug", e)) + self.log.emit(("debug", e.args)) self.log.emit(("info", "Airvpn: Request failed - aborting")) self.remove_temp_dir(self.provider) self.failed.emit("Sorry, something went wrong") @@ -254,12 +279,13 @@ def call_air_api(self, payload): self.failed.emit("Network error&No internet connection&{}".format(self.provider)) def mullvad(self): + self.allow_ip(["api.mullvad.net", "mullvad.net", "raw.githubusercontent.com"]) self.mullvad_servers = {} self.password = "m" self.log.emit(("info", "Downloading certificates for Mullvad")) auth = 0 + certificates = {"ca.crt":"mullvad_ca.crt" ,"api_root_ca.pem":"mullvad_crl.pem"} with requests.Session() as self.session: - try: certfiles = ["ca.crt", "api_root_ca.pem"] git_raw = "https://raw.githubusercontent.com/mullvad/mullvadvpn-app/master/dist-assets/" @@ -267,7 +293,7 @@ def mullvad(self): for c in certfiles: certificate = self.session.get("{}{}".format(git_raw, c), timeout=2) - with open("{}/{}".format(self.temp_path, c), 'w') as cert_file: + with open("{}/{}".format(self.temp_path, certificates[c]), 'w') as cert_file: cert_file.write(certificate.content.decode("utf-8")) page = self.session.get('https://www.mullvad.net/en/servers/', timeout=2) @@ -331,36 +357,35 @@ def mullvad(self): "tunnel" : "WireGuard" } + wg_file = "mullvad_wg.conf" + wg_keys = self.gen_wg_key(wg_file) + if wg_keys is not None: + data = [('account', self.username), + ('pubkey', wg_keys[1]) + ] + + pub_up = self.session.post("https://api.mullvad.net/wg/", data=data) + if pub_up.status_code < 400: + wg_address = pub_up.content.decode("utf-8").split("\n")[0] + + wg_conf = [ + "[Interface]\n", + "DNS = 193.138.219.228\n", + "\n", + "[Peer]\n", + "AllowedIPs = 0.0.0.0/0, ::/0\n" + ] + + with open("{}/{}".format(self.temp_path, wg_file), "w") as wg: + wg_conf.insert(1, "PrivateKey = {}\n".format(wg_keys[0])) + wg_conf.insert(2, "Address = {}\n".format(wg_address)) + wg.writelines(wg_conf) - private_key = check_output(["wg", "genkey"]).decode("utf-8").split("\n")[0] - pubgen = run(["wg", "pubkey"], stdout=PIPE, input=private_key, encoding='ascii') - pubkey = pubgen.stdout.split("\n")[0] - data = [('account', self.username), - ('pubkey', pubkey) - ] - - pub_up = self.session.post("https://api.mullvad.net/wg/", data=data) - if pub_up.status_code < 400: - wg_address = pub_up.content.decode("utf-8").split("\n")[0] - - wg_conf = [ - "[Interface]\n", - "DNS = 193.138.219.228\n", - "\n", - "[Peer]\n", - "AllowedIPs = 0.0.0.0/0, ::/0\n" - ] - - with open("{}/mullvad_wg.conf".format(self.temp_path), "w") as wg: - wg_conf.insert(1, "PrivateKey = {}\n".format(private_key)) - wg_conf.insert(2, "Address = {}\n".format(wg_address)) - wg.writelines(wg_conf) - - else: - m = "Authentication failed&Perhaps the credentials you entered are wrong&{}".format(self.provider) - self.remove_temp_dir(self.provider) - self.failed.emit(m) - auth = 1 + else: + m = "Mullvad: Authentication failed&Perhaps the credentials you entered are wrong&{}".format(self.provider) + self.remove_temp_dir(self.provider) + self.failed.emit(m) + auth = 1 except (CalledProcessError, FileNotFoundError) as e: @@ -395,6 +420,7 @@ def cc_translate(self, country_raw): return country def pia(self): + self.allow_ip(["www.privateinternetaccess.com"]) self.pia_servers = {} self.pia_protocols = {} self.log.emit(("info", "Downloading PIA config files")) @@ -463,6 +489,25 @@ def pia(self): "tunnel" : "OpenVPN" } + certificates = { + "{}/strong/crl.rsa.4096.pem".format(self.temp_path) :"{}/pia_crl.rsa.4096.pem".format(self.temp_path), + "{}/strong/ca.rsa.4096.crt".format(self.temp_path) :"{}/pia_ca.rsa.4096.crt".format(self.temp_path) + } + + for orig, dest in certificates.items(): + try: + shutil.copyfile(orig, dest) + + except FileNotFoundError as e: + self.log.emit(("error", e)) + + try: + shutil.rmtree("{}/ip".format(self.temp_path)) + shutil.rmtree("{}/strong".format(self.temp_path)) + + except FileNotFoundError as e: + self.log.emit(("error", e)) + self.copy_certs(self.provider) self.finished.emit(pia_dict) @@ -472,6 +517,7 @@ def pia(self): self.failed.emit("Network error&No internet connection&{}".format(self.provider)) def windscribe(self): + self.allow_ip(["windscribe.com", "assets.windscribe.com", "res.windscribe.com"]) self.windscribe_servers = {} self.windscribe_protocols = {} self.header = { @@ -511,21 +557,16 @@ def windscribe(self): userpass = json.loads(get_cred.content.decode("utf-8")) try: - self.username = userpass["username"] - self.password = userpass["password"] self.log.emit(("info", "Created Windscribe credentials for OpenVPN")) - self.windscribe_get_servers() - - """ - with open("{}/windscribe_userpass.txt".format(CERTDIR), "w") as cd: + with open("{}/windscribe_userpass.txt".format(self.temp_path), "w") as cd: cd.write("{}\n{}\n".format(userpass["username"], userpass["password"])) - self.log.emit(("debug", "Windscribe OpenVPN credentials written to {}/windscribe_userpass.txt".format(CERTDIR))) - """ + self.log.emit(("debug", "Windscribe OpenVPN credentials written to {}/windscribe_userpass.txt".format(self.temp_path))) - except KeyError: + self.windscribe_get_servers() + except KeyError: self.log.emit(("info", "Windscribe: Login failed")) - m = "Authentication failed&Perhaps the credentials you entered are wrong&{}".format(self.provider) + m = "Windscribe: Authentication failed&Perhaps the credentials you entered are wrong&{}".format(self.provider) self.remove_temp_dir(self.provider) self.failed.emit(m) @@ -610,6 +651,7 @@ def windscribe_get_servers(self): self.finished.emit(ws_dict) def protonvpn(self): + self.allow_ip(["api.protonmail.ch"]) self.proton_servers = {} self.log.emit(("info", "Downloading ProtonVPN server configs")) @@ -707,6 +749,135 @@ def protonvpn(self): self.remove_temp_dir(self.provider) self.failed.emit("Network error&No internet connection&{}".format(self.provider)) + def azirevpn(self): + self.az_servers = {} + self.allow_ip(["azirevpn.net"]) + + try: + + try: + self.log.emit(("info", "Downloading AzireVPN OpenVPN configs")) + az_api_url = "https://api.azirevpn.com/v1/locations" + az_servers = json.loads(requests.get(az_api_url, timeout=2).content.decode("utf-8")) + + except requests.exceptions.RequestException as e: + az_servers = {"locations" : []} + self.log.emit(("error", "Network error: Unable to retrieve data from api.azirevpn.com")) + self.remove_temp_dir(self.provider) + self.failed.emit("Network error&No internet connection&{}".format(self.provider)) + + for s in az_servers["locations"]: + name = s["name"] + "-openvpn" + "-azirevpn" + wg_name = s["name"] + "-wireguard" + "-azirevpn" + country = country_translate(s["iso"]) + hostname = s["endpoints"]["openvpn"][0]["hostname"] + ip = resolve(hostname)[0] + self.log.emit(("info", "Importing {}".format(name))) + + if ip != "Failed to resolve": + self.az_servers[name] = { + "name": name, + "provider" : self.provider, + "city" : s["city"], + "ip" : ip, + "country" : country, + "tunnel" : "OpenVPN" + } + + crt_url = s["openvpn-ca"] + crt_file = "{}/{}.crt".format(self.temp_path, name) + crt = requests.get(crt_url, timeout=2).content.decode("utf-8") + with open(crt_file, "w") as c: + c.write(crt) + + tls_url = s["openvpn-tls-key"] + tls_file = "{}/{}.key".format(self.temp_path, name) + tls = requests.get(tls_url, timeout=2).content.decode("utf-8") + with open(tls_file, "w") as t: + t.write(tls) + + else: + self.log.emit(("Error: Could not resolve {} - skipping".format(hostname))) + + try: + wg_file = "{}.conf".format(wg_name) + wg_api_url = s["endpoints"]["wireguard"] + wg_keys = self.gen_wg_key(wg_file) + + if wg_keys is not None: + data = { + 'username' : str(self.username), + 'password' : str(self.password), + 'pubkey' : wg_keys[1] + } + + pub_up = requests.post(wg_api_url, data=data, timeout=10) + if pub_up.status_code == 200: + api_resp = json.loads(pub_up.content.decode("utf-8")) + + if api_resp["status"] != "error": + wg_ip = resolve(api_resp["data"]["Endpoint"].split(":")[0])[0] + wg_conf = [ + "[Interface]\n", + "PrivateKey = {}\n".format(wg_keys[0]), + "Address = {}\n".format(api_resp["data"]["Address"]), + "DNS = {}\n".format(api_resp["data"]["DNS"]), + "\n", + "[Peer]\n", + "PublicKey = {}\n".format(api_resp["data"]["PublicKey"]), + "Endpoint = {}:51820\n".format(wg_ip), + "AllowedIPs = 0.0.0.0/0, ::/0\n" + ] + + self.az_servers[wg_name] = { + "name": wg_name, + "provider" : self.provider, + "city" : s["city"], + "ip" : wg_ip, + "country" : country, + "tunnel" : "WireGuard", + "path" : "{}/{}.conf".format(self.provider, wg_name) + } + + with open("{}/{}".format(self.temp_path, wg_file), "w") as wg: + wg.writelines(wg_conf) + + else: + m = "AzireVPN: Authentication failed&Perhaps the credentials you entered are wrong&{}".format(self.provider) + self.log.emit(("error", m)) + self.remove_temp_dir(self.provider) + self.failed.emit(m) + + except (CalledProcessError, FileNotFoundError) as e: + self.log.emit(("info", "WireGuard is not installed/not found - skipping")) + + except (requests.exceptions.RequestException, json.JSONDecodeError) as e: + self.log.emit(("debug", e)) + self.log.emit(("info", "Network error: Uploading WireGuard public key failed")) + + except Exception as e: + self.log.emit(("debug", e)) + self.log.emit(("error", "An unexpected error occured: Aborting")) + self.remove_temp_dir(self.provider) + self.failed.emit("AzireVPN import failed&An unknown error occured&{}".format(self.provider)) + + else: + az_protocols = { + "protocol_1" : {"protocol": "UDP", "port": "1194"}, + "protocol_2" : {"protocol": "TCP", "port": "1194"}, + "protocol_3" : {"protocol": "UDP", "port": "443"}, + "protocol_4" : {"protocol": "TCP", "port": "443"} + } + + azire_dict = { + "server" : self.az_servers, + "protocol" : az_protocols, + "provider" : "AzireVPN" + } + + self.copy_certs(self.provider) + self.finished.emit(azire_dict) + def add_folder(self): self.conf_files = [f for f in os.listdir(self.folderpath) if f.endswith('.ovpn') or f.endswith('.conf')] self.cert_files = [f for f in os.listdir(self.folderpath) if f.endswith('.ovpn') or f.endswith('.conf')] @@ -761,7 +932,7 @@ def import_configs(self): else: server = line.split(" ")[1] - ip = resolve(server) + ip = resolve(server)[0] if ip != "Failed to resolve": modify[index] = "remote {} {}\n".format(ip, port) @@ -802,7 +973,7 @@ def import_configs(self): ip = result.group() else: - ip = resolve(server) + ip = resolve(server)[0] if ip != "Failed to resolve": modify[index] = "Endpoint = {}:{}\n".format(ip, port) @@ -864,58 +1035,63 @@ def sanity_check(self, path): return unrelated_files + def gen_wg_key(self, config): + #check if key already exists + if os.path.exists("{}/{}/{}".format(ROOTDIR, self.provider, config)) and self.update == "0": + self.log.emit(("debug", "WireGuard keys for {} have already been generated".format(config.split("/")[-1]))) + wg_keys = None + + else: + self.log.emit(("info", "Generating WireGuard keys for {}".format(config.split("/")[-1]))) + + try: + private_key = check_output(["wg", "genkey"]).decode("utf-8").split("\n")[0] + pubgen = run(["wg", "pubkey"], stdout=PIPE, input=private_key, encoding='ascii') + public_key = pubgen.stdout.split("\n")[0] + wg_keys = (private_key, public_key) + except (CalledProcessError, FileNotFoundError) as e: + wg_keys = None + self.log.emit(("info", "WireGuard is not installed/not found - skipping")) + + return wg_keys + + def allow_ip(self, hosts): + for host in hosts: + self.log.emit(("info", "Creating temporary rule to access {}".format(host))) + ips = resolve(host) + for i in ips: + if i != "" and i != "Failed to resolve": + firewall.allow_dest_ip(i, "-I") + self.allowed_ips.append(i) + def copy_certs(self, provider): - oldmask = os.umask(0o077) + for i in self.allowed_ips: + firewall.allow_dest_ip(i, "-D") - if not os.path.exists("{}/certs".format(ROOTDIR)): - os.makedirs("{}/certs".format(ROOTDIR)) + provider_dir = "{}/{}".format(ROOTDIR, provider) + if not os.path.exists(provider_dir): + os.makedirs(provider_dir) - with open("{}/{}-auth.txt".format(CERTDIR, self.provider) , "w") as passfile: + oldmask = os.umask(0o077) + with open("{}/{}-auth.txt".format(provider_dir, self.provider) , "w") as passfile: passfile.write('{}\n{}\n'.format(self.username, self.password)) if provider in SUPPORTED_PROVIDERS: - self.Airvpn_files = [ - ("ssh_ppk", "sshtunnel.key"), - ("ssl_crt", "stunnel.crt"), - ("ca", "ca.crt"), - ("ta", "ta.key"), - ("key", "user.key"), - ("crt", "user.crt"), - ("tls_crypt", "tls-crypt.key") - ] - - self.Mullvad_files = [ - ("ca.crt", "mullvad_ca.crt"), - ("api_root_ca.pem", "mullvad_crl.pem"), - ("mullvad_wg.conf", "mullvad_wg.conf") - ] - - self.PIA_files = [ - ("strong/crl.rsa.4096.pem", "pia_crl.rsa.4096.pem"), - ("strong/ca.rsa.4096.crt", "pia_ca.rsa.4096.crt") - ] - - self.Windscribe_files = [ - ("ca.crt", "ca_ws.crt"), - ("ta.key", "ta_ws.key"), - ] - - self.ProtonVPN_files = [ - ("proton_ca.crt", "proton_ca.crt"), - ("proton_ta.key", "proton_ta.key") - ] - - for cert in getattr(self, "{}_files".format(provider)): + for f in os.listdir(self.temp_path): + shutil.copyfile("{}/{}".format(self.temp_path, f), "{}/{}".format(provider_dir, f)) + Popen(['chown', 'root', '{}/{}'.format(provider_dir, f)]) + Popen(['chmod', '0600', '{}/{}'.format(provider_dir, f)]) - try: - origin = "{}/{}".format(self.temp_path, cert[0]) - dest = "{}/{}".format(CERTDIR, cert[1]) - shutil.copyfile(origin, dest) - self.log.emit(("debug", "Copied {} to {}".format(origin, dest))) + try: + openvpn_orig_conf = "{}/{}_config".format(ROOTDIR, provider) + openvpn_dest_conf = "{}/{}/openvpn.conf".format(ROOTDIR, provider) + if not os.path.exists(openvpn_dest_conf): + shutil.copyfile(openvpn_orig_conf, openvpn_dest_conf) + Popen(['chmod', '0655', openvpn_dest_conf]) - except FileNotFoundError: - self.log.emit(("error", "Copying {} to {} failed: No such file".format(cert, CERTDIR))) + except FileNotFoundError: + self.log.emit(("error", "{} does not exist".format(openvpn_orig_conf))) else: path = "{}/copy/".format(self.temp_path) @@ -946,20 +1122,13 @@ def copy_certs(self, provider): shutil.copytree(f_source, f_dest) self.log.emit(("debug", "copied folder {} to {}".format(f, f_dest))) - #doesn't work if importing existing provider - #shutil.copytree("{}/copy/".format(self.temp_path), "{}/{}/".format(ROOTDIR, provider)) - - for key in [f for f in os.listdir("{}/certs".format(ROOTDIR))]: - Popen(['chown', 'root', '{}/certs/{}'.format(ROOTDIR, key)]) - Popen(['chmod', '0600', '{}/certs/{}'.format(ROOTDIR, key)]) - os.umask(oldmask) self.remove_temp_dir(self.provider) def remove_temp_dir(self, provider): try: shutil.rmtree(self.temp_path) - self.log.emit(("debug", "Removed temporary directory")) + self.log.emit(("debug", "Removed temporary download directory for {}".format(provider))) except FileNotFoundError: pass @@ -968,13 +1137,14 @@ def resolve(host): try: dig_cmd = ["dig", "+time=2", "+tries=2", "{}".format(host), "+short"] ip = check_output(dig_cmd).decode("utf-8") - ip = ip.split("\n")[0] + ip = ip.split("\n") except (FileNotFoundError, CalledProcessError): - ip = "Failed to resolve" + ip = ["Failed to resolve"] return ip + class UpdateCheck(QtCore.QThread): release_found = QtCore.pyqtSignal(str) log = QtCore.pyqtSignal(tuple) diff --git a/qomui/utils.py b/qomui/utils.py index 59ab7cc..d7a2eff 100755 --- a/qomui/utils.py +++ b/qomui/utils.py @@ -5,7 +5,7 @@ import getpass import pwd -SUPPORTED_PROVIDERS = ["Airvpn", "Mullvad", "ProtonVPN", "PIA", "Windscribe"] +SUPPORTED_PROVIDERS = ["Airvpn", "AzireVPN", "Mullvad", "ProtonVPN", "PIA", "Windscribe"] def get_user_group(): username = getpass.getuser() diff --git a/qomui/widgets.py b/qomui/widgets.py index 3f46a35..9c8b564 100755 --- a/qomui/widgets.py +++ b/qomui/widgets.py @@ -11,6 +11,7 @@ import configparser import requests import shlex +import logging from PyQt5 import QtCore, QtWidgets, QtGui from qomui import update @@ -31,7 +32,7 @@ def _translate(context, text, disambig): ROOTDIR = "/usr/share/qomui" HOMEDIR = "{}/.qomui".format(os.path.expanduser("~")) -SUPPORTED_PROVIDERS = ["Airvpn", "Mullvad", "ProtonVPN", "PIA", "Windscribe"] +SUPPORTED_PROVIDERS = ["Airvpn", "AzireVPN", "Mullvad", "PIA", "ProtonVPN", "Windscribe"] class favouriteButton(QtWidgets.QAbstractButton): def __init__(self, parent=None): @@ -610,7 +611,7 @@ def run(self): time_measure = time.time() elapsed = time_measure - start_time - if int(elapsed) % 3600 == 0: + if int(elapsed) % 900 == 0: self.check.emit() return_time = self.time_format(int(elapsed)) @@ -864,14 +865,17 @@ def get_desktop_files(self): else: name = c["Desktop Entry"]["Name"] icon = c["Desktop Entry"]["Icon"] + logging.debug("Adding {} to bypass app list".format(name)) self.bypassAppList.append((name, icon, desktop_file)) except KeyError: name = c["Desktop Entry"]["Name"] icon = c["Desktop Entry"]["Icon"] + logging.debug("Adding {} to bypass app list".format(name)) self.bypassAppList.append((name, icon, desktop_file)) - except: - pass + except Exception as e: + logging.error(e) + logging.error("Failed to add {} to bypass app list".format(name)) self.bypassAppList = sorted(self.bypassAppList) self.pop_AppList() @@ -997,7 +1001,7 @@ def load_config_file(self): def display_config(self): if self.provider in SUPPORTED_PROVIDERS: - config = "{}/{}_config".format(ROOTDIR, self.provider) + config = "{}/{}/openvpn.conf".format(ROOTDIR, self.provider) else: config = "{}/{}".format(ROOTDIR, self.server_info["path"]) @@ -1074,3 +1078,4 @@ def accept_change(self): self.modified.emit(change_dict) self.hide() + diff --git a/resources/Airvpn_config b/resources/Airvpn_config index 8d85aa6..e28e642 100644 --- a/resources/Airvpn_config +++ b/resources/Airvpn_config @@ -12,7 +12,7 @@ cipher AES-256-CBC comp-lzo no auth-nocache verb 3 -ca /usr/share/qomui/certs/ca.crt -cert /usr/share/qomui/certs/user.crt -key /usr/share/qomui/certs/user.key +ca /usr/share/qomui/Airvpn/ca.crt +cert /usr/share/qomui/Airvpn/user.crt +key /usr/share/qomui/Airvpn/user.key diff --git a/resources/Airvpn_config_old b/resources/Airvpn_config_old new file mode 100644 index 0000000..b1bc115 --- /dev/null +++ b/resources/Airvpn_config_old @@ -0,0 +1,17 @@ +client +dev tun +proto +remote +resolv-retry infinite +nobind +persist-key +persist-tun +push-peer-info +remote-cert-tls server +cipher AES-256-CBC +comp-lzo no +auth-nocache +verb 3 +ca /usr/share/qomui/certs/ca.crt +cert /usr/share/qomui/certs/user.crt +key /usr/share/qomui/certs/user.key diff --git a/resources/AzireVPN_config b/resources/AzireVPN_config new file mode 100644 index 0000000..52abe0d --- /dev/null +++ b/resources/AzireVPN_config @@ -0,0 +1,19 @@ +client +dev tun +proto +remote +resolv-retry infinite +nobind +persist-key +persist-tun +remote-cert-tls server +reneg-sec 0 +keepalive 10 60 +mute-replay-warnings +explicit-exit-notify 3 +cipher AES-256-CBC +auth SHA512 +tls-version-min 1.2 +verb 3 + +auth-user-pass /usr/share/qomui/AzireVPN/AzireVPN-auth.txt diff --git a/resources/Mullvad_config b/resources/Mullvad_config index 6584137..1418b4a 100644 --- a/resources/Mullvad_config +++ b/resources/Mullvad_config @@ -15,7 +15,7 @@ sndbuf 524288 rcvbuf 524288 verb 3 -auth-user-pass /usr/share/qomui/certs/Mullvad-auth.txt -ca /usr/share/qomui/certs/mullvad_ca.crt +auth-user-pass /usr/share/qomui/Mullvad/Mullvad-auth.txt +ca /usr/share/qomui/Mullvad/mullvad_ca.crt tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA diff --git a/resources/Mullvad_config_old b/resources/Mullvad_config_old new file mode 100644 index 0000000..6584137 --- /dev/null +++ b/resources/Mullvad_config_old @@ -0,0 +1,21 @@ +client +dev tun +proto +remote +resolv-retry infinite +nobind +persist-key +persist-tun +remote-cert-tls server +cipher AES-256-CBC +comp-lzo +fast-io +ping-restart 60 +sndbuf 524288 +rcvbuf 524288 +verb 3 + +auth-user-pass /usr/share/qomui/certs/Mullvad-auth.txt +ca /usr/share/qomui/certs/mullvad_ca.crt + +tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA diff --git a/resources/PIA_config b/resources/PIA_config index 7b54dcb..2c7b522 100644 --- a/resources/PIA_config +++ b/resources/PIA_config @@ -10,11 +10,11 @@ cipher aes-256-cbc auth sha256 tls-client remote-cert-tls server -auth-user-pass /usr/share/qomui/certs/PIA-auth.txt +auth-user-pass /usr/share/qomui/PIA/PIA-auth.txt comp-lzo no verb 3 reneg-sec 0 -crl-verify /usr/share/qomui/certs/pia_crl.rsa.4096.pem -ca /usr/share/qomui/certs/pia_ca.rsa.4096.crt +crl-verify /usr/share/qomui/PIA/pia_crl.rsa.4096.pem +ca /usr/share/qomui/PIA/pia_ca.rsa.4096.crt disable-occ diff --git a/resources/PIA_config_old b/resources/PIA_config_old new file mode 100644 index 0000000..4910c29 --- /dev/null +++ b/resources/PIA_config_old @@ -0,0 +1,20 @@ +client +dev tun +proto +remote +resolv-retry infinite +nobind +persist-key +persist-tun +cipher aes-256-cbc +auth sha256 +tls-client +remote-cert-tls server +auth-user-pass /usr/share/qomui/PIA/PIA-auth.txt +comp-lzo no +verb 3 +reneg-sec 0 +crl-verify /usr/share/qomui/certs/pia_crl.rsa.4096.pem +ca /usr/share/qomui/certs/pia_ca.rsa.4096.crt +disable-occ + diff --git a/resources/ProtonVPN_config b/resources/ProtonVPN_config index 8154220..a11a4bd 100644 --- a/resources/ProtonVPN_config +++ b/resources/ProtonVPN_config @@ -19,7 +19,7 @@ pull fast-io key-direction 1 -auth-user-pass /usr/share/qomui/certs/ProtonVPN-auth.txt -ca /usr/share/qomui/certs/proton_ca.crt -tls-auth /usr/share/qomui/certs/proton_ta.key +auth-user-pass /usr/share/qomui/ProtonVPN/ProtonVPN-auth.txt +ca /usr/share/qomui/ProtonVPN/proton_ca.crt +tls-auth /usr/share/qomui/ProtonVPN/proton_ta.key diff --git a/resources/ProtonVPN_config_old b/resources/ProtonVPN_config_old new file mode 100644 index 0000000..4834fb4 --- /dev/null +++ b/resources/ProtonVPN_config_old @@ -0,0 +1,24 @@ +client +dev tun +proto +remote +resolv-retry infinite +nobind +cipher AES-256-CBC +auth SHA512 +comp-lzo +verb 3 +tun-mtu 1500 +tun-mtu-extra 32 +mssfix 1450 +persist-key +persist-tun +reneg-sec 0 +remote-cert-tls server +pull +fast-io +key-direction 1 + +auth-user-pass /usr/share/qomui/certs/ProtonVPN-auth.txt +ca /usr/share/qomui/certs/proton_ca.crt +tls-auth /usr/share/qomui/certs/proton_ta.key diff --git a/resources/Windscribe_config b/resources/Windscribe_config index cc89e51..b9a2e08 100644 --- a/resources/Windscribe_config +++ b/resources/Windscribe_config @@ -16,7 +16,7 @@ persist-key persist-tun key-direction 1 -auth-user-pass /usr/share/qomui/certs/Windscribe-auth.txt -ca /usr/share/qomui/certs/ca_ws.crt -tls-auth /usr/share/qomui/certs/ta_ws.key +auth-user-pass /usr/share/qomui/Windscribe/Windscribe-auth.txt +ca /usr/share/qomui/Windscribe/ca.crt +tls-auth /usr/share/qomui/Windscribe/ta.key diff --git a/resources/Windscribe_config_old b/resources/Windscribe_config_old new file mode 100644 index 0000000..8de9dcb --- /dev/null +++ b/resources/Windscribe_config_old @@ -0,0 +1,23 @@ +client +dev tun +proto +remote +nobind +resolv-retry infinite +auth SHA512 +auth-nocache +cipher AES-256-GCM +ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM +comp-lzo +verb 3 +mute-replay-warnings +remote-cert-tls server +persist-key +persist-tun +key-direction 1 + +auth-user-pass /usr/share/qomui/certs/Windscribe-auth.txt +ca /usr/share/qomui/certs/ca_ws.crt +tls-auth /usr/share/qomui/certs/ta_ws.key + + diff --git a/resources/bypass_route.sh b/scripts/bypass_route.sh similarity index 100% rename from resources/bypass_route.sh rename to scripts/bypass_route.sh diff --git a/resources/bypass_up.sh b/scripts/bypass_up.sh similarity index 100% rename from resources/bypass_up.sh rename to scripts/bypass_up.sh diff --git a/resources/hop.sh b/scripts/hop.sh similarity index 100% rename from resources/hop.sh rename to scripts/hop.sh diff --git a/resources/hop_down.sh b/scripts/hop_down.sh similarity index 100% rename from resources/hop_down.sh rename to scripts/hop_down.sh diff --git a/setup.py b/setup.py index e07ba70..f1c06ea 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,8 @@ import glob import os -VERSION = "0.8.1" +VERSION = "0.8.2" + data_files = [ ('/usr/share/qomui', ['resources/countries.json']), ('/usr/share/applications/', ['resources/qomui.desktop']), @@ -15,23 +16,31 @@ ('/usr/share/icons/hicolor/scalable/apps/', ['resources/qomui.svg', 'resources/qomui_off.svg']), ('/usr/share/qomui/', [ + 'resources/AzireVPN_config', 'resources/Airvpn_config', 'resources/PIA_config', 'resources/ProtonVPN_config', 'resources/Windscribe_config', + 'resources/Airvpn_config_old', + 'resources/Mullvad_config', + 'resources/Mullvad_config_old', + 'resources/PIA_config_old', + 'resources/ProtonVPN_config_old', + 'resources/Windscribe_config_old', 'resources/default_config.json', 'resources/firewall_default.json', - 'resources/Mullvad_config', 'resources/ssl_config', 'resources/qomui.png', - 'resources/hop.sh', - 'resources/hop_down.sh', - 'resources/bypass_up.sh', - 'resources/bypass_route.sh', 'resources/airvpn_api.pem', 'resources/airvpn_cacert.pem', 'VERSION'] ), + ('/usr/share/qomui/scripts/', [ + 'scripts/hop.sh', + 'scripts/hop_down.sh', + 'scripts/bypass_up.sh', + 'scripts/bypass_route.sh' + ]), ('/usr/share/qomui/flags/', glob.glob('resources/flags/*')) ]