From 45939be0800f8cb77dcac854706ed1c7ac757931 Mon Sep 17 00:00:00 2001
From: "kali kaneko (leap communications)" <kali@leap.se>
Date: Mon, 14 Jun 2021 21:45:48 +0200
Subject: [PATCH] [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 55e1f6b0..cb996edc 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 6615d3b7..f105bfc8 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\ " +
-- 
GitLab