From 73b3344f96c7b6b918d8571cce2b9125898bf740 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Sun, 5 Feb 2023 14:40:31 -0500 Subject: [PATCH 1/4] Use appropriate rpm macros No functional change intended. --- Makefile | 1 + rpm_spec/core-agent.spec.in | 130 ++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 65 deletions(-) diff --git a/Makefile b/Makefile index fc4958ff8..475c2121d 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ VERSION := $(file Date: Sat, 4 Feb 2023 23:35:35 -0500 Subject: [PATCH 2/4] Replace iptables with nftables nftables is the modern replacement for iptables. It has more features and is actively maintained. iptables is deprecated and may be removed from distributions in the future. Instead of a 1-to-1 translation, update the code to use nftables features such as sets and maps. This makes anti-spoofing checks either O(1) or O(log N) (depending on how sets and maps are implemented) in the number of downstream network interfaces. The masquerading path benefits even more, as it now uses netdev rules to directly forward a packet to a specific destination while statelessly changing its addresses. This also removes the legacy iptables support from the firewall daemon. Since the anti-spoofing rules require nftables, supporting iptables in the firewall daemon makes no sense anymore. Furthermore, software flow offload allows NATd traffic to bypass all layer 3 processing. This is implemented using flowtables, which use the ingress hook and therefore come before the prerouting hook. DNS is now handled entirely in Python, rather than by a combination of Python and Bash scripts. This allows much better error checking and makes the code actually possible to understand. It will also be much more extensible in the future. As just one example, this Python code could be used by the firewall daemon to automatically update DNS in response to systemd-resolved events. Instead of using network interface names, the nftables rules use interface groups. This should speed up processing (though no benchmarks have been done) and avoids problems if interface names change in the future. The only exception is the prerouting hooks, which use interface names to ensure that interface ID reuse does not cause problems. The new anti-spoofing rules are deny-by-default. If there is no entry in the "allowed" nftables map for a given IP protocol, all traffic with that protocol will be considered spoofed and dropped. This avoids having to explicitly disable IPv6 on interfaces with no ipv6 addresses. --- Makefile | 9 +- debian/qubes-core-agent-networking.install | 9 +- network/Makefile | 1 - network/get-dns-from-resolved | 49 ---- network/qubes-antispoof.nft | 42 ++++ network/qubes-iptables | 25 +- network/qubes-ipv4.nft | 35 +++ network/qubes-ipv6-disabled.nft | 13 + network/qubes-ipv6.nft | 40 +++ network/qubes-setup-dnat-to-ns | 143 ++++++++--- network/setup-ip | 3 +- network/vif-qubes-nat.sh | 54 ++--- network/vif-route-qubes | 71 ++++-- qubesagent/firewall.py | 269 +-------------------- qubesagent/test_firewall.py | 263 +------------------- rpm_spec/core-agent.spec.in | 13 +- selinux/qubes-xendriverdomain.fc | 6 +- vm-systemd/75-qubes-vm.preset | 1 + vm-systemd/qubes-antispoof.service | 10 + vm-systemd/qubes-core-agent-linux.tmpfiles | 1 + vm-systemd/qubes-iptables.service | 2 + vm-systemd/qubes-sysinit.sh | 2 +- 22 files changed, 360 insertions(+), 701 deletions(-) delete mode 100755 network/get-dns-from-resolved create mode 100644 network/qubes-antispoof.nft create mode 100644 network/qubes-ipv4.nft create mode 100644 network/qubes-ipv6-disabled.nft create mode 100644 network/qubes-ipv6.nft create mode 100644 vm-systemd/qubes-antispoof.service diff --git a/Makefile b/Makefile index 475c2121d..e95dd1103 100644 --- a/Makefile +++ b/Makefile @@ -125,7 +125,7 @@ endif # Systemd service files SYSTEMD_ALL_SERVICES := $(wildcard vm-systemd/qubes-*.service) vm-systemd/dev-xvdc1-swap.service -SYSTEMD_NETWORK_SERVICES := vm-systemd/qubes-firewall.service vm-systemd/qubes-iptables.service vm-systemd/qubes-updates-proxy.service +SYSTEMD_NETWORK_SERVICES := vm-systemd/qubes-firewall.service vm-systemd/qubes-iptables.service vm-systemd/qubes-updates-proxy.service vm-systemd/qubes-antispoof.service SYSTEMD_SELINUX_SERVICES := vm-systemd/qubes-relabel-root.service vm-systemd/qubes-relabel-rw.service SYSTEMD_CORE_SERVICES := $(filter-out $(SYSTEMD_NETWORK_SERVICES) $(SYSTEMD_SELINUX_SERVICES), $(SYSTEMD_ALL_SERVICES)) @@ -209,9 +209,10 @@ install-netvm: install-systemd-networking-dropins install-networkmanager install -m 0644 -D network/tinyproxy-updates.conf $(DESTDIR)/etc/tinyproxy/tinyproxy-updates.conf install -m 0644 -D network/updates-blacklist $(DESTDIR)/etc/tinyproxy/updates-blacklist - install -m 0400 -D network/iptables $(DESTDIR)/etc/qubes/iptables.rules - install -m 0400 -D network/ip6tables $(DESTDIR)/etc/qubes/ip6tables.rules - install -m 0400 -D network/ip6tables-enabled $(DESTDIR)/etc/qubes/ip6tables-enabled.rules + install -m 0400 -D network/qubes-ipv4.nft $(DESTDIR)/etc/qubes/qubes-ipv4.nft + install -m 0400 -D network/qubes-ipv6.nft $(DESTDIR)/etc/qubes/qubes-ipv6.nft + install -m 0400 -D network/qubes-antispoof.nft $(DESTDIR)/etc/qubes/qubes-antispoof.nft + install -m 0400 -D network/qubes-ipv6-disabled.nft $(DESTDIR)/etc/qubes/qubes-ipv6-disabled.nft install -m 0755 -D qubes-rpc/qubes.UpdatesProxy $(DESTDIR)/etc/qubes-rpc/qubes.UpdatesProxy diff --git a/debian/qubes-core-agent-networking.install b/debian/qubes-core-agent-networking.install index 4fdd0810b..d9c675d58 100644 --- a/debian/qubes-core-agent-networking.install +++ b/debian/qubes-core-agent-networking.install @@ -1,8 +1,9 @@ etc/dhclient.d/qubes-setup-dnat-to-ns.sh etc/qubes-rpc/qubes.UpdatesProxy -etc/qubes/ip6tables.rules -etc/qubes/ip6tables-enabled.rules -etc/qubes/iptables.rules +etc/qubes/qubes-ipv6-disabled.nft +etc/qubes/qubes-ipv6.nft +etc/qubes/qubes-ipv4.nft +etc/qubes/qubes-antispoof.nft etc/sysctl.d/81-qubes.conf.optional etc/tinyproxy/tinyproxy-updates.conf etc/tinyproxy/updates-blacklist @@ -11,6 +12,7 @@ etc/xen/scripts/vif-qubes-nat.sh etc/xen/scripts/vif-route-qubes lib/systemd/system/qubes-firewall.service lib/systemd/system/qubes-iptables.service +lib/systemd/system/qubes-antispoof.service lib/systemd/system/qubes-network.service lib/systemd/system/qubes-network-uplink.service lib/systemd/system/qubes-network-uplink@.service @@ -20,7 +22,6 @@ usr/lib/qubes/init/network-proxy-setup.sh usr/lib/qubes/init/network-proxy-stop.sh usr/lib/qubes/init/network-uplink-wait.sh usr/lib/qubes/init/qubes-iptables -usr/lib/qubes/get-dns-from-resolved usr/lib/qubes/qubes-setup-dnat-to-ns usr/lib/qubes/setup-ip usr/lib/tmpfiles.d/qubes-core-agent-linux.conf diff --git a/network/Makefile b/network/Makefile index 0047c1e32..30d9eb2a2 100644 --- a/network/Makefile +++ b/network/Makefile @@ -12,7 +12,6 @@ install: install -t $(DESTDIR)$(QUBESLIBDIR) \ setup-ip \ tinyproxy-wrapper \ - get-dns-from-resolved \ update-proxy-configs install -d $(DESTDIR)$(BINDIR) install -t $(DESTDIR)$(BINDIR) \ diff --git a/network/get-dns-from-resolved b/network/get-dns-from-resolved deleted file mode 100755 index 118a8cf5d..000000000 --- a/network/get-dns-from-resolved +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/python3 -# -# The Qubes OS Project, http://www.qubes-os.org -# -# Copyright (C) 2022 Marek Marczykowski-Górecki -# -# -# 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 2 -# 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 . -# - -import dbus - - -def get_dns(): - bus = dbus.SystemBus() - resolve1 = bus.get_object('org.freedesktop.resolve1', - '/org/freedesktop/resolve1') - resolve1_proxy = dbus.Interface(resolve1, - dbus_interface='org.freedesktop.resolve1') - dns = resolve1.Get('org.freedesktop.resolve1.Manager', - 'DNS', - dbus_interface='org.freedesktop.DBus.Properties') - return dns - - -def print_ipv4_dns(dns): - # filter on IPv4 (family 2) and then sort global DNS first - dns_filtered_sorted = sorted((d for d in dns if d[1] == 2), - key=lambda x: x[0] != 0) - - for dns in dns_filtered_sorted: - # take only address array - address = dns[2] - print('nameserver {:d}.{:d}.{:d}.{:d}'.format(*address)) - - -if __name__ == '__main__': - print_ipv4_dns(get_dns()) diff --git a/network/qubes-antispoof.nft b/network/qubes-antispoof.nft new file mode 100644 index 000000000..cfa45f0cb --- /dev/null +++ b/network/qubes-antispoof.nft @@ -0,0 +1,42 @@ +#!/usr/sbin/nft -f +table ip qubes { + set downstream { + type ipv4_addr + } + + set allowed { + type ifname . ipv4_addr + } + + chain prerouting { + type filter hook prerouting priority raw; policy accept; + iifgroup 2 goto antispoof + ip saddr @downstream counter drop + } + + chain antispoof { + iifname . ip saddr @allowed accept + counter drop + } +} + +table ip6 qubes { + set downstream { + type ipv6_addr + } + + set allowed { + type ifname . ipv6_addr + } + + chain antispoof { + iifname . ip6 saddr @allowed accept + counter drop + } + + chain prerouting { + type filter hook prerouting priority raw; policy accept; + iifgroup 2 goto antispoof + ip6 saddr @downstream counter drop + } +} diff --git a/network/qubes-iptables b/network/qubes-iptables index 6daa9ac9c..2def65e4a 100755 --- a/network/qubes-iptables +++ b/network/qubes-iptables @@ -21,36 +21,29 @@ IPTABLES=iptables IPTABLES_DATA_DIR=/etc/qubes -if [ ! -x /sbin/$IPTABLES ]; then +if [ ! -x "/sbin/$IPTABLES" ]; then echo $"${IPTABLES}: /sbin/$IPTABLES does not exist." exit 5 fi -start() { +start () { ipt=$1 - IPTABLES_DATA=$IPTABLES_DATA_DIR/${ipt}.rules + IPTABLES_DATA=$IPTABLES_DATA_DIR/qubes-${ipt}.nft ipv6_enabled= if qubesdb-read /qubes-ip6 >/dev/null 2>&1 || \ qubesdb-read /qubes-netvm-gateway6 >/dev/null 2>&1; then ipv6_enabled=true fi - # if IPv6 is enabled, load alternative rules file - if [ "$ipt" = "ip6tables" ] && [ -n "$ipv6_enabled" ]; then - IPTABLES_DATA=$IPTABLES_DATA_DIR/${ipt}-enabled.rules + # if IPv6 is disabled, load alternative rules file + if [ "$ipt" = "ip6tables" ] && [ -z "$ipv6_enabled" ]; then + IPTABLES_DATA=$IPTABLES_DATA_DIR/qubes-${ipt}-disabled.nft fi - CMD=$ipt # Do not start if there is no config file. [ ! -f "$IPTABLES_DATA" ] && return 6 - CMD_ARGS= - if "$CMD-restore" --help 2>&1 | grep -q wait=; then - # 'wait' must be last on command line if secs not specified - CMD_ARGS=--wait - fi - - echo -n $"${CMD}: Applying firewall rules: " + echo -n "nft: Applying firewall rules: " - "$CMD-restore" "$IPTABLES_DATA" $CMD_ARGS + nft -f "$IPTABLES_DATA" ret="$?" if [ "$ret" -eq 0 ]; then echo OK @@ -63,7 +56,7 @@ start() { case "$1" in start) - start iptables && start ip6tables + start ipv4 && start ipv6 RETVAL=$? ;; *) diff --git a/network/qubes-ipv4.nft b/network/qubes-ipv4.nft new file mode 100644 index 000000000..d40926933 --- /dev/null +++ b/network/qubes-ipv4.nft @@ -0,0 +1,35 @@ +#!/usr/sbin/nft -f +table ip qubes { + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + oifgroup 2 accept + oif lo accept + masquerade + } + + chain input { + type filter hook input priority filter; policy drop; + jump custom-input + ct state invalid counter drop + iifgroup 2 meta l4proto udp udp dport 68 counter drop + ct state related,established accept + iifgroup 2 meta l4proto icmp accept + iif lo accept + iifgroup 2 counter reject with icmp type host-prohibited + counter + } + + chain forward { + type filter hook forward priority filter; policy accept; + jump custom-forward + ct state invalid counter drop + ct state related,established accept + iifgroup != 2 counter drop + oifgroup 2 counter drop + } + + # These chains are reserved for end-users. Qubes OS itself will + # never modify them. + chain custom-input {} + chain custom-forward {} +} diff --git a/network/qubes-ipv6-disabled.nft b/network/qubes-ipv6-disabled.nft new file mode 100644 index 000000000..83d5d3797 --- /dev/null +++ b/network/qubes-ipv6-disabled.nft @@ -0,0 +1,13 @@ +#!/usr/sbin/nft -f +table ip6 qbs-filter { + chain input { + type filter hook input priority filter; policy drop; + iif lo accept + counter + } + + chain forward { + type filter hook forward priority filter; policy drop; + counter + } +} diff --git a/network/qubes-ipv6.nft b/network/qubes-ipv6.nft new file mode 100644 index 000000000..e341c30de --- /dev/null +++ b/network/qubes-ipv6.nft @@ -0,0 +1,40 @@ +#!/usr/sbin/nft -f +table ip6 qubes { + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + oifgroup 2 accept + oif lo accept + masquerade + } + + chain _icmpv6 { + meta l4proto != ipv6-icmp counter reject with icmpv6 type admin-prohibited + icmpv6 type { nd-router-advert, nd-redirect } counter drop + accept + } + + chain input { + type filter hook input priority filter; policy drop; + jump custom-input + ct state invalid counter drop + ct state related,established accept + iifgroup 2 goto _icmpv6 + iif lo accept + meta l4proto udp ip6 saddr fe80::/64 ip6 daddr fe80::/64 udp dport 546 accept + meta l4proto ipv6-icmp accept + counter + } + + chain forward { + type filter hook forward priority filter; policy accept; + ct state invalid counter drop + ct state related,established accept + iifgroup != 2 counter drop + oifgroup 2 counter drop + } + + # These chains are reserved for end-users. Qubes OS itself will + # never modify them. + chain custom-input {} + chain custom-forward {} +} diff --git a/network/qubes-setup-dnat-to-ns b/network/qubes-setup-dnat-to-ns index 3b868b7a0..7a760a91b 100755 --- a/network/qubes-setup-dnat-to-ns +++ b/network/qubes-setup-dnat-to-ns @@ -1,34 +1,111 @@ -#!/bin/sh -addrule() -{ - if [ "$FIRSTONE" = yes ] ; then - FIRSTONE=no - RULE1="-A PR-QBS -d $NS1 -p udp --dport 53 -j DNAT --to $1 --A PR-QBS -d $NS1 -p tcp --dport 53 -j DNAT --to $1" - RULE2="-A PR-QBS -d $NS2 -p udp --dport 53 -j DNAT --to $1 --A PR-QBS -d $NS2 -p tcp --dport 53 -j DNAT --to $1" - else - RULE2="-A PR-QBS -d $NS2 -p udp --dport 53 -j DNAT --to $1 --A PR-QBS -d $NS2 -p tcp --dport 53 -j DNAT --to $1" - fi -} -export PATH="$PATH":/sbin:/bin -# shellcheck disable=SC1091 -. /var/run/qubes/qubes-ns -if [ "X$NS1" = "X" ] ; then exit ; fi -iptables -t nat -F PR-QBS -FIRSTONE=yes -if systemctl -q is-active systemd-resolved && \ - grep -q '^nameserver.*127\.0\.0\.53' /etc/resolv.conf; then - /usr/lib/qubes/get-dns-from-resolved -else - grep ^nameserver /etc/resolv.conf -fi | grep -v ":.*:" | head -2 | - ( - # shellcheck disable=SC2034 - while read -r x y z ; do - addrule "$y" - done - (echo "*nat"; echo "$RULE1"; echo "$RULE2"; echo COMMIT) | iptables-restore -n - ) +#!/usr/bin/python3 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2022 Marek Marczykowski-Górecki +# +# +# 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 2 +# 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 . +# +from __future__ import annotations +import dbus +import qubesdb +from typing import List +from ipaddress import IPv4Address +import os + +def get_dns_resolv_conf(): + nameservers = [] + try: + resolv = open("/etc/resolv.conf", "r", encoding="UTF-8") + except IOError: + return nameservers + with resolv: + for line in resolv: + tokens = line.split(None, 2) + if len(tokens) < 2 or tokens[0] != "nameserver": + continue + try: + nameservers.append(IPv4Address(tokens[1])) + except ValueError: + pass + return nameservers + +def get_dns_resolved(): + try: + bus = dbus.SystemBus() + except dbus.exceptions.DBusException as s: + if s.get_dbus_name() == 'org.freedesktop.DBus.Error.NoReply': + return get_dns_resolv_conf() + raise + try: + resolve1 = bus.get_object('org.freedesktop.resolve1', + '/org/freedesktop/resolve1') + resolve1_proxy = dbus.Interface(resolve1, + dbus_interface='org.freedesktop.resolve1') + dns = resolve1.Get('org.freedesktop.resolve1.Manager', + 'DNS', + dbus_interface='org.freedesktop.DBus.Properties') + except dbus.exceptions.DBusException as s: + error = s.get_dbus_name() + if error in ( + 'org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.NameHasNoOwner', + 'org.freedesktop.DBus.Error.NoSuchUnit', + ) or error.startswith('org.freedesktop.systemd1.'): + return get_dns_resolv_conf() + raise + # Use global entries first + dns.sort(key=lambda x: x[0] != 0) + # Only keep IPv4 entries. systemd-resolved is trusted to return valid + # addresses. + return [IPv4Address(bytes(addr)) for _g, family, addr in dns if family == 2] + +def install_firewall_rules(dns): + qdb = qubesdb.QubesDB() + qubesdb_dns = [] + for i in ('/qubes-netvm-primary-dns', '/qubes-netvm-secondary-dns'): + ns_maybe = qdb.read(i) + if ns_maybe is None: + continue + try: + qubesdb_dns.append(IPv4Address(ns_maybe.decode("ascii", "strict"))) + except (UnicodeDecodeError, ValueError): + pass + res = [ + 'add table ip qubes', + # Add the chain so that the subsequent delete will work. If the chain already + # exists this is a harmless no-op. + 'add chain ip qubes dnat-dns', + # Delete the chain so that if the chain already exists, it will be removed. + # The removal of the old chain and addition of the new one happen as a single + # atomic operation, so there is no period where neither chain is present or + # where both are present. + 'delete chain ip qubes dnat-dns', + 'table ip qubes {', + 'chain dnat-dns {', + 'type nat hook prerouting priority dstnat; policy accept;', + ] + for vm_nameserver, dest in zip(qubesdb_dns, get_dns_resolved()): + dns_ = str(dest) + res += [ + f"ip daddr {vm_nameserver} udp dport 53 dnat to {dns_}", + f"ip daddr {vm_nameserver} tcp dport 53 dnat to {dns_}", + ] + res += ["}\n}\n"] + os.execvp("nft", ("nft", "--", "\n".join(res))) + +if __name__ == '__main__': + install_firewall_rules(get_dns_resolved()) diff --git a/network/setup-ip b/network/setup-ip index 01801b0c6..ad1349019 100755 --- a/network/setup-ip +++ b/network/setup-ip @@ -29,7 +29,7 @@ configure_network() { if [ -n "$ip6" ]; then /sbin/ip address add "$ip6/$netmask6" dev "$INTERFACE" fi - /sbin/ip link set dev "$INTERFACE" up + /sbin/ip link set dev "$INTERFACE" group 1 up if [ -n "$gateway" ]; then add_link_route "$gateway" "$INTERFACE" @@ -85,6 +85,7 @@ configure_network_nm() { local ip4_nm_config local ip6_nm_config local uuid + /sbin/ip link set dev "$INTERFACE" group 1 prefix="$(get_prefix_from_subnet "$netmask")" prefix6="$netmask6" diff --git a/network/vif-qubes-nat.sh b/network/vif-qubes-nat.sh index 2c85f1cc7..7323036c6 100755 --- a/network/vif-qubes-nat.sh +++ b/network/vif-qubes-nat.sh @@ -47,7 +47,8 @@ function netns run ip addr flush dev "$netns_appvm_if" run ip netns delete "$netns" || : -if test "$command" == online; then +if test "$command" = online; then + echo 1 > "/proc/sys/net/ipv6/conf/$netns_appvm_if/disable_ipv6" run ip netns add "$netns" run ip link set "$netns_appvm_if" netns "$netns" @@ -57,50 +58,37 @@ if test "$command" == online; then # as the actual VM, so that our neighbor entry works. run ip link add name "$netns_netvm_if" address "$mac" type veth \ peer name "$netvm_if" address "$netvm_mac" + echo 1 > "/proc/sys/net/ipv6/conf/$netns_netvm_if/disable_ipv6" run ip link set dev "$netns_netvm_if" netns "$netns" - netns ip6tables -t raw -I PREROUTING -j DROP - netns ip6tables -P INPUT DROP - netns ip6tables -P FORWARD DROP - netns ip6tables -P OUTPUT DROP - netns sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward' - netns iptables -t raw -I PREROUTING -i "$netns_appvm_if" ! -s "$appvm_ip" -j DROP - if test -n "$undetectable_netvm_ips"; then # prevent an AppVM connecting to its own ProxyVM IP because that makes the internal IPs detectable even with no firewall rules - netns iptables -t raw -I PREROUTING -i "$netns_appvm_if" -d "$netvm_ip" -j DROP - - # same for the gateway/DNS IPs - netns iptables -t raw -I PREROUTING -i "$netns_appvm_if" -d "$netvm_gw_ip" -j DROP - netns iptables -t raw -I PREROUTING -i "$netns_appvm_if" -d "$netvm_dns1_ip" -j DROP - netns iptables -t raw -I PREROUTING -i "$netns_appvm_if" -d "$netvm_dns2_ip" -j DROP - fi - - netns iptables -t nat -I PREROUTING -i "$netns_netvm_if" -j DNAT --to-destination "$appvm_ip" - netns iptables -t nat -I POSTROUTING -o "$netns_netvm_if" -j SNAT --to-source "$netvm_ip" - - netns iptables -t nat -I PREROUTING -i "$netns_appvm_if" -d "$appvm_gw_ip" -j DNAT --to-destination "$netvm_gw_ip" - netns iptables -t nat -I POSTROUTING -o "$netns_appvm_if" -s "$netvm_gw_ip" -j SNAT --to-source "$appvm_gw_ip" - - if test -n "$appvm_dns1_ip"; then - netns iptables -t nat -I PREROUTING -i "$netns_appvm_if" -d "$appvm_dns1_ip" -j DNAT --to-destination "$netvm_dns1_ip" - netns iptables -t nat -I POSTROUTING -o "$netns_appvm_if" -s "$netvm_dns1_ip" -j SNAT --to-source "$appvm_dns1_ip" + more_antispoof=" ip daddr != { $netvm_ip, $netvm_gw_ip, $netvm_dns1_ip, $netvm_dns2_ip }" + else + more_antispoof= fi - if test -n "$appvm_dns2_ip"; then - netns iptables -t nat -I PREROUTING -i "$netns_appvm_if" -d "$appvm_dns2_ip" -j DNAT --to-destination "$netvm_dns2_ip" - netns iptables -t nat -I POSTROUTING -o "$netns_appvm_if" -s "$netvm_dns2_ip" -j SNAT --to-source "$appvm_dns2_ip" - fi + netns nft " +table netdev antispoof { + chain antispoof { + type filter hook ingress device $netns_appvm_if priority filter; policy drop; + ip saddr $appvm_ip$more_antispoof ip saddr set $netvm_ip fwd to $netns_netvm_if + arp htype 1 arp ptype ip arp hlen 6 arp plen 4 arp saddr ether $mac arp saddr ip $appvm_ip accept + counter + } + chain reverse { + type filter hook ingress device $netns_netvm_if priority filter; policy drop; + ip daddr $netvm_ip ip daddr set $appvm_ip fwd to $netns_appvm_if + ether type arp accept + counter + } +}" netns ip addr add "$netvm_ip" dev "$netns_netvm_if" netns ip addr add "$appvm_gw_ip" dev "$netns_appvm_if" netns ip link set "$netns_netvm_if" up netns ip link set "$netns_appvm_if" up - - netns ip route add "$appvm_ip" dev "$netns_appvm_if" src "$appvm_gw_ip" - netns ip route add "$netvm_gw_ip" dev "$netns_netvm_if" src "$netvm_ip" - netns ip route add default via "$netvm_gw_ip" dev "$netns_netvm_if" src "$netvm_ip" fi diff --git a/network/vif-route-qubes b/network/vif-route-qubes index fbd4a679e..7c4585879 100755 --- a/network/vif-route-qubes +++ b/network/vif-route-qubes @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -- #============================================================================ # /etc/xen/vif-route-qubes # @@ -20,6 +20,7 @@ # this script). #============================================================================ +set -e dir=$(dirname "$0") || exit # shellcheck disable=SC1091,SC1090 . "$dir/vif-common.sh" @@ -55,12 +56,6 @@ conntrack_purge () { [[ "$output" =~ $deleted ]] } -ipt_arg= -if "iptables-restore" --help 2>&1 | grep -q wait=; then - # 'wait' must be last on command line if secs not specified - ipt_arg=--wait -fi - # shellcheck disable=SC2154 if [ "${ip}" ]; then # get first IPv4 and first IPv6 @@ -118,14 +113,40 @@ case "$command" in online) echo 1 >"/proc/sys/net/ipv4/conf/${vif}/proxy_arp" ipcmd='add' - iptables_cmd='-I PREROUTING 1' + nftables_cmd=add cmdprefix='' ipv6_disabled=$(cat "/proc/sys/net/ipv6/conf/$vif/disable_ipv6" || echo 1) + interfaces='' + separator='' + + for i in /proc/sys/net/ipv4/conf/*; do + i=${i:24} + case $i in (all|default|*[!A-Za-z0-9._]*) continue;; esac + interfaces+="$separator$i" + separator=', ' + done + if [[ -n "$separator" ]]; then + nft " +add table inet qubes-nat-accel +delete table inet qubes-nat-accel +table inet qubes-nat-accel { + flowtable qubes-accel { + hook ingress priority filter + devices = { $interfaces } + } + chain qubes-accel { + # run after other filter hooks + type filter hook forward priority filter + 5; policy accept; + meta l4proto { tcp, udp } iifgroup 2 oifgroup 1 flow add @qubes-accel + counter + } +}" + fi ;; offline) do_without_error ifdown "${vif}" ipcmd='del' - iptables_cmd='-D PREROUTING' + nftables_cmd=delete cmdprefix='do_without_error' # cleanup IPv6 config even if _now_ it is disabled ipv6_disabled=0 @@ -152,12 +173,13 @@ qubesdb_read_bool () { case $status in (0) if test -n "$value"; then return 0; else return 1; fi;; (2) return "$2";; - (*) echo "unexpected return value $status from qubesdb-read" >&2; exit 1;; + (*) fatal "unexpected return value $status from qubesdb-read";; esac } # Harden against various attacks if requested -if qubesdb_read_bool /qubes-service/harden-network-interfaces 1; then +if [[ "$command" = 'online' ]] && + qubesdb_read_bool /qubes-service/harden-network-interfaces 1; then /usr/lib/systemd/systemd-sysctl \ "--prefix=/net/ipv4/conf/all" \ "--prefix=/net/ipv4/neigh/all" \ @@ -173,34 +195,35 @@ fi # add anti-spoofing rules before enabling the interface if [ "${ip}" ]; then + peer_mac=$(xenstore-read -- "$XENBUS_PATH/mac") || + fatal 'cannot obtain peer MAC from Xenstore' + readonly peer_mac + [[ "$peer_mac" =~ ^[0-9a-f]{2}(:[0-9a-f]{2}){5}$ ]] || + fatal "bad mac address $peer_mac" + # If we’ve been given a list of IP addresses, then add routes from us to # the VMs we serve using those addresses. for addr in ${ip}; do if [[ "$addr" = *:* ]]; then - ipt=ip6tables-restore + ipt=ip6 else - ipt=iptables-restore + ipt=ip fi - printf '%s\n' "*raw" \ - "$iptables_cmd -i ${vif} ! -s ${addr} -j DROP" \ - "$iptables_cmd ! -i vif+ -s ${addr} -j DROP" \ - "COMMIT" | - ${cmdprefix} "$ipt" --noflush $ipt_arg if ! conntrack_purge -s "$addr" || ! conntrack_purge -d "$addr"; then printf 'Cannot purge stale conntrack entries for %q\n' "$addr">&2 exit 1 fi + nft "$nftables_cmd element $ipt qubes allowed { \"$vif\" . $addr } +$nftables_cmd element $ipt qubes downstream { $addr }" done - # if no IPv6 is assigned, block all IPv6 traffic on that interface - if ! [[ "$ip" = *:* ]]; then - echo -e "*raw\\n$iptables_cmd -i ${vif} -j DROP\\nCOMMIT" | - ${cmdprefix} ip6tables-restore --noflush $ipt_arg - fi + # if no IPv6 is assigned, IPv6 traffic on the interface is + # implicitly considered spoofed as there is no entry in the + # "allowed" map fi if [ "$command" = "online" ]; then - ip link set dev "${vif}" up + ip link set dev "${vif}" group 2 up fi if [ "${ip}" ]; then diff --git a/qubesagent/firewall.py b/qubesagent/firewall.py index 97bedaa5c..8264f453d 100755 --- a/qubesagent/firewall.py +++ b/qubesagent/firewall.py @@ -362,272 +362,6 @@ def main(self): def terminate(self): self.terminate_requested = True - -class IptablesWorker(FirewallWorker): - supported_rule_opts = ['action', 'proto', 'dst4', 'dst6', 'dsthost', - 'dstports', 'specialtarget', 'icmptype'] - - def __init__(self): - super(IptablesWorker, self).__init__() - self.chains = { - 4: set(), - 6: set(), - } - - @staticmethod - def chain_for_addr(addr): - """Generate iptables chain name for given source address address""" - return 'qbs-' + addr.replace('.', '-').replace(':', '-')[-20:] - - def run_ipt(self, family, args, **kwargs): - # pylint: disable=no-self-use - if family == 6: - subprocess.check_call(['ip6tables'] + args, **kwargs) - else: - subprocess.check_call(['iptables'] + args, **kwargs) - - def run_ipt_restore(self, family, args): - # pylint: disable=no-self-use - if family == 6: - return subprocess.Popen(['ip6tables-restore'] + args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - else: - return subprocess.Popen(['iptables-restore'] + args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - - def create_chain(self, addr, chain, family): - """ - Create iptables chain and hook traffic coming from `addr` to it. - - :param addr: source IP from which traffic should be handled by the - chain - :param chain: name of the chain to create - :param family: address family (4 or 6) - :return: None - """ - - self.run_ipt(family, ['-N', chain]) - self.run_ipt(family, - ['-I', 'QBS-FORWARD', '-s', addr, '-j', chain]) - self.chains[family].add(chain) - - def prepare_rules(self, chain, rules, family): - """ - Helper function to translate rules list into input for iptables-restore - - :param chain: name of the chain to put rules into - :param rules: list of rules - :param family: address family (4 or 6) - :return: tuple: (input for iptables-restore, dict of DNS records resolved - during execution) - :rtype: (str, dict) - """ - - iptables = "*filter\n" - - fullmask = '/128' if family == 6 else '/32' - - dns = list(addr + fullmask for addr in self.dns_addresses(family)) - - ret_dns = {} - - for rule in rules: - unsupported_opts = set(rule.keys()).difference( - set(self.supported_rule_opts)) - if unsupported_opts: - raise RuleParseError( - 'Unsupported rule option(s): {!s}'.format(unsupported_opts)) - if 'dst4' in rule and family == 6: - raise RuleParseError('IPv4 rule found for IPv6 address') - if 'dst6' in rule and family == 4: - raise RuleParseError('dst6 rule found for IPv4 address') - - if 'proto' in rule: - if rule['proto'] == 'icmp' and family == 6: - protos = ['icmpv6'] - else: - protos = [rule['proto']] - else: - protos = None - - if 'dst4' in rule: - dsthosts = [rule['dst4']] - elif 'dst6' in rule: - dsthosts = [rule['dst6']] - elif 'dsthost' in rule: - addrinfo = self.resolve_dns(rule['dsthost'], family) - dsthosts = set(item[4][0] + fullmask for item in addrinfo) - ret_dns[rule['dsthost']] = dsthosts - else: - dsthosts = None - - if 'dstports' in rule: - dstports = rule['dstports'].replace('-', ':') - else: - dstports = None - - if rule.get('specialtarget', None) == 'dns': - if dstports not in ('53:53', None): - continue - else: - dstports = '53:53' - if not dns: - continue - if protos is not None: - protos = {'tcp', 'udp'}.intersection(protos) - else: - protos = {'tcp', 'udp'} - - if dsthosts is not None: - dsthosts = set(dns).intersection(dsthosts) - else: - dsthosts = dns - - if 'icmptype' in rule: - icmptype = rule['icmptype'] - else: - icmptype = None - - # make them iterable - if protos is None: - protos = [None] - if dsthosts is None: - dsthosts = [None] - - if rule['action'] == 'accept': - action = 'ACCEPT' - elif rule['action'] == 'drop': - action = 'REJECT --reject-with {}'.format( - 'icmp6-adm-prohibited' if family == 6 else - 'icmp-admin-prohibited') - else: - raise RuleParseError( - 'Invalid rule action {}'.format(rule['action'])) - - # sorting here is only to ease writing tests - for proto in sorted(protos): - for dsthost in sorted(dsthosts): - ipt_rule = '-A {}'.format(chain) - if dsthost is not None: - ipt_rule += ' -d {}'.format(dsthost) - if proto is not None: - ipt_rule += ' -p {}'.format(proto) - if dstports is not None: - ipt_rule += ' --dport {}'.format(dstports) - if icmptype is not None: - ipt_rule += ' --icmp-type {}'.format(icmptype) - ipt_rule += ' -j {}\n'.format(action) - iptables += ipt_rule - - iptables += 'COMMIT\n' - return (iptables, ret_dns) - - def apply_rules_family(self, source, rules, family): - """ - Apply rules for given source address. - Handle only rules for given address family (IPv4 or IPv6). - - :param source: source address - :param rules: rules list - :param family: address family, either 4 or 6 - :return: None - """ - - chain = self.chain_for_addr(source) - if chain not in self.chains[family]: - self.create_chain(source, chain, family) - - (iptables, dns) = self.prepare_rules(chain, rules, family) - try: - self.run_ipt(family, ['-F', chain]) - p = self.run_ipt_restore(family, ['-n']) - (output, _) = p.communicate(iptables.encode()) - if p.returncode != 0: - raise RuleApplyError( - 'iptables-restore failed: {}'.format(output)) - self.update_dns_info(source, dns) - except subprocess.CalledProcessError as e: - raise RuleApplyError('\'iptables -F {}\' failed: {}'.format( - chain, e.output)) - - connections = self.conntrack_get_connections(family, source) - for con in connections: - is_blocked = self.is_blocked(rules, con, dns) - if is_blocked: - self.conntrack_drop(source, con) - - def apply_rules(self, source, rules): - if self.is_ip6(source): - self.apply_rules_family(source, rules, 6) - else: - self.apply_rules_family(source, rules, 4) - - def update_connected_ips(self, family): - ips = self.get_connected_ips(family) - - if not ips: - # Just flush. - self.run_ipt(family, ['-t', 'raw', '-F', 'QBS-PREROUTING']) - self.run_ipt(family, ['-t', 'mangle', '-F', 'QBS-POSTROUTING']) - return - - # Temporarily set policy to DROP while updating the rules. - self.run_ipt(family, ['-t', 'raw', '-P', 'PREROUTING', 'DROP']) - self.run_ipt(family, ['-t', 'mangle', '-P', 'POSTROUTING', 'DROP']) - - self.run_ipt(family, ['-t', 'raw', '-F', 'QBS-PREROUTING']) - self.run_ipt(family, ['-t', 'mangle', '-F', 'QBS-POSTROUTING']) - - for ip in ips: - self.run_ipt(family, [ - '-t', 'raw', '-A', 'QBS-PREROUTING', - '!', '-i', 'vif+', '-s', ip, '-j', 'DROP']) - self.run_ipt(family, [ - '-t', 'mangle', '-A', 'QBS-POSTROUTING', - '!', '-o', 'vif+', '-d', ip, '-j', 'DROP']) - - self.run_ipt(family, ['-t', 'raw', '-P', 'PREROUTING', 'ACCEPT']) - self.run_ipt(family, ['-t', 'mangle', '-P', 'POSTROUTING', 'ACCEPT']) - - def init(self): - # Chains QBS-FORWARD, QBS-PREROUTING, QBS-POSTROUTING - # need to be created before running this. - try: - self.run_ipt(4, ['-F', 'QBS-FORWARD']) - self.run_ipt(4, - ['-A', 'QBS-FORWARD', '!', '-i', 'vif+', '-j', 'RETURN']) - self.run_ipt(4, ['-A', 'QBS-FORWARD', '-j', 'DROP']) - self.run_ipt(4, ['-t', 'raw', '-F', 'QBS-PREROUTING']) - self.run_ipt(4, ['-t', 'mangle', '-F', 'QBS-POSTROUTING']) - - self.run_ipt(6, ['-F', 'QBS-FORWARD']) - self.run_ipt(6, - ['-A', 'QBS-FORWARD', '!', '-i', 'vif+', '-j', 'RETURN']) - self.run_ipt(6, ['-A', 'QBS-FORWARD', '-j', 'DROP']) - self.run_ipt(6, ['-t', 'raw', '-F', 'QBS-PREROUTING']) - self.run_ipt(6, ['-t', 'mangle', '-F', 'QBS-POSTROUTING']) - except subprocess.CalledProcessError: - self.log_error( - 'Error initializing iptables. ' - 'You probably need to create QBS-FORWARD, QBS-PREROUTING and ' - 'QBS-POSTROUTING chains first.' - ) - sys.exit(1) - - def cleanup(self): - for family in (4, 6): - self.run_ipt(family, ['-F', 'QBS-FORWARD']) - self.run_ipt(family, ['-t', 'raw', '-F', 'QBS-PREROUTING']) - self.run_ipt(family, ['-t', 'mangle', '-F', 'QBS-POSTROUTING']) - for chain in self.chains[family]: - self.run_ipt(family, ['-F', chain]) - self.run_ipt(family, ['-X', chain]) - - class NftablesWorker(FirewallWorker): supported_rule_opts = ['action', 'proto', 'dst4', 'dst6', 'dsthost', 'dstports', 'specialtarget', 'icmptype'] @@ -899,7 +633,8 @@ def main(): if shutil.which('nft'): worker = NftablesWorker() else: - worker = IptablesWorker() + print('Sorry, iptables no longer supported', file=sys.stderr) + sys.exit(1) context = daemon.DaemonContext() context.stderr = sys.stderr context.detach_process = False diff --git a/qubesagent/test_firewall.py b/qubesagent/test_firewall.py index 3e68a07b1..17c336127 100644 --- a/qubesagent/test_firewall.py +++ b/qubesagent/test_firewall.py @@ -103,46 +103,6 @@ def dns_addresses(family=None): return ['2001::1', '2001::2'] -class IptablesWorker(qubesagent.firewall.IptablesWorker): - '''Override methods actually modifying system state to only log what - would be done''' - - def __init__(self): - # pylint: disable=super-init-not-called - # don't call super on purpose - avoid connecting to QubesDB - # super(IptablesWorker, self).__init__() - # copied __init__: - self.qdb = DummyQubesDB(self) - self.log = logging.getLogger('qubes.tests') - self.chains = { - 4: set(), - 6: set(), - } - - #: instead of really running `iptables`, log what would be called - self.called_commands = { - 4: [], - 6: [], - } - #: rules that would be loaded with `iptables-restore` - self.loaded_iptables = { - 4: None, - 6: None, - } - - def run_ipt(self, family, args, **kwargs): - self.called_commands[family].append(args) - - def run_ipt_restore(self, family, args): - return DummyIptablesRestore(self, family) - - @staticmethod - def dns_addresses(family=None): - if family == 4: - return ['1.1.1.1', '2.2.2.2'] - else: - return ['2001::1', '2001::2'] - class NftablesWorker(qubesagent.firewall.NftablesWorker): '''Override methods actually modifying system state to only log what @@ -151,7 +111,7 @@ class NftablesWorker(qubesagent.firewall.NftablesWorker): def __init__(self): # pylint: disable=super-init-not-called # don't call super on purpose - avoid connecting to QubesDB - # super(IptablesWorker, self).__init__() + # super(NftablesWorker, self).__init__() # copied __init__: self.qdb = DummyQubesDB(self) self.log = logging.getLogger('qubes.tests') @@ -199,227 +159,6 @@ def test_701_dns_info(self): self.obj.apply_rules('10.137.0.1', [{'action': 'drop'}]) self.assertIsNone(self.obj.qdb.read('/dns/10.137.0.1/ripe.net')) -class TestIptablesWorker(TestCase, WorkerCommon): - def setUp(self): - super(TestIptablesWorker, self).setUp() - self.obj = IptablesWorker() - self.subprocess_patch = patch('subprocess.call') - self.subprocess_mock = self.subprocess_patch.start() - - def tearDown(self): - self.subprocess_patch.stop() - - def test_000_chain_for_addr(self): - self.assertEqual( - self.obj.chain_for_addr('10.137.0.1'), 'qbs-10-137-0-1') - self.assertEqual( - self.obj.chain_for_addr('fd09:24ef:4179:0000::3'), - 'qbs-09-24ef-4179-0000--3') - - def test_001_create_chain(self): - testdata = [ - (4, '10.137.0.1', 'qbs-10-137-0-1'), - (6, 'fd09:24ef:4179:0000::3', 'qbs-fd09-24ef-4179-0000--3') - ] - for family, addr, chain in testdata: - self.obj.create_chain(addr, chain, family) - self.assertEqual(self.obj.called_commands[family], - [['-N', chain], - ['-I', 'QBS-FORWARD', '-s', addr, '-j', chain]]) - - def test_002_prepare_rules4(self): - rules = [ - {'action': 'accept', 'proto': 'tcp', - 'dstports': '80-80', 'dst4': '1.2.3.0/24'}, - {'action': 'accept', 'proto': 'udp', - 'dstports': '443-1024', 'dsthost': 'yum.qubes-os.org'}, - {'action': 'accept', 'specialtarget': 'dns'}, - {'action': 'drop', 'proto': 'udp', 'specialtarget': 'dns'}, - {'action': 'drop', 'proto': 'icmp'}, - {'action': 'drop'}, - ] - expected_iptables = ( - "*filter\n" - "-A chain -d 1.2.3.0/24 -p tcp --dport 80:80 -j ACCEPT\n" - "-A chain -d 147.75.102.29/32 -p udp --dport 443:1024 -j ACCEPT\n" - "-A chain -d 1.1.1.1/32 -p tcp --dport 53:53 -j ACCEPT\n" - "-A chain -d 2.2.2.2/32 -p tcp --dport 53:53 -j ACCEPT\n" - "-A chain -d 1.1.1.1/32 -p udp --dport 53:53 -j ACCEPT\n" - "-A chain -d 2.2.2.2/32 -p udp --dport 53:53 -j ACCEPT\n" - "-A chain -d 1.1.1.1/32 -p udp --dport 53:53 -j REJECT " - "--reject-with icmp-admin-prohibited\n" - "-A chain -d 2.2.2.2/32 -p udp --dport 53:53 -j REJECT " - "--reject-with icmp-admin-prohibited\n" - "-A chain -p icmp -j REJECT " - "--reject-with icmp-admin-prohibited\n" - "-A chain -j REJECT " - "--reject-with icmp-admin-prohibited\n" - "COMMIT\n" - ) - ret = self.obj.prepare_rules('chain', rules, 4) - self.assertEqual(ret[0], expected_iptables) - self.assertPrepareRulesDnsRet(ret[1], 'yum.qubes-os.org', 4) - with self.assertRaises(qubesagent.firewall.RuleParseError): - self.obj.prepare_rules('chain', [{'unknown': 'xxx'}], 4) - with self.assertRaises(qubesagent.firewall.RuleParseError): - self.obj.prepare_rules('chain', [{'dst6': 'a::b'}], 4) - with self.assertRaises(qubesagent.firewall.RuleParseError): - self.obj.prepare_rules('chain', [{'dst4': '3.3.3.3'}], 6) - - def test_003_prepare_rules6(self): - rules = [ - {'action': 'accept', 'proto': 'tcp', - 'dstports': '80-80', 'dst6': 'a::b/128'}, - {'action': 'accept', 'proto': 'tcp', - 'dsthost': 'ripe.net'}, - {'action': 'accept', 'specialtarget': 'dns'}, - {'action': 'drop', 'proto': 'udp', 'specialtarget': 'dns'}, - {'action': 'drop', 'proto': 'icmp'}, - {'action': 'drop'}, - ] - expected_iptables = ( - "*filter\n" - "-A chain -d a::b/128 -p tcp --dport 80:80 -j ACCEPT\n" - "-A chain -d 2001:67c:2e8:22::c100:68b/128 -p tcp -j ACCEPT\n" - "-A chain -d 2001::1/128 -p tcp --dport 53:53 -j ACCEPT\n" - "-A chain -d 2001::2/128 -p tcp --dport 53:53 -j ACCEPT\n" - "-A chain -d 2001::1/128 -p udp --dport 53:53 -j ACCEPT\n" - "-A chain -d 2001::2/128 -p udp --dport 53:53 -j ACCEPT\n" - "-A chain -d 2001::1/128 -p udp --dport 53:53 -j REJECT " - "--reject-with icmp6-adm-prohibited\n" - "-A chain -d 2001::2/128 -p udp --dport 53:53 -j REJECT " - "--reject-with icmp6-adm-prohibited\n" - "-A chain -p icmpv6 -j REJECT " - "--reject-with icmp6-adm-prohibited\n" - "-A chain -j REJECT " - "--reject-with icmp6-adm-prohibited\n" - "COMMIT\n" - ) - ret = self.obj.prepare_rules('chain', rules, 6) - self.assertEqual(ret[0], expected_iptables) - self.assertPrepareRulesDnsRet(ret[1], 'ripe.net', 6) - - def test_004_apply_rules4(self): - self.obj.conntrack_get_connections = Mock(return_value=[]) - rules = [{'action': 'accept'}] - chain = 'qbs-10-137-0-1' - self.obj.apply_rules('10.137.0.1', rules) - self.assertEqual(self.obj.called_commands[4], - [ - ['-N', chain], - ['-I', 'QBS-FORWARD', '-s', '10.137.0.1', '-j', chain], - ['-F', chain]]) - self.assertEqual(self.obj.loaded_iptables[4], - self.obj.prepare_rules(chain, rules, 4)[0]) - self.assertEqual(self.obj.called_commands[6], []) - self.assertIsNone(self.obj.loaded_iptables[6]) - - def test_005_apply_rules6(self): - self.obj.conntrack_get_connections = Mock(return_value=[]) - rules = [{'action': 'accept'}] - chain = 'qbs-2000--a' - self.obj.apply_rules('2000::a', rules) - self.assertEqual(self.obj.called_commands[6], - [ - ['-N', chain], - ['-I', 'QBS-FORWARD', '-s', '2000::a', '-j', chain], - ['-F', chain]]) - self.assertEqual(self.obj.loaded_iptables[6], - self.obj.prepare_rules(chain, rules, 6)[0]) - self.assertEqual(self.obj.called_commands[4], []) - self.assertIsNone(self.obj.loaded_iptables[4]) - - def test_006_init(self): - self.obj.init() - self.assertEqual(self.obj.called_commands[4], [ - ['-F', 'QBS-FORWARD'], - ['-A', 'QBS-FORWARD', '!', '-i', 'vif+', '-j', 'RETURN'], - ['-A', 'QBS-FORWARD', '-j', 'DROP'], - ['-t', 'raw', '-F', 'QBS-PREROUTING'], - ['-t', 'mangle', '-F', 'QBS-POSTROUTING'], - ]) - self.assertEqual(self.obj.called_commands[6], [ - ['-F', 'QBS-FORWARD'], - ['-A', 'QBS-FORWARD', '!', '-i', 'vif+', '-j', 'RETURN'], - ['-A', 'QBS-FORWARD', '-j', 'DROP'], - ['-t', 'raw', '-F', 'QBS-PREROUTING'], - ['-t', 'mangle', '-F', 'QBS-POSTROUTING'], - ]) - - def test_007_cleanup(self): - self.obj.init() - self.obj.create_chain('1.2.3.4', 'chain-ip4-1', 4) - self.obj.create_chain('1.2.3.6', 'chain-ip4-2', 4) - self.obj.create_chain('2000::1', 'chain-ip6-1', 6) - self.obj.create_chain('2000::2', 'chain-ip6-2', 6) - # forget about commands called earlier - self.obj.called_commands[4] = [] - self.obj.called_commands[6] = [] - self.obj.cleanup() - self.assertEqual([self.obj.called_commands[4][0]] + - sorted(self.obj.called_commands[4][1:], key=operator.itemgetter(1)), - [ - ['-F', 'QBS-FORWARD'], - ['-F', 'chain-ip4-1'], - ['-X', 'chain-ip4-1'], - ['-F', 'chain-ip4-2'], - ['-X', 'chain-ip4-2'], - ['-t', 'mangle', '-F', 'QBS-POSTROUTING'], - ['-t', 'raw', '-F', 'QBS-PREROUTING'], - ]) - self.assertEqual([self.obj.called_commands[6][0]] + - sorted(self.obj.called_commands[6][1:], key=operator.itemgetter(1)), - [ - ['-F', 'QBS-FORWARD'], - ['-F', 'chain-ip6-1'], - ['-X', 'chain-ip6-1'], - ['-F', 'chain-ip6-2'], - ['-X', 'chain-ip6-2'], - ['-t', 'mangle', '-F', 'QBS-POSTROUTING'], - ['-t', 'raw', '-F', 'QBS-PREROUTING'], - ]) - - def test_008_update_connected_ips(self): - self.obj.qdb.entries['/connected-ips'] = b'10.137.0.1 10.137.0.2' - self.obj.called_commands[4] = [] - self.obj.update_connected_ips(4) - - self.assertEqual(self.obj.called_commands[4], [ - ['-t', 'raw', '-P', 'PREROUTING', 'DROP'], - ['-t', 'mangle', '-P', 'POSTROUTING', 'DROP'], - ['-t', 'raw', '-F', 'QBS-PREROUTING'], - ['-t', 'mangle', '-F', 'QBS-POSTROUTING'], - ['-t', 'raw', '-A', 'QBS-PREROUTING', - '!', '-i', 'vif+', '-s', '10.137.0.1', '-j', 'DROP'], - ['-t', 'mangle', '-A', 'QBS-POSTROUTING', - '!', '-o', 'vif+', '-d', '10.137.0.1', '-j', 'DROP'], - ['-t', 'raw', '-A', 'QBS-PREROUTING', - '!', '-i', 'vif+', '-s', '10.137.0.2', '-j', 'DROP'], - ['-t', 'mangle', '-A', 'QBS-POSTROUTING', - '!', '-o', 'vif+', '-d', '10.137.0.2', '-j', 'DROP'], - ['-t', 'raw', '-P', 'PREROUTING', 'ACCEPT'], - ['-t', 'mangle', '-P', 'POSTROUTING', 'ACCEPT'], - ]) - - def test_009_update_connected_ips_empty(self): - self.obj.qdb.entries['/connected-ips'] = b'' - self.obj.called_commands[4] = [] - self.obj.update_connected_ips(4) - - self.assertEqual(self.obj.called_commands[4], [ - ['-t', 'raw', '-F', 'QBS-PREROUTING'], - ['-t', 'mangle', '-F', 'QBS-POSTROUTING'], - ]) - - def test_010_update_connected_ips_missing(self): - self.obj.called_commands[4] = [] - self.obj.update_connected_ips(4) - - self.assertEqual(self.obj.called_commands[4], [ - ['-t', 'raw', '-F', 'QBS-PREROUTING'], - ['-t', 'mangle', '-F', 'QBS-POSTROUTING'], - ]) - class TestNftablesWorker(TestCase, WorkerCommon): def setUp(self): super(TestNftablesWorker, self).setUp() diff --git a/rpm_spec/core-agent.spec.in b/rpm_spec/core-agent.spec.in index e19387208..be9cef8ad 100644 --- a/rpm_spec/core-agent.spec.in +++ b/rpm_spec/core-agent.spec.in @@ -615,6 +615,7 @@ sed 's/^net.ipv4.ip_forward.*/#\0/' -i /etc/sysctl.conf %post networking %systemd_post qubes-firewall.service %systemd_post qubes-iptables.service +%systemd_post qubes-antispoof.service %systemd_post qubes-network.service %systemd_post qubes-network-uplink.service %systemd_post qubes-updates-proxy.service @@ -650,6 +651,7 @@ fi %preun networking %systemd_preun qubes-firewall.service %systemd_preun qubes-iptables.service +%systemd_preun qubes-antispoof.service %systemd_preun qubes-network.service %systemd_preun qubes-updates-proxy.service @@ -803,7 +805,7 @@ rm -f %{name}-%{version} %{kde5_service_dir}/qvm-copy.desktop %{kde5_service_dir}/qvm-move.desktop %{kde5_service_dir}/qvm-dvm.desktop -/etc/fstab +%config /etc/fstab /etc/pki/rpm-gpg/RPM-GPG-KEY-qubes* %dir /etc/qubes-rpc %config(noreplace) /etc/qubes-rpc/qubes.ShowInTerminal @@ -1001,9 +1003,10 @@ rm -f %{name}-%{version} %files networking %config /etc/sysctl.d/81-qubes.conf.optional %config(noreplace) /etc/qubes-rpc/qubes.UpdatesProxy -%config(noreplace) /etc/qubes/ip6tables.rules -%config(noreplace) /etc/qubes/ip6tables-enabled.rules -%config(noreplace) /etc/qubes/iptables.rules +%config(noreplace) /etc/qubes/qubes-antispoof.nft +%config(noreplace) /etc/qubes/qubes-ipv4.nft +%config(noreplace) /etc/qubes/qubes-ipv6.nft +%config(noreplace) /etc/qubes/qubes-ipv6-disabled.nft %config(noreplace) /etc/tinyproxy/tinyproxy-updates.conf %config(noreplace) /etc/tinyproxy/updates-blacklist %config(noreplace) /etc/udev/rules.d/99-qubes-network.rules @@ -1012,6 +1015,7 @@ rm -f %{name}-%{version} /etc/xen/scripts/vif-route-qubes %_unitdir/qubes-firewall.service %_unitdir/qubes-iptables.service +%_unitdir/qubes-antispoof.service %_unitdir/qubes-network.service %_unitdir/qubes-network-uplink.service %_unitdir/qubes-network-uplink@.service @@ -1021,7 +1025,6 @@ rm -f %{name}-%{version} /usr/lib/qubes/init/network-proxy-stop.sh /usr/lib/qubes/init/network-uplink-wait.sh /usr/lib/qubes/init/qubes-iptables -/usr/lib/qubes/get-dns-from-resolved /usr/lib/qubes/qubes-setup-dnat-to-ns /usr/lib/qubes/setup-ip %_tmpfilesdir/qubes-core-agent-linux.conf diff --git a/selinux/qubes-xendriverdomain.fc b/selinux/qubes-xendriverdomain.fc index e484f8b84..dfef5e39f 100644 --- a/selinux/qubes-xendriverdomain.fc +++ b/selinux/qubes-xendriverdomain.fc @@ -1 +1,5 @@ -# Intentionally left blank +define(`slash_run',`dnl +/var/run/$1 $3 gen_context(system_u:object_r:$2_t,s0) +/run/$1 $3 gen_context(system_u:object_r:$2_t,s0) +')dnl +slash_run(`xen(/.*)?',`xend_var_run') diff --git a/vm-systemd/75-qubes-vm.preset b/vm-systemd/75-qubes-vm.preset index 45c0e6682..7573e2d25 100644 --- a/vm-systemd/75-qubes-vm.preset +++ b/vm-systemd/75-qubes-vm.preset @@ -99,6 +99,7 @@ enable qubes-mount-dirs.service enable qubes-rootfs-resize.service enable qubes-firewall.service enable qubes-meminfo-writer.service +enable qubes-antispoof.service enable qubes-iptables.service enable qubes-updates-proxy-forwarder.socket enable haveged.service diff --git a/vm-systemd/qubes-antispoof.service b/vm-systemd/qubes-antispoof.service new file mode 100644 index 000000000..ae4d7e512 --- /dev/null +++ b/vm-systemd/qubes-antispoof.service @@ -0,0 +1,10 @@ +[Unit] +Description=Qubes anti-spoofing firewall rules + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/sbin/nft -f /etc/qubes/qubes-antispoof.nft + +[Install] +WantedBy=basic.target diff --git a/vm-systemd/qubes-core-agent-linux.tmpfiles b/vm-systemd/qubes-core-agent-linux.tmpfiles index 4f2362553..0131eb170 100644 --- a/vm-systemd/qubes-core-agent-linux.tmpfiles +++ b/vm-systemd/qubes-core-agent-linux.tmpfiles @@ -1 +1,2 @@ d /run/tinyproxy-updates 0755 tinyproxy tinyproxy +d /run/xen 0700 root root diff --git a/vm-systemd/qubes-iptables.service b/vm-systemd/qubes-iptables.service index e5287753e..466814778 100644 --- a/vm-systemd/qubes-iptables.service +++ b/vm-systemd/qubes-iptables.service @@ -1,5 +1,7 @@ [Unit] Description=Qubes base firewall settings +Requires=qubes-antispoof.service +After=qubes-antispoof.service [Service] Type=oneshot diff --git a/vm-systemd/qubes-sysinit.sh b/vm-systemd/qubes-sysinit.sh index cc2164cdd..1acf7b99c 100755 --- a/vm-systemd/qubes-sysinit.sh +++ b/vm-systemd/qubes-sysinit.sh @@ -19,7 +19,7 @@ done [ -d /sys/fs/selinux ] && selinux_flag=Z || selinux_flag= -mkdir "-p$selinux_flag" /run/qubes /run/qubes-service /run/xen-hotplug +mkdir "-p$selinux_flag" /run/qubes /run/qubes-service /run/xen-hotplug /run/xen chgrp qubes /run/qubes chmod 0775 /run/qubes From 41fff23c6dae16e6eb3f686a79297f1fc5cd8ca7 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Fri, 21 Apr 2023 16:09:20 -0400 Subject: [PATCH 3/4] Add permanent neighbor entries again Previously, neighbor discovery used ARP or NDP, both of which have nonzero attack surface. Additionally, the strict anti-spoofing rules used by Qubes OS tend to break NDP, causing IPv6 traffic to be unreliable. This solves the second problem by adding permanent entries to Linux's neighbor cache. Since the peer MAC address is already known, it is not necessary to use ARP or NDP to discover it. This allows NDP to be blocked outright if it has not been blocked already. --- network/setup-ip | 91 ++++++++++++++++++++++++---------------- network/vif-qubes-nat.sh | 2 + network/vif-route-qubes | 5 +++ 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/network/setup-ip b/network/setup-ip index ad1349019..495960d31 100755 --- a/network/setup-ip +++ b/network/setup-ip @@ -13,7 +13,8 @@ add_default_route () { /sbin/ip -- route replace to unicast default via "$1" dev "$2" onlink } -configure_network() { +readonly netvm_mac=fe:ff:ff:ff:ff:ff +configure_network () { local MAC="$1" local INTERFACE="$2" local ip="$3" @@ -24,10 +25,19 @@ configure_network() { local gateway6="$8" local primary_dns="$9" local secondary_dns="${10}" + local custom="${11}" - /sbin/ip address add "$ip/$netmask" dev "$INTERFACE" + /sbin/ip -- address add "$ip/$netmask" dev "$INTERFACE" + if [[ "$custom" = false ]]; then + /sbin/ip -- neighbour replace to "$gateway" dev "$INTERFACE" \ + lladdr "$netvm_mac" nud permanent + fi if [ -n "$ip6" ]; then - /sbin/ip address add "$ip6/$netmask6" dev "$INTERFACE" + /sbin/ip -- address add "$ip6/$netmask6" dev "$INTERFACE" + if [[ "$custom" = false ]]; then + /sbin/ip -- neighbour replace to "$gateway6" dev "$INTERFACE" \ + lladdr "$netvm_mac" nud permanent + fi fi /sbin/ip link set dev "$INTERFACE" group 1 up @@ -67,7 +77,7 @@ configure_network() { fi } -configure_network_nm() { +configure_network_nm () { local MAC="$1" local INTERFACE="$2" local ip="$3" @@ -78,6 +88,7 @@ configure_network_nm() { local gateway6="$8" local primary_dns="$9" local secondary_dns="${10}" + local custom="${11}" local prefix local prefix6 @@ -155,6 +166,16 @@ __EOF__ chmod 600 "$nm_config" # reload connection nmcli connection load "$nm_config" || : + if [[ "$custom" = false ]]; then + /sbin/ip -- neighbour replace to "$gateway6" dev "$INTERFACE" \ + lladdr "$netvm_mac" nud permanent + fi + if [ -n "$ip6" ]; then + if [[ "$custom" = false ]]; then + /sbin/ip -- neighbour replace to "$gateway6" dev "$INTERFACE" \ + lladdr "$netvm_mac" nud permanent + fi + fi } configure_qubes_ns() { @@ -189,36 +210,36 @@ fi if [ "$ACTION" == "add" ]; then MAC="$(get_mac_from_iface "$INTERFACE")" - if [ -n "$MAC" ]; then - ip="$(/usr/bin/qubesdb-read "/net-config/$MAC/ip" 2> /dev/null)" || ip= - ip6="$(/usr/bin/qubesdb-read "/net-config/$MAC/ip6" 2> /dev/null)" || ip6= - netmask="$(/usr/bin/qubesdb-read "/net-config/$MAC/netmask" 2> /dev/null)" || netmask= - netmask6="$(/usr/bin/qubesdb-read "/net-config/$MAC/netmask6" 2> /dev/null)" || netmask6= - gateway="$(/usr/bin/qubesdb-read "/net-config/$MAC/gateway" 2> /dev/null)" || gateway= - gateway6="$(/usr/bin/qubesdb-read "/net-config/$MAC/gateway6" 2> /dev/null)" || gateway6= - - # Handle legacy values - LEGACY_MAC="$(/usr/bin/qubesdb-read /qubes-mac 2> /dev/null)" || LEGACY_MAC= - if [ "$MAC" == "$LEGACY_MAC" ] || [ -z "$LEGACY_MAC" ]; then - if [ -z "$ip" ]; then - ip="$(/usr/bin/qubesdb-read /qubes-ip 2> /dev/null)" || ip= - fi - if [ -z "$ip6" ]; then - ip6="$(/usr/bin/qubesdb-read /qubes-ip6 2> /dev/null)" || ip6= - fi - if [ -z "$gateway" ]; then - gateway="$(/usr/bin/qubesdb-read /qubes-gateway 2> /dev/null)" || gateway= - fi - if [ -z "$gateway6" ]; then - gateway6="$(/usr/bin/qubesdb-read /qubes-gateway6 2> /dev/null)" || gateway6= - fi + prefix="/net-config/$MAC" + if [[ -n "$MAC" ]]; then + # prefix begins with / so -- is not needed + if /usr/bin/qubesdb-read "${prefix}custom" >/dev/null 2>&1; then + custom=true + elif [[ "$?" = '2' ]]; then + custom=false + else + echo "Could not check if ${prefix}custom exists!" >&2 + exit 1 fi - - if [ -z "$netmask" ]; then - netmask="255.255.255.255" + if ip4=$(exec /usr/bin/qubesdb-read "${prefix}ip" 2>/dev/null); then + : + elif [[ "$?" = '2' ]]; then + prefix=/qubes- + ip4=$(exec /usr/bin/qubesdb-read "${prefix}ip") + else + echo "Could not check if /net-config/$MAC/ip exists!" >&2 + exit 1 fi - if [ -z "$netmask6" ]; then - netmask6="128" + netmask=$(exec /usr/bin/qubesdb-read --default=255.255.255.255 "${prefix}netmask") + gateway=$(exec /usr/bin/qubesdb-read "${prefix}gateway") + if ip6=$(exec /usr/bin/qubesdb-read "${prefix}ip6" 2>/dev/null); then + netmask6=$(exec /usr/bin/qubesdb-read --default=128 "${prefix}netmask6") + gateway6=$(exec /usr/bin/qubesdb-read "${prefix}gateway6") + elif [[ "$?" != '2' ]]; then + echo 'Could not check if IPv6 is enabled' >&2 + exit 1 + else + ip6='' netmask6=128 gateway6='' fi primary_dns=$(/usr/bin/qubesdb-read /qubes-primary-dns 2>/dev/null) || primary_dns= @@ -233,12 +254,12 @@ if [ "$ACTION" == "add" ]; then "--prefix=/net/ipv6/conf/$INTERFACE" \ "--prefix=/net/ipv6/neigh/$INTERFACE" - if [ -n "$ip" ]; then + if [ -n "$ip4" ]; then # If NetworkManager is enabled, let it configure the network if qsvc network-manager && [ -e /usr/bin/nmcli ]; then - configure_network_nm "$MAC" "$INTERFACE" "$ip" "$ip6" "$netmask" "$netmask6" "$gateway" "$gateway6" "$primary_dns" "$secondary_dns" + configure_network_nm "$MAC" "$INTERFACE" "$ip4" "$ip6" "$netmask" "$netmask6" "$gateway" "$gateway6" "$primary_dns" "$secondary_dns" "$custom" else - configure_network "$MAC" "$INTERFACE" "$ip" "$ip6" "$netmask" "$netmask6" "$gateway" "$gateway6" "$primary_dns" "$secondary_dns" + configure_network "$MAC" "$INTERFACE" "$ip4" "$ip6" "$netmask" "$netmask6" "$gateway" "$gateway6" "$primary_dns" "$secondary_dns" "$custom" fi network=$(qubesdb-read /qubes-netvm-network 2>/dev/null) || network= diff --git a/network/vif-qubes-nat.sh b/network/vif-qubes-nat.sh index 7323036c6..1b38bbec6 100755 --- a/network/vif-qubes-nat.sh +++ b/network/vif-qubes-nat.sh @@ -86,6 +86,8 @@ table netdev antispoof { } }" + netns ip neighbour add to "$appvm_ip" dev "$netns_appvm_if" lladdr "$mac" nud permanent + netns ip neighbour add to "$netvm_gw_ip" dev "$netns_netvm_if" lladdr "$netvm_mac" nud permanent netns ip addr add "$netvm_ip" dev "$netns_netvm_if" netns ip addr add "$appvm_gw_ip" dev "$netns_appvm_if" diff --git a/network/vif-route-qubes b/network/vif-route-qubes index 7c4585879..1f9c6994f 100755 --- a/network/vif-route-qubes +++ b/network/vif-route-qubes @@ -214,6 +214,11 @@ if [ "${ip}" ]; then printf 'Cannot purge stale conntrack entries for %q\n' "$addr">&2 exit 1 fi + # Add neighbour entries + if [[ "$command" = 'online' ]]; then + ip -- neighbour add to "$addr" \ + dev "$vif" lladdr "$peer_mac" nud permanent + fi nft "$nftables_cmd element $ipt qubes allowed { \"$vif\" . $addr } $nftables_cmd element $ipt qubes downstream { $addr }" done From 8d5275662c45c852cf3682b4999e9ed017bb4c51 Mon Sep 17 00:00:00 2001 From: Demi Marie Obenour Date: Wed, 19 Apr 2023 16:34:19 -0400 Subject: [PATCH 4/4] Remove an unneded use of cat Tiny speedup; otherwise no functional change. --- network/vif-route-qubes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/vif-route-qubes b/network/vif-route-qubes index 1f9c6994f..1bcb26d1c 100755 --- a/network/vif-route-qubes +++ b/network/vif-route-qubes @@ -115,7 +115,7 @@ case "$command" in ipcmd='add' nftables_cmd=add cmdprefix='' - ipv6_disabled=$(cat "/proc/sys/net/ipv6/conf/$vif/disable_ipv6" || echo 1) + read -r ipv6_disabled < "/proc/sys/net/ipv6/conf/$vif/disable_ipv6" || ipv6_disabled=1 interfaces='' separator=''