diff options
author | kali kaneko (leap communications) <kali@leap.se> | 2021-06-14 21:45:48 +0200 |
---|---|---|
committer | kali kaneko (leap communications) <kali@leap.se> | 2021-06-14 21:45:58 +0200 |
commit | 45939be0800f8cb77dcac854706ed1c7ac757931 (patch) | |
tree | 314b32733c5126c4b7f53349f2818f9fcf865b88 /helpers | |
parent | 1410e4cecccb860fc1dd29d1d4021ff72b5dc1ab (diff) |
[feat] allow to define explicitely allowed private address
By default, bitmask-root allows traffic to devices in local networks.
However, this behavior depends on it correctly identifying the local
network of the default route, and it can fail on more complex network
setups (one common failure mode is when one of the ifaces gets a
link-local ip).
This commit introduces an explicit mechanism, by parsing lines in
/etc/bitmask/ipv4.allow
/etc/bitmask/ipv6.allow
If valid private ips are defined in either of the files, the behavior
will change to fail close for local devices, and allow traffic (both tcp
and udp) to the defined ips, on all ports.
- Resolves: #503
Diffstat (limited to 'helpers')
-rw-r--r-- | helpers/bitmask-root | 76 |
1 files changed, 68 insertions, 8 deletions
diff --git a/helpers/bitmask-root b/helpers/bitmask-root index 6615d3b..f105bfc 100644 --- a/helpers/bitmask-root +++ b/helpers/bitmask-root @@ -43,6 +43,7 @@ The `openvpn start` action is special: it calls exec on openvpn and replaces the current process. If the `restart` parameter is passed, the firewall will not be teared down in the case of an error during launch. """ +import ipaddress import os import re import signal @@ -83,7 +84,7 @@ def get_no_group_name(): def tostr(s): return s.decode('utf-8') -VERSION = "12" +VERSION = "13" SCRIPT = "bitmask-root" NAMESERVER_TCP = "10.41.0.1" NAMESERVER_UDP = "10.42.0.1" @@ -275,6 +276,29 @@ def get_process_list(): return filter(None, res) +def getIPv4AllowAddresses(): + lines = [] + try: + with open("/etc/bitmask/ipv4.allow", 'r') as f: + lines = [l.strip() for l in f.readlines()] + except FileNotFoundError: + return lines + + lines = filter(lambda x: ipaddress.ip_address(x).version == 4, lines) + return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) + +def getIPv6AllowAddresses(): + lines = [] + try: + with open("/etc/bitmask/ipv6.allow", 'r') as f: + lines = [l.strip() for l in f.readlines()] + except FileNotFoundError: + return lines + + lines = filter(lambda x: ipaddress.ip_address(x).version == 6, lines) + return list(filter(lambda x: ipaddress.ip_address(x).is_private, lines)) + + def run(command, *args, **options): """ Run an external command. @@ -655,6 +679,16 @@ def firewall_start(args): local_network_ipv6 = get_local_network_ipv6(default_device) gateways = get_gateways(args) + # allow local address in listed exception list + # this will allow all ports and both tcp and udp. + def allow4(ip): + ip4tables("--append", BITMASK_CHAIN, "--destination", ip, + "-o", default_device, "--jump", "ACCEPT") + + def allow6(ip): + ip6tables("--append", BITMASK_CHAIN, "--destination", ip, + "-o", default_device, "--jump", "ACCEPT") + # add custom chain "bitmask" to front of OUTPUT chain for both # the 'filter' and the 'nat' tables. if not ipv4_chain_exists(BITMASK_CHAIN): @@ -707,11 +741,14 @@ def firewall_start(args): "--protocol", "tcp", "--dport", "53", "--jump", "MASQUERADE") # allow local network traffic + + ipv4_exceptions = getIPv4AllowAddresses() if local_network_ipv4: - # allow local network destinations - ip4tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv4, "-o", default_device, - "--jump", "ACCEPT") + if len(ipv4_exceptions) == 0: + # allow all local network destinations if no explicit allow rules defined + ip4tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv4, "-o", default_device, + "--jump", "ACCEPT") # allow local network sources for DNS # (required to allow local network DNS that gets rewritten by NAT # to get passed through so that MASQUERADE can set correct source IP) @@ -731,10 +768,15 @@ def firewall_start(args): "--protocol", "udp", "--destination", "224.0.0.251", "--dport", "5353", "-o", default_device, "--jump", "RETURN") + + + ipv6_exceptions = getIPv6AllowAddresses() if local_network_ipv6: - ip6tables("--append", BITMASK_CHAIN, - "--destination", local_network_ipv6, "-o", default_device, - "--jump", "ACCEPT") + if len(ipv6_exceptions) == 0: + # allow all local network destinations if no explicit allow rules defined + ip6tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv6, "-o", default_device, + "--jump", "ACCEPT") # allow multicast Simple Service Discovery Protocol ip6tables("--append", BITMASK_CHAIN, "--protocol", "udp", @@ -751,12 +793,29 @@ def firewall_start(args): ip4tables("--append", BITMASK_CHAIN, "--destination", gateway, "-o", default_device, "--jump", "ACCEPT") + # TODO allow ipv6 traffic to gws too + # log rejected packets to syslog if DEBUG: iptables("--append", BITMASK_CHAIN, "-o", default_device, "--jump", "LOG", "--log-prefix", "iptables denied: ", "--log-level", "7") + # allow explicit private exceptions + if len(ipv4_exceptions) != 0: + for ip in ipv4_exceptions: + allow4(ip) + ip4tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv4, "-o", default_device, + "--jump", "REJECT") + + if len(ipv6_exceptions) != 0: + for ip in ipv6_exceptions: + allow6(ip) + ip6tables("--append", BITMASK_CHAIN, + "--destination", local_network_ipv6, "-o", default_device, + "--jump", "REJECT") + # for now, ensure all other ipv6 packets get rejected (regardless of # device). not sure why, but "-p any" doesn't work. ip6tables("--append", BITMASK_CHAIN, "-p", "tcp", "--jump", "REJECT") @@ -766,6 +825,7 @@ def firewall_start(args): ip4tables("--append", BITMASK_CHAIN, "-o", default_device, "--jump", "REJECT") + # On Qubes OS, add anti-leak rules for proxyVM qubes-firewall.service # Must stay on 'top' of chain! if QUBES_PROXY and QUBES_VER >= 3 and run("grep", "installed\ by\ " + |