summaryrefslogtreecommitdiff
path: root/pkg/linux/bitmask-root
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/linux/bitmask-root')
-rwxr-xr-xpkg/linux/bitmask-root346
1 files changed, 346 insertions, 0 deletions
diff --git a/pkg/linux/bitmask-root b/pkg/linux/bitmask-root
new file mode 100755
index 00000000..5b49a187
--- /dev/null
+++ b/pkg/linux/bitmask-root
@@ -0,0 +1,346 @@
+#!/usr/bin/python2
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+"""
+This is a privileged helper script for safely running certain commands as root.
+It should only be called by the Bitmask application.
+
+USAGE:
+ bitmask-root firewall stop
+ bitmask-root firewall start GATEWAY1 GATEWAY2 ...
+ bitmask-root openvpn stop
+ bitmask-root openvpn start CONFIG1 CONFIG1 ...
+"""
+# TODO should be tested with python3, which can be the default on some distro.
+
+from __future__ import print_function
+import os
+import subprocess
+import socket
+import sys
+import re
+
+##
+## CONSTANTS
+##
+
+OPENVPN = "/usr/sbin/openvpn"
+IPTABLES = "/sbin/iptables"
+IP6TABLES = "/sbin/ip6tables"
+UPDATE_RESOLV_CONF = "/etc/openvpn/update-resolv-conf"
+
+FIXED_FLAGS = [
+ "--setenv", "LEAPOPENVPN", "1",
+ "--nobind",
+ "--client",
+ "--dev", "tun",
+ "--tls-client",
+ "--remote-cert-tls", "server",
+ "--management-signal",
+ "--management", "/tmp/openvpn.socket", "unix",
+ "--up", UPDATE_RESOLV_CONF,
+ "--down", UPDATE_RESOLV_CONF,
+ "--script-security", "2"
+]
+
+ALLOWED_FLAGS = {
+ "--remote": ["IP", "NUMBER", "PROTO"],
+ "--tls-cipher": ["CIPHER"],
+ "--cipher": ["CIPHER"],
+ "--auth": ["CIPHER"],
+ "--management-client-user": ["USER"],
+ "--cert": ["FILE"],
+ "--key": ["FILE"],
+ "--ca": ["FILE"]
+}
+
+PARAM_FORMATS = {
+ "NUMBER": lambda s: re.match("^\d+$", s),
+ "PROTO": lambda s: re.match("^(tcp|udp)$", s),
+ "IP": lambda s: is_valid_address(s),
+ "CIPHER": lambda s: re.match("^[A-Z0-9-]+$", s),
+ "USER": lambda s: re.match("^[a-zA-Z0-9_\.\@][a-zA-Z0-9_\-\.\@]*\$?$", s), # IEEE Std 1003.1-2001
+ "FILE": lambda s: os.path.isfile(s)
+}
+
+DEBUG=os.getenv("DEBUG")
+if DEBUG:
+ import logging
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+ ch = logging.StreamHandler()
+ ch.setLevel(logging.DEBUG)
+ ch.setFormatter(formatter)
+ logger = logging.getLogger(__name__)
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(ch)
+ logger.debug(" ".join(sys.argv))
+
+##
+## UTILITY
+##
+
+def is_valid_address(value):
+ """
+ Validate that the passed ip is a valid IP address.
+
+ :param value: the value to be validated
+ :type value: str
+ :rtype: bool
+ """
+ try:
+ socket.inet_aton(value)
+ return True
+ except Exception:
+ print "MALFORMED IP: %s!" % value
+ return False
+
+def split_list(list, regex):
+ """
+ Splits a list based on a regex:
+ e.g. split_list(["xx", "yy", "x1", "zz"], "^x") => [["xx", "yy"], ["x1", "zz"]]
+
+ :param list: the list to be split.
+ :type list: list
+ :rtype: list
+ """
+ if not hasattr(regex, "match"):
+ regex = re.compile(regex)
+ result = []
+ i = 0
+ while True:
+ if regex.match(list[i]):
+ result.append([])
+ while True:
+ result[-1].append(list[i])
+ i += 1
+ if i >= len(list) or regex.match(list[i]):
+ break
+ else:
+ i += 1
+ if i >= len(list):
+ break
+ return result
+
+# i think this is not needed with shell=False
+#def sanify(command, *args):
+# return [command] + [pipes.quote(a) for a in args]
+
+def run(command, *args, **options):
+ parts = [command]
+ parts.extend(args)
+ if DEBUG:
+ print "run: " + " ".join(parts)
+ if options.get("check", True) == False or options.get("detach", False) == True:
+ subprocess.Popen(parts)
+ else:
+ try:
+ devnull = open('/dev/null', 'w')
+ subprocess.check_call(parts, stdout=devnull, stderr=devnull)
+ return 0
+ except subprocess.CalledProcessError as ex:
+ if options.get("exitcode", False) == True:
+ return ex.returncode
+ else:
+ bail("Could not run %s: %s" % (ex.cmd, ex.output))
+
+##
+## OPENVPN
+##
+
+def parse_openvpn_flags(args):
+ """
+ takes argument list from the command line and parses it, only allowing some configuration flags.
+ """
+ result = []
+ try:
+ for flag in split_list(args, "^--"):
+ flag_name = flag[0]
+ if ALLOWED_FLAGS.has_key(flag_name):
+ result.append(flag_name)
+ required_params = ALLOWED_FLAGS[flag_name]
+ if len(required_params) > 0:
+ flag_params = flag[1:]
+ if len(flag_params) != len(required_params):
+ print "ERROR: not enough params for %s" % flag_name
+ return None
+ for param, param_type in zip(flag_params, required_params):
+ if PARAM_FORMATS[param_type](param):
+ result.append(param)
+ else:
+ print "ERROR: Bad argument %s" % param
+ return None
+ else:
+ print "WARNING: unrecognized openvpn flag %s" % flag_name
+ return result
+ except Exception as ex:
+ print ex
+ return None
+
+
+def openvpn_start(args):
+ openvpn_flags = parse_openvpn_flags(args)
+ if openvpn_flags:
+ flags = FIXED_FLAGS + openvpn_flags
+ run(OPENVPN, *flags, detach=True)
+ else:
+ bail('ERROR: could not parse openvpn options')
+
+def openvpn_stop(args):
+ print "stop"
+
+##
+## FIREWALL
+##
+
+def get_gateways(gateways):
+ result = [gateway for gateway in gateways if is_valid_address(gateway)]
+ if not len(result):
+ bail("No valid gateways specified")
+ else:
+ return result
+
+def get_default_device():
+ routes = subprocess.check_output([IP, "route", "show"])
+ match = re.search("^default .*dev ([^\s]*) .*$", routes, flags=re.M)
+ if len(match.groups()) >= 1:
+ return match.group(1)
+ else:
+ bail("could not find default device")
+
+def get_local_network_ipv4(device):
+ addresses = subprocess.check_output([IP, "-o", "address", "show", "dev", device])
+ match = re.search("^.*inet ([^ ]*) .*$", addresses, flags=re.M)
+ if len(match.groups()) >= 1:
+ return match.group(1)
+ else:
+ return None
+
+def get_local_network_ipv6(device):
+ addresses = subprocess.check_output([IP, "-o", "address", "show", "dev", device])
+ match = re.search("^.*inet6 ([^ ]*) .*$", addresses, flags=re.M)
+ if len(match.groups()) >= 1:
+ return match.group(1)
+ else:
+ return None
+
+def run_iptable_with_check(cmd, *args, **options):
+ """
+ runs an iptables command checking to see if it should:
+ for --insert: run only if rule does not already exist.
+ for --delete: run only if rule does exist.
+ other commands are run normally.
+ """
+ if "--insert" in args:
+ check_args = [arg.replace("--insert", "--check") for arg in args]
+ check_code = run(cmd, *check_args, exitcode=True)
+ if check_code != 0:
+ run(cmd, *args, **options)
+ elif "--delete" in args:
+ check_args = [arg.replace("--delete", "--check") for arg in args]
+ check_code = run(cmd, *check_args, exitcode=True)
+ if check_code == 0:
+ run(cmd, *args, **options)
+ else:
+ run(cmd, *args, **options)
+
+def iptables(*args, **options):
+ ip4tables(*args, **options)
+ ip6tables(*args, **options)
+
+def ip4tables(*args, **options):
+ run_iptable_with_check(IPTABLES, *args, **options)
+
+def ip6tables(*args, **options):
+ run_iptable_with_check(IP6TABLES, *args, **options)
+
+def ipv4_chain_exists(table):
+ code = run(IPTABLES, "--list", table, "--numeric", exitcode=True)
+ return code == 0
+
+def ipv6_chain_exists(table):
+ code = run(IP6TABLES, "--list", table, "--numeric", exitcode=True)
+ return code == 0
+
+def firewall_start(args):
+ default_device = get_default_device()
+ local_network_ipv4 = get_local_network_ipv4(default_device)
+ local_network_ipv6 = get_local_network_ipv6(default_device)
+ gateways = get_gateways(args)
+
+ # add custom chain "bitmask"
+ if not ipv4_chain_exists("bitmask"):
+ ip4tables("--new-chain", "bitmask")
+ if not ipv6_chain_exists("bitmask"):
+ ip6tables("--new-chain", "bitmask")
+ iptables("--insert", "OUTPUT", "--jump", "bitmask")
+
+ # reject everything
+ iptables("--insert", "bitmask", "-o", default_device, "--jump", "REJECT")
+
+ # allow traffic to gateways
+ for gateway in gateways:
+ ip4tables("--insert", "bitmask", "--destination", gateway, "-o", default_device, "--jump", "ACCEPT")
+
+ # allow traffic to IPs on local network
+ if local_network_ipv4:
+ ip4tables("--insert", "bitmask", "--destination", local_network_ipv4, "-o", default_device, "--jump", "ACCEPT")
+ if local_network_ipv6:
+ ip6tables("--insert", "bitmask", "--destination", local_network_ipv6, "-o", default_device, "--jump", "ACCEPT")
+
+ # block DNS requests to anyone but the service provider or localhost
+ ip4tables("--insert", "bitmask", "--protocol", "udp", "--dport", "53", "--jump", "REJECT")
+ for allowed_dns in gateways + ["127.0.0.1","127.0.1.1"]:
+ ip4tables("--insert", "bitmask", "--protocol", "udp", "--dport", "53", "--destination", allowed_dns, "--jump", "ACCEPT")
+
+def firewall_stop(args):
+ iptables("--delete", "OUTPUT", "--jump", "bitmask")
+ if ipv4_chain_exists("bitmask"):
+ ip4tables("--flush", "bitmask")
+ ip4tables("--delete-chain", "bitmask")
+ if ipv6_chain_exists("bitmask"):
+ ip6tables("--flush", "bitmask")
+ ip6tables("--delete-chain", "bitmask")
+
+
+def bail(msg=""):
+ if msg:
+ print(msg)
+ exit(1)
+
+def main():
+ if len(sys.argv) >= 3:
+ command = "_".join(sys.argv[1:3])
+ args = sys.argv[3:]
+ if command == "openvpn_start":
+ openvpn_start(args)
+ elif command == "openvpn_stop":
+ openvpn_stop(args)
+ elif command == "firewall_start":
+ firewall_start(args)
+ elif command == "firewall_stop":
+ firewall_stop(args)
+ else:
+ bail("no such command")
+ else:
+ bail("no such command")
+
+if __name__ == "__main__":
+ main()
+ print "done"
+ exit(0)
+