From 2e756314dde91f447c465af80553ac37586aa9d4 Mon Sep 17 00:00:00 2001 From: corrad1nho Date: Mon, 30 Jul 2018 19:26:13 +0200 Subject: [PATCH] version 0.6.3 --- CHANGELOG.md | 8 ++++ README.md | 22 +++++----- VERSION | 2 +- qomui/bypass.py | 71 +++++++++++++++++++++++++------- qomui/qomui_gui.py | 50 +++++++++++++++++------ qomui/qomui_service.py | 91 ++++++++++++++++++++++++++++++++++-------- setup.py | 2 +- 7 files changed, 187 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0176786..92aa643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ##Changelog +version 0.6.3: +- [change] bypass mode supports ipv6 now +- [change] alternative DNS servers are used for bypass +- [change] WireGuard is now written correctly (pull request from zx2c4) - requires all WireGuard configs to be readded +- [change] exit dialog has a 5 sec timeout now +- [change] umask set before chmod to avoid race conditions (pull request from zx2c4) +- [bugfix] bypass should now work properly with WireGuard connections + 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) diff --git a/README.md b/README.md index 95e110b..84782b9 100755 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ Screenshots were taken on Arch Linux/Plasma Arc Dark Theme - Qomui will adapt to #### Ubuntu -Download and install [DEB-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.2/qomui-0.6.2-amd64.deb) +Download and install [DEB-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.3/qomui-0.6.3-amd64.deb) #### Fedora -Download and install [RPM-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.2/qomui-0.6.2-1.x86_64.rpm) +Download and install [RPM-Package](https://github.com/corrad1nho/qomui/releases/download/v0.6.3/qomui-0.6.3-1.x86_64.rpm) #### Arch @@ -111,6 +111,13 @@ 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.3: +- [change] bypass mode supports ipv6 now +- [change] WireGuard is now written correctly (pull request from zx2c4) - requires all WireGuard configs to be readded +- [change] exit dialog has a 5 sec timeout now +- [change] umask set before chmod to avoid race conditions (pull request from zx2c4) +- [bugfix] bypass should now work properly with WireGuard connections + 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) @@ -120,14 +127,3 @@ version 0.6.2: - [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 -- [change] missing flags for Windscribe added -- [change] autocompletion for "c" and "v" options in cli -- [change] most cli commands are not case-sensitive anymore -- [bugfix] alternative dns servers not parsed correctly -- [bugfix] crashes when loading default configuration -- [bugfix] configs are not imported if url cannot be resolved -- [bugfix] old connection not killed after network change detected (in rare cases) - diff --git a/VERSION b/VERSION index b1d7abc..a0a1517 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.2 \ No newline at end of file +0.6.3 \ No newline at end of file diff --git a/qomui/bypass.py b/qomui/bypass.py index c013c45..e8bff2b 100644 --- a/qomui/bypass.py +++ b/qomui/bypass.py @@ -11,9 +11,9 @@ default_interface = None ROOTDIR = "/usr/share/qomui/" -def create_cgroup(user, group, default_interface, default_gateway): +def create_cgroup(user, group, default_interface, default_gateway, default_interface_6, default_gateway_6): - cleanup = delete_cgroup(default_interface) + cleanup = delete_cgroup(default_interface, default_interface_6) cgroup_iptables = [["-t", "mangle", "-A", "OUTPUT", "-m", "cgroup", "--cgroup", "0x00110011", "-j", "MARK", "--set-mark", "11"], @@ -46,30 +46,66 @@ def create_cgroup(user, group, default_interface, default_gateway): for rule in cgroup_iptables: firewall.add_rule(rule) + if default_gateway_6 != "None": + cgroup_iptables.pop(1) + cgroup_iptables.insert(1, ["-t", "nat", "-A", "POSTROUTING", "-m", "cgroup", + "--cgroup", "0x00110011", "-o", "%s" %default_interface_6 , "-j", "MASQUERADE"]) + + for rule in cgroup_iptables: + firewall.add_rule_6(rule) + + else: + logging.debug("Blocking ipv6 via bypass_qomui") + cgroup_iptables.pop(1) + cgroup_iptables.insert(1, ["-t", "nat", "-A", "POSTROUTING", "-m", "cgroup", + "--cgroup", "0x00110011", "-o", "%s" %default_interface , "-j", "MASQUERADE"]) + cgroup_iptables.pop(2) + cgroup_iptables.insert(2, ["-I", "OUTPUT", "1", "-m", "cgroup", "--cgroup", "0x00110011", "-j", "DROP"]) + cgroup_iptables.pop(3) + cgroup_iptables.insert(3, ["-I", "OUTPUT", "1", "-m", "cgroup", "--cgroup", "0x00110011", "-j", "DROP"]) + + for rule in cgroup_iptables: + firewall.add_rule_6(rule) + + route_cmds = [ + ["ip", "route", "flush", "table", "bypass_qomui"], + ["ip", "rule", "add", "fwmark", "11", "table", "bypass_qomui"] + ] + + try: + check_call(["ip", "route", "flush", "table", "bypass_qomui"]) + except CalledProcessError: + pass + try: check_call(["ip", "route", "flush", "table", "bypass_qomui"]) check_call(["ip", "rule", "add", "fwmark", "11", "table", "bypass_qomui"]) - check_call(["ip", "route", "add", "default", "via", "%s" %default_gateway, "table", "bypass_qomui"]) + check_call(["ip", "route", "add", "default", "via", + "%s" %default_gateway, "dev", "%s" %default_interface, "table", "bypass_qomui"]) + except CalledProcessError: + logging.error("Could not set ipv4 routes for cgroup") + + try: + check_call(["ip", "-6", "route", "flush", "table", "bypass_qomui"]) + check_call(["ip", "-6", "rule", "add", "fwmark", "11", "table", "bypass_qomui"]) + check_call(["ip", "-6", "route", "add", "default", "via", + "%s" %default_gateway_6, "dev", "%s" %default_interface_6, "table", "bypass_qomui"]) + except CalledProcessError: + logging.error("Could not set ipv6 routes for cgroup") + + try: check_call(["cgcreate", "-t", "%s:%s" %(user, group), "-a" "%s:%s" %(user, group), "-g", "net_cls:bypass_qomui"]) except CalledProcessError: - self.logger.error("Configuration of cgroup failed") + logging.error("Creating cgroup failed") with open ("/proc/sys/net/ipv4/conf/all/rp_filter", 'w') as rp_edit_all: rp_edit_all.write("2") with open ("/proc/sys/net/ipv4/conf/%s/rp_filter" %default_interface, 'w') as rp_edit_int: rp_edit_int.write("2") - logging.info("Succesfully create cgroup to bypass OpenVPN tunnel") + logging.info("Succesfully created cgroup to bypass OpenVPN tunnel") - try: - dnsmasq = Popen(["dnsmasq", "--port=5354", "--interface=%s" %default_interface]) - - return dnsmasq.pid + 2 - except CalledProcessError: - logging.error("Failed to start dnsmasq for cgroup qomui_bypass") - return None - -def delete_cgroup(default_interface): +def delete_cgroup(default_interface, default_interface_6): cgroup_iptables_del = [["-t", "mangle", "-D", "OUTPUT", "-m", "cgroup", "--cgroup", "0x00110011", "-j", "MARK", "--set-mark", "11"], @@ -99,6 +135,13 @@ def delete_cgroup(default_interface): for rule in cgroup_iptables_del: firewall.add_rule(rule) + cgroup_iptables_del.pop(1) + cgroup_iptables_del.insert(1, ["-t", "nat", "-D", "POSTROUTING", "-m", "cgroup", + "--cgroup", "0x00110011", "-o", "%s" %default_interface_6 , "-j", "MASQUERADE"]) + + for rule in cgroup_iptables_del: + firewall.add_rule_6(rule) + try: os.rmdir(cgroup_path) except (OSError, FileNotFoundError): diff --git a/qomui/qomui_gui.py b/qomui/qomui_gui.py index c231f84..e4d1c8c 100755 --- a/qomui/qomui_gui.py +++ b/qomui/qomui_gui.py @@ -96,7 +96,6 @@ def leaveEvent(self, event): def sizeHint(self): return QtCore.QSize(25, 25) - class QomuiGui(QtWidgets.QWidget): status = "inactive" server_dict = {} @@ -965,20 +964,45 @@ def restoreUi(self, reason): self.showNormal() def closeEvent(self, event): - ret = self.messageBox("Do you want to exit program or minimize to tray?", - "", - buttons=["Minimize", "Exit", "Cancel"], - icon="Question" - ) - if ret == 1: - self.tray.hide() - self.kill() - event.accept() + self.exit_event = event + self.confirm = QtWidgets.QMessageBox() + self.timeout = 5 + self.confirm.setText("Do you want to exit program or minimize to tray?") + info = "Closing in %s seconds" %self.timeout + self.confirm.setInformativeText(info) + self.confirm.setIcon(QtWidgets.QMessageBox.Question) + self.confirm.addButton(QtWidgets.QPushButton("Minimize"), QtWidgets.QMessageBox.NoRole) + self.confirm.addButton(QtWidgets.QPushButton("Exit"), QtWidgets.QMessageBox.YesRole) + self.confirm.addButton(QtWidgets.QPushButton("Cancel"), QtWidgets.QMessageBox.RejectRole) + self.timer = QtCore.QTimer(self) + self.timer.setInterval(1000) + self.timer.timeout.connect(self.change_timeout) + self.timer.start() + + ret = self.confirm.exec_() + self.timer.stop() + + if ret == 2: + self.exit_event.ignore() + elif ret == 0: - event.ignore() self.hide() - elif ret == 2: - event.ignore() + + elif ret == 1: + self.tray.hide() + self.kill() + self.exit_event.accept() + + def change_timeout(self): + self.timeout -= 1 + info = "Closing in %s seconds" %self.timeout + self.confirm.setInformativeText(info) + if self.timeout <= 0: + self.timer.stop() + self.confirm.hide() + self.tray.hide() + self.kill() + self.exit_event.accept() def load_json(self, json_file): try: diff --git a/qomui/qomui_service.py b/qomui/qomui_service.py index cecbf39..2af2989 100755 --- a/qomui/qomui_service.py +++ b/qomui/qomui_service.py @@ -13,7 +13,7 @@ import logging.handlers import json import signal -from subprocess import Popen, PIPE, check_output, CalledProcessError, STDOUT +from subprocess import Popen, PIPE, check_output, check_call, CalledProcessError, STDOUT import dbus import dbus.service from dbus.mainloop.pyqt5 import DBusQtMainLoop @@ -348,22 +348,47 @@ def bypass(self, ug): except AttributeError: pass - default_gateway = self.default_gateway_check()["gateway"] - if default_gateway != "None": + default_routes = self.default_gateway_check() + default_gateway_4 = default_routes["gateway"] + default_gateway_6 = default_routes["gateway_6"] + self.default_interface_4 = default_routes["interface"] + self.default_interface_6 = default_routes["interface_6"] + + if default_gateway_4 != "None" or default_gateway_6 != "None": try: if self.config["bypass"] == 1: - pid = bypass.create_cgroup(self.ug["user"], self.ug["group"], - self.default_interface, default_gateway + bypass.create_cgroup(self.ug["user"], self.ug["group"], + self.default_interface_4, default_gateway_4, + self.default_interface_6, default_gateway_6 ) - self.dnsmasq_pid = (pid, "dnsmasq") - self.logger.debug("dnsmasq-PID = %s" %pid) + + if self.default_interface_4 != "None": + interface = self.default_interface_4 + + else: + interface = self.default_interface_6 + + try: + dnsmasq = Popen(["dnsmasq", "--port=5354", "--interface=%s" %interface, + "--server=%s" %self.config["alt_dns1"], + "--server=%s" %self.config["alt_dns2"] + ]) + + self.logger.debug(dnsmasq.pid +2) + self.dnsmasq_pid = (dnsmasq.pid +2, "dnsmasq") + + except CalledProcessError: + logging.error("Failed to start dnsmasq for cgroup qomui_bypass") + elif self.config["bypass"] == 0: + try: - bypass.delete_cgroup(self.default_interface) + bypass.delete_cgroup(self.default_interface_4, self.default_interface_6) except AttributeError: pass + except KeyError: - self.logger.warning('Could not read all values from file') + self.logger.warning('Config file corrupted - bypass option does not exist') @dbus.service.method(BUS_NAME, in_signature='', out_signature='a{ss}') def default_gateway_check(self): @@ -371,13 +396,28 @@ def default_gateway_check(self): route_cmd = ["ip", "route", "show", "default", "0.0.0.0/0"] default_route = check_output(route_cmd).decode("utf-8") parse_route = default_route.split(" ") - self.default_interface = parse_route[4] - default_gateway = parse_route[2] - default_interface = parse_route[4] - return {"gateway" : default_gateway, "interface" : default_interface} + default_gateway_4 = parse_route[2] + default_interface_4 = parse_route[4] + except (CalledProcessError, IndexError): self.logger.info('Could not identify default gateway - no network connectivity') - return {"gateway" : "None", "interface" : "None"} + default_gateway_4 = "None" + default_interface_4 = "None" + + try: + route_cmd = ["ip", "-6", "route", "show", "default", "::/0"] + default_route = check_output(route_cmd).decode("utf-8") + parse_route = default_route.split(" ") + default_gateway_6 = parse_route[2] + default_interface_6 = parse_route[4] + + except (CalledProcessError, IndexError): + self.logger.info('Could not identify default gateway for ipv6 - no network connectivity') + default_gateway_6 = "None" + default_interface_6 = "None" + + return {"gateway" : default_gateway_4, "gateway_6" : default_gateway_6, + "interface" : default_interface_4, "interface_6" : default_interface_6} @dbus.service.signal(BUS_NAME, signature='s') def reply(self, msg): @@ -475,8 +515,8 @@ def wireguard(self): else: shutil.copyfile("%s/%s" %(ROOTDIR, self.ovpn_dict["path"]), path) - os.umask(oldmask) + os.umask(oldmask) Popen(['chmod', '0600', path]) self.wg(path) @@ -655,8 +695,8 @@ def wg(self, wg_file): name = self.ovpn_dict["name"] self.logger.info("Establishing connection to %s" %name) - wg_rules = [["-I", "INPUT", "1", "-i", "wg_qomui", "-j", "ACCEPT"], - ["-I", "OUTPUT", "1", "-o", "wg_qomui", "-j", "ACCEPT"] + wg_rules = [["-I", "INPUT", "2", "-i", "wg_qomui", "-j", "ACCEPT"], + ["-I", "OUTPUT", "2", "-o", "wg_qomui", "-j", "ACCEPT"] ] for rule in wg_rules: @@ -671,6 +711,23 @@ def wg(self, wg_file): for line in cmd_wg.stdout: logging.info(line) self.wg_connect = 1 + + #Necessary, otherwise bypass mode breaks + if self.config["bypass"] == 1: + + try: + check_call(["ip", "rule", "del", "fwmark", "11", "table", "bypass_qomui"]) + check_call(["ip", "-6", "rule", "del", "fwmark", "11", "table", "bypass_qomui"]) + except CalledProcessError: + pass + + try: + check_call(["ip", "rule", "add", "fwmark", "11", "table", "bypass_qomui"]) + check_call(["ip", "-6", "rule", "add", "fwmark", "11", "table", "bypass_qomui"]) + self.logger.debug("Packet classification for bypass table reset") + except CalledProcessError: + self.logger.warning("Could not reset packet classification for bypass table") + self.reply("success") except (CalledProcessError, FileNotFoundError): diff --git a/setup.py b/setup.py index bf7e015..e678b0a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import glob import os -VERSION = "0.6.2" +VERSION = "0.6.3" data_files = [ ('/usr/share/applications/', ['resources/qomui.desktop']),