diff --git a/CHANGELOG.md b/CHANGELOG.md index 387e27d..792e5bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ##Changelog +version 0.6.2: +- [change] api-url for ProtonVPN updated - the one introduced in last update was out of date +- [change] added support for Windscribe's stealth feature (OpenVPN over SSL) +- [change] postrm functions added to deb/rpm/aur packages +- [change] automatic reconnections for double hop if first hop fails/disconnects +- [change] adjusted OpenVPN configs of Mullvad and Windscribe to match official ones +- [bugfix] tray icon not always updated after establishing double hop connection +- [bugfix] qomui crashes while performing latency checks when server(s) are deleted + version 0.6.1: - [new] support for Windscribe - [new] support for ProtonVPN diff --git a/README.md b/README.md index 66a2e37..cb37509 100755 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Qomui (Qt OpenVPN Management UI) is an easy-to-use OpenVPN Gui for GNU/Linux wit - command-line interface ### Screenshots -Taken on Arch/Plasma Arc Dark Theme - Qomui will adapt to your theme
+Screenshots were taken on Arch Linux/Plasma Arc Dark Theme - Qomui will adapt to your theme.
@@ -38,11 +38,11 @@ Taken on Arch/Plasma Arc Dark Theme - Qomui will adapt to your theme
#### Ubuntu -Download and install [DEB-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.1/qomui-0.6.1-amd64.deb) +Download and install [DEB-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.2/qomui-0.6.2-amd64.deb) #### Fedora -Download and install [RPM-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.1/qomui-0.6.1-1.x86_64.rpm) +Download and install [RPM-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.2/qomui-0.6.2-1.x86_64.rpm) #### Arch @@ -111,6 +111,15 @@ qomui-cli --help Qomui has been my first ever programming experience and a practical challenge for myself to learn a bit of Python. Hence, I'm aware that there is a lot of code that could probably be improved, streamlined and made more beautiful. I might have made some horrible mistakes, too. I'd appreciate any feedback as well as suggestions for new features. ### Changelog +version 0.6.2: +- [change] api-url for ProtonVPN updated - the one introduced in last update was out of date +- [change] added support for Windscribe's stealth feature (OpenVPN over SSL) +- [change] postrm functions added to deb/rpm/aur packages +- [change] automatic reconnections for double hop if first hop fails/disconnects +- [change] adjusted OpenVPN configs of Mullvad and Windscribe to match official ones +- [bugfix] tray icon not always updated after establishing double hop connection +- [bugfix] qomui crashes while performing latency checks when server(s) are deleted + version 0.6.1: - [new] support for Windscribe - [new] support for ProtonVPN @@ -122,13 +131,3 @@ version 0.6.1: - [bugfix] configs are not imported if url cannot be resolved - [bugfix] old connection not killed after network change detected (in rare cases) -version 0.6.0: -- [new] support for Wireguard -- [new] cli-interface -- [change] additional parameters parsed from .desktop-files -- [change] update routine now uses dpkg/rpm if installed as DEB/RPM package - reinstall required! -- [bugfix] crashes at start when system tray not available -- [bugfix] Info for active connection sometimes not updated correctly -- [bugfix] Doublehop fails on Fedora - - diff --git a/VERSION b/VERSION index 7ceb040..b1d7abc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 \ No newline at end of file +0.6.2 \ No newline at end of file diff --git a/qomui/qomui_gui.py b/qomui/qomui_gui.py index 09cad1e..9dd6b57 100755 --- a/qomui/qomui_gui.py +++ b/qomui/qomui_gui.py @@ -106,6 +106,7 @@ class QomuiGui(QtWidgets.QWidget): firewall_rules_changed = False hop_active = 0 hop_log_monitor = 0 + tun_hop = None hop_server_dict = None bypass_dict = {} config_dict = {} @@ -1139,23 +1140,23 @@ def networkstate(self, networkstate): def providerChosen(self): provider = self.addProviderBox.currentText() - if provider == "Airvpn" or provider == "PIA" or provider == "Windscribe" or provider == "ProtonVPN": + + p_txt = {"Airvpn" : ("Username", "Password"), + "PIA" : ("Username", "Password"), + "Windscribe" : ("Username", "Password"), + "Mullvad" : ("Account Numner", "N.A. - Leave empty"), + "ProtonVPN" : ("OpenVPN username", "OpenVPN password") + } + + if provider in SUPPORTED_PROVIDERS: self.addProviderEdit.setVisible(False) - self.addProviderUserEdit.setPlaceholderText(_translate("Form", "Username", None)) - self.addProviderPassEdit.setPlaceholderText(_translate("Form", "Password", None)) + self.addProviderUserEdit.setPlaceholderText(_translate("Form", p_txt[provider][0], None)) + self.addProviderPassEdit.setPlaceholderText(_translate("Form", p_txt[provider][1], None)) if provider in self.provider_list: self.addProviderDownloadBt.setText(_translate("Form", "Update", None)) else: self.addProviderDownloadBt.setText(_translate("Form", "Download", None)) - elif provider == "Mullvad": - self.addProviderEdit.setVisible(False) - self.addProviderUserEdit.setPlaceholderText(_translate("Form", "Account Number", None)) - self.addProviderPassEdit.setPlaceholderText(_translate("Form", "N.A.", None)) - if provider in self.provider_list: - self.addProviderDownloadBt.setText(_translate("Form", "Update", None)) - else: - self.addProviderDownloadBt.setText(_translate("Form", "Download", None)) else: self.addProviderEdit.setVisible(True) self.addProviderEdit.setPlaceholderText(_translate("Form", @@ -1458,18 +1459,21 @@ def display_latency(self, result): server = result[0] latency_string = result[1] latency_float = result[2] - old_index = self.index_list.index(server) - bisect.insort(self.latency_list, latency_float) - update_index = self.latency_list.index(latency_float) - rm = self.index_list.index(server) - self.index_list.pop(rm) - self.index_list.insert(update_index, server) - if getattr(self, server).isHidden() == True: - hidden = True - self.serverListWidget.takeItem(old_index) - self.add_server_widget(server, self.server_dict[server], insert=update_index) - self.serverListWidget.setRowHidden(update_index, hidden) - getattr(self, server).display_latency(latency_string) + try: + old_index = self.index_list.index(server) + bisect.insort(self.latency_list, latency_float) + update_index = self.latency_list.index(latency_float) + rm = self.index_list.index(server) + self.index_list.pop(rm) + self.index_list.insert(update_index, server) + if getattr(self, server).isHidden() == True: + hidden = True + self.serverListWidget.takeItem(old_index) + self.add_server_widget(server, self.server_dict[server], insert=update_index) + self.serverListWidget.setRowHidden(update_index, hidden) + getattr(self, server).display_latency(latency_string) + except ValueError: + pass def show_favourite_servers(self, state): self.randomSeverBt.setVisible(True) @@ -1748,7 +1752,6 @@ def openvpn_log_monitor(self, reply): self.status = "active" self.hop_log_monitor = 0 self.WaitBar.setVisible(False) - self.show_active_connection(self.ovpn_dict, self.hop_server_dict) try: if self.config_dict["simpletray"] == 0: self.trayIcon = QtGui.QIcon('%s/flags/%s.png' % (ROOTDIR, @@ -1759,10 +1762,14 @@ def openvpn_log_monitor(self, reply): except KeyError: self.trayIcon = QtGui.QIcon('%s/flags/%s.png' % (ROOTDIR, self.ovpn_dict["country"] - )) - self.tray.setIcon(QtGui.QIcon(self.trayIcon)) + )) + finally: + self.tray.setIcon(QtGui.QIcon(self.trayIcon)) + self.show_active_connection(self.ovpn_dict, self.hop_server_dict, tun_hop=self.tun_hop) + self.tun_hop = None elif self.hop_active == 2 and self.hop_log_monitor != 1: + self.tun_hop = self.qomui_service.return_tun_device("hop") self.hop_log_monitor = 1 with open('%s/last_server.json' % (HOMEDIR), 'w') as lserver: @@ -1804,12 +1811,12 @@ def show_failmsg(self, text, information): self.failmsg.setWindowModality(QtCore.Qt.WindowModal) self.failmsg.show() - def show_active_connection(self, current_server, hop_dict): + def show_active_connection(self, current_server, hop_dict, tun_hop=None): self.tray.setToolTip("Connected to %s" %self.ovpn_dict["name"]) QtWidgets.QApplication.restoreOverrideCursor() - tun = self.qomui_service.return_tun_device() + tun = self.qomui_service.return_tun_device("tun") self.ActiveWidget.setVisible(True) - self.ActiveWidget.setText(self.ovpn_dict, self.hop_server_dict, tun) + self.ActiveWidget.setText(self.ovpn_dict, self.hop_server_dict, tun, tun_hop) self.ActiveWidget.disconnect.connect(self.kill) self.ActiveWidget.reconnect.connect(self.reconnect) self.gridLayout.addWidget(self.ActiveWidget, 0, 0, 1, 3) @@ -1961,24 +1968,28 @@ def apply_edit(self, modifications): json.dump(self.server_dict, s) if len(new_config) != 0: - if provider in SUPPORTED_PROVIDERS: - temp_file = "%s/temp/%s_config" %(HOMEDIR, provider) - with open(temp_file, "w") as config_change: - config_change.writelines(new_config) - else: - temp_file = "%s/temp/%s" %(HOMEDIR, val["path"].split("/")[1]) - if modifications["apply_all"] == 1: - for k, v in self.server_dict.items(): - if v["provider"] == provider: - path = "%s/temp/%s" %(HOMEDIR, v["path"].split("/")[1]) - with open(path, "w") as config_change: - index = modifications["index"] - rpl = new_config[index].split(" ") - ip_insert = "%s %s %s" %(rpl[0], v["ip"], rpl[2]) - new_config[index] = ip_insert - config_change.writelines(new_config) - - self.qomui_service.copy_rootdir("CHANGE_%s" %provider, "%s/temp" %(HOMEDIR)) + try: + if provider in SUPPORTED_PROVIDERS: + temp_file = "%s/temp/%s_config" %(HOMEDIR, provider) + with open(temp_file, "w") as config_change: + config_change.writelines(new_config) + else: + temp_file = "%s/temp/%s" %(HOMEDIR, val["path"].split("/")[1]) + if modifications["apply_all"] == 1: + for k, v in self.server_dict.items(): + if v["provider"] == provider: + path = "%s/temp/%s" %(HOMEDIR, v["path"].split("/")[1]) + with open(path, "w") as config_change: + index = modifications["index"] + rpl = new_config[index].split(" ") + ip_insert = "%s %s %s" %(rpl[0], v["ip"], rpl[2]) + new_config[index] = ip_insert + config_change.writelines(new_config) + + self.qomui_service.copy_rootdir("CHANGE_%s" %provider, "%s/temp" %(HOMEDIR)) + + except FileNotFoundError: + pass def search_listitem(self, key): for row in range(self.serverListWidget.count()): @@ -2298,8 +2309,9 @@ def retranslateUi(self, ConnectionWidget): self.uploadLabel.setText(_translate("ConnectionWidget", "Upload:", None)) self.timeLabel.setText(_translate("ConnectionWidget", "Time:", None)) - def setText(self, server_dict, hop_dict, tun): + def setText(self, server_dict, hop_dict, tun, tun_hop=None): self.tun = tun + self.tun_hop = tun_hop self.statusLabel.setText("Active Connection") city = self.city_port_label(server_dict) self.ServerWidget.setText(server_dict["name"], server_dict["provider"], @@ -2318,7 +2330,7 @@ def setText(self, server_dict, hop_dict, tun): self.hopActiveLabel.setVisible(False) self.ServerWidget.hide_button(0) - self.calcThread = NetMon(self.tun) + self.calcThread = NetMon(self.tun, self.tun_hop) self.calcThread.stat.connect(self.show_stats) self.calcThread.ip.connect(self.show_ip) self.calcThread.time.connect(self.update_time) @@ -2370,8 +2382,8 @@ def __init__ (self, parent=None): def setupUi(self, LineWidget): self.setAutoFillBackground(True) - self.setFixedHeight(2) - self.setBackgroundRole(self.palette().Highlight) + self.setFixedHeight(1) + #self.setBackgroundRole(self.palette().Highlight) class NetMon(QtCore.QThread): stat = QtCore.pyqtSignal(list) @@ -2379,9 +2391,10 @@ class NetMon(QtCore.QThread): time = QtCore.pyqtSignal(str) lost = QtCore.pyqtSignal() - def __init__(self, tun): + def __init__(self, tun, tun_hop=None): QtCore.QThread.__init__(self) self.tun = tun + self.tun_hop = tun_hop def run(self): connected = True @@ -2412,6 +2425,8 @@ def run(self): try: counter = psutil.net_io_counters(pernic=True)[self.tun] + if self.tun_hop is not None: + tun_hop_test = psutil.net_io_counters(pernic=True)[self.tun_hop] t1 = time.time() stat = (counter.bytes_recv, counter.bytes_sent) DLrate, ULrate = [(now - last) / (t1 - t0) / 1024.0 for now, last in zip(stat, last_stat)] diff --git a/qomui/qomui_service.py b/qomui/qomui_service.py index 83bb8e7..b7848fc 100755 --- a/qomui/qomui_service.py +++ b/qomui/qomui_service.py @@ -40,6 +40,7 @@ class QomuiDbus(dbus.service.Object): firewall_opt = 1 hop_dict = {"none" : "none"} tun = "tun0" + tun_hop = "tun0" connect_status = 0 config = {} wg_connect = 0 @@ -121,9 +122,12 @@ def disable_ipv6(self, i): disable_ipv6 = Popen(['sysctl', '-w', 'net.ipv6.conf.all.disable_ipv6=0']) self.logger.info('(Re-)enabled ipv6') - @dbus.service.method(BUS_NAME, in_signature='', out_signature='s') - def return_tun_device(self): - return self.tun + @dbus.service.method(BUS_NAME, in_signature='s', out_signature='s') + def return_tun_device(self, tun): + if tun == "tun": + return self.tun + elif tun == "hop": + return self.tun_hop @dbus.service.method(BUS_NAME, in_signature='', out_signature='') def disconnect(self): @@ -719,7 +723,12 @@ def ovpn(self, ovpn_file, h, cwd_ovpn): if self.dns_found == 0: self.update_dns() elif line.find('TUN/TAP device') != -1: - self.tun = line_format.split(" ")[3] + if h == "2": + self.tun = line_format.split(" ")[3] + elif h == "1": + self.tun_hop = line_format.split(" ")[3] + else: + self.tun = line_format.split(" ")[3] elif line.find('PUSH: Received control message:') != -1: dns_option_1 = line_format.find('dhcp-option') if dns_option_1 != -1: @@ -736,9 +745,26 @@ def ovpn(self, ovpn_file, h, cwd_ovpn): elif line.find("Restart pause, 10 second(s)") != -1: self.reply("fail1") self.logger.info("Connection attempt failed") + elif line.find("SIGTERM[soft,auth-failure]") != -1: + self.reply("fail1") + self.logger.info("Connection attempt failed") elif line.find('SIGTERM[soft,auth-failure]') != -1: self.reply("fail2") self.logger.info("Authentication error while trying to connect") + elif line.find('write UDP: Operation not permitted') != -1: + ips = [] + try: + hop_ip = self.hop_dict["ip"] + ips.append(hop_ip) + except: + pass + + remote_ip = self.ovpn_dict["ip"] + ips.append(remote_ip) + + for ip in ips: + rule = (['-I', 'OUTPUT', '1', '-d', '%s' %ip, '-j', 'ACCEPT']) + self.allow_ip(ip, rule) elif line == '': break line = ovpn_exe.stdout.readline() diff --git a/qomui/update.py b/qomui/update.py index a6802df..601f482 100644 --- a/qomui/update.py +++ b/qomui/update.py @@ -564,49 +564,66 @@ def __init__(self, username, password): self.proton_server_dict = {} def run(self): + + headers = {'x-pm-appversion': 'Other', + 'x-pm-apiversion': '3', + 'Accept': 'application/vnd.protonmail.v1+json' + } + try: - path = "%s/temp" %DIRECTORY - server_url = "https://account.protonvpn.com/api/vpn/servers" - get_servers = json.loads(requests.get(server_url).content.decode("utf-8")) - - for s in get_servers["Servers"]: - name = s["Domain"] - country = country_translate(s["Country"]) - ip = s["IPs"][0]["EntryIP"] - self.proton_server_dict[name] = {"name" : name, "country" : country, "city": "", "ip" : ip, "provider" : "ProtonVPN", "tunnel": "OpenVPN"} + with requests.Session() as self.session: + self.session.headers.update(headers) + path = "%s/temp" %DIRECTORY + api_url = "https://api.protonmail.ch/vpn/logicals" + get_servers = json.loads(self.session.get(api_url).content.decode("utf-8")) + + with open ("%s/s.json" %path, "w") as s: + json.dump(get_servers, s) - cert_url = "https://account.protonvpn.com/api/vpn/config?ID=34&Platform=Linux" - ovpn = requests.get(cert_url).content.decode("utf-8") + for s in get_servers["LogicalServers"]: + name = s["Domain"] + cc = s["ExitCountry"] + if cc == "UK": + cc = "GB" + country = country_translate(cc) + city = s["City"] + if city is None: + city = "" + + ip = s["Servers"][0]["EntryIP"] + self.proton_server_dict[name] = {"name" : name, "country" : country, "city": city, "ip" : ip, "provider" : "ProtonVPN", "tunnel": "OpenVPN"} - ca_cert = BeautifulSoup(ovpn, "lxml").find("ca") - with open("%s/proton_ca.crt" %path, "w") as ca: - ca.write(str(ca_cert)) - - ta_key = "\n%s" %ovpn.split("")[1] - with open("%s/proton_ta.key" %path, "w") as ta: - ta.write(str(ta_key)) + + cert_url = "https://account.protonvpn.com/api/vpn/config?ID=34&Platform=Linux" + ovpn = requests.get(cert_url).content.decode("utf-8") + + ca_cert = BeautifulSoup(ovpn, "lxml").find("ca") + with open("%s/proton_ca.crt" %path, "w") as ca: + ca.write(str(ca_cert)) - with open("%s/proton_userpass.txt" %path, "w") as up: - up.write('%s\n%s' % (self.username, self.password)) + ta_key = "\n%s" %ovpn.split("")[1] + with open("%s/proton_ta.key" %path, "w") as ta: + ta.write(str(ta_key)) + + with open("%s/proton_userpass.txt" %path, "w") as up: + up.write('%s\n%s' % (self.username, self.password)) + + self.proton_protocol_dict = {"protocol_1" : {"protocol": "UDP", "port": "1194"}, + "protocol_2" : {"protocol": "TCP", "port": "443"} + } - self.proton_protocol_dict = {"protocol_1" : {"protocol": "UDP", "port": "1194"}, - "protocol_2" : {"protocol": "TCP", "port": "443"} - } + proton_dict = {"server" : self.proton_server_dict, + "protocol" : self.proton_protocol_dict, + "provider" : "ProtonVPN", + "path" : path + } + + self.down_finished.emit(proton_dict) - proton_dict = {"server" : self.proton_server_dict, - "protocol" : self.proton_protocol_dict, - "provider" : "ProtonVPN", - "path" : path - } - - self.down_finished.emit(proton_dict) - except requests.exceptions.RequestException as e: self.importFail.emit("Network error: no internet connection") - - class AddFolder(QtCore.QThread): down_finished = QtCore.pyqtSignal(dict) importFail = QtCore.pyqtSignal(str) diff --git a/resources/Mullvad_config b/resources/Mullvad_config index ecfaf4b..8d7c40c 100644 --- a/resources/Mullvad_config +++ b/resources/Mullvad_config @@ -9,7 +9,8 @@ persist-tun remote-cert-tls server cipher AES-256-CBC comp-lzo -tun-ipv6 +fast-io +ping-restart 60 sndbuf 524288 rcvbuf 524288 verb 3 diff --git a/resources/Windscribe_config b/resources/Windscribe_config index 86dfe1e..3d27483 100644 --- a/resources/Windscribe_config +++ b/resources/Windscribe_config @@ -5,7 +5,9 @@ remote nobind resolv-retry infinite auth SHA512 -cipher AES-256-CBC + auth-nocache +cipher AES-256-GCM +ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM comp-lzo verb 3 mute-replay-warnings diff --git a/setup.py b/setup.py index 473b951..bf7e015 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import glob import os -VERSION = "0.6.1" +VERSION = "0.6.2" data_files = [ ('/usr/share/applications/', ['resources/qomui.desktop']),