From 45939be0800f8cb77dcac854706ed1c7ac757931 Mon Sep 17 00:00:00 2001 From: "kali kaneko (leap communications)" Date: Mon, 14 Jun 2021 21:45:48 +0200 Subject: [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 --- debian/changelog | 2 +- helpers/bitmask-root | 76 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/debian/changelog b/debian/changelog index 55e1f6b..cb996ed 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -riseup-vpn (0.21.2.5) groovy; urgency=medium +riseup-vpn (0.21.2.6) groovy; urgency=medium * Initial Release. 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\ " + -- cgit v1.2.3