#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## Copyright (C) 2024 - 2024 Benjamin Grande M. S. <ben.grande.b@gmail.com>
## See the file COPYING for copying conditions.

#### meta start
#### project Whonix
#### category networking and firewall
#### description
## firewall script
#### meta end

## NOTE: If you make changes to this firewall, think about, if it would
##       make sense to add the changes to Whonix-Gateway script as well.
##       Some things like dropping invalid packages, should be shared in
##       /usr/libexec/whonix-firewall/firewall-common

## TODO:
## - Should allow unlimited TCP/UDP/IPv6 traffic on the virtual external interface (OnionCat / OpenVPN).

## source for some rules:
## http://www.cyberciti.biz/faq/ip6tables-ipv6-firewall-for-linux/

set -eu -o pipefail -o errtrace

# shellcheck source=../libexec/whonix-firewall/firewall-common
source /usr/libexec/whonix-firewall/firewall-common

variables_defaults() {
  common_variables_defaults_post

  ## Not in use/defined yet.
  ## INT_IF could be the internal network.
  ## Internal interface
  [ -n "${INT_IF:-}" ] || INT_IF="eth1"

  if test -f /usr/share/qubes/marker-vm; then
    ## Would fail if netvm is set to 'none',
    ## which is the case in Qubes R4 TemplateVMs.
    [ -n "${GATEWAY_IP:-}" ] || GATEWAY_IP="$(qubesdb-read /qubes-gateway 2> /dev/null)" || GATEWAY_IP="127.0.0.1"
  else
    ## IP HARDCODED. If you want to change IP, set variable GATEWAY_IP through a
    ## drop-in configuration snippet in /etc/whonix_firewall.d
    ## configuration folder instead.
    [ -n "${GATEWAY_IP:-}" ] || GATEWAY_IP="10.152.152.10"
  fi

  ## Since hardcoded in anon-ws-disable-stacked-tor.
  ## IP HARDCODED. If you want to change IP, set variable GATEWAY_IP through a
  ## drop-in configuration snippet in /etc/whonix_firewall.d
  ## configuration folder instead.
  [ -n "${GATEWAY_IP_HARDCODED:-}" ] || GATEWAY_IP_HARDCODED="10.152.152.10"

  [ -n "${TUNNEL_FIREWALL_ALLOW_CONTROL_PORT_FILTER_PROXY:-}" ] || TUNNEL_FIREWALL_ALLOW_CONTROL_PORT_FILTER_PROXY=""
  [ -n "${TUNNEL_FIREWALL_ALLOW_LOCAL_NET:-}" ] || TUNNEL_FIREWALL_ALLOW_LOCAL_NET=""
  [ -n "${TUNNEL_FIREWALL_ALLOW_NOTUNNEL_USER:-}" ] || TUNNEL_FIREWALL_ALLOW_NOTUNNEL_USER=""
  [ -n "${TUNNEL_FIREWALL_ALLOW_SDWDATE_USER:-}" ] || TUNNEL_FIREWALL_ALLOW_SDWDATE_USER=""
  [ -n "${TUNNEL_FIREWALL_ALLOW_SYSTEMCHECK:-}" ] || TUNNEL_FIREWALL_ALLOW_SYSTEMCHECK=""
  [ -n "${TUNNEL_FIREWALL_ALLOW_TB_UPDATER:-}" ] || TUNNEL_FIREWALL_ALLOW_TB_UPDATER=""

  ## Destinations you do not routed through VPN.
  if [ -z "${LOCAL_NET:-}" ]; then
    if test -f /usr/share/qubes/marker-vm; then
      LOCAL_NET="\
            127.0.0.0/24 \
            10.137.0.0/16 \
            10.138.0.0/16 \
         "
    else
      ## 10.0.2.2/24: VirtualBox DHCP
      ## IP HARDCODED unfortunately. Use a /etc/whonix_firewall.d configuration folder drop-in if you want to change it.
      LOCAL_NET="\
            127.0.0.0/24 \
            192.168.0.0/24 \
            192.168.1.0/24 \
            10.152.152.0/24 \
            10.0.2.2/24 \
         "
    fi
  fi

}

nft_defaults() {
  ## Flush old rules.

  $nftables_cmd add table inet filter
  $nftables_cmd add table inet nat

  $nftables_cmd flush table inet filter
  $nftables_cmd flush table inet nat

  $nftables_cmd add chain inet nat output
  $nftables_cmd add chain inet nat prerouting

  ## Set secure defaults.
  $nftables_cmd "add chain inet filter input { type filter hook input priority 0; policy drop; }"

  ## forward rules does not actually do anything if forwarding is disabled. Better be safe just in case.
  $nftables_cmd "add chain inet filter forward { type filter hook forward priority 0; policy drop; }"

  ## Will be lifted below.
  $nftables_cmd "add chain inet filter output { type filter hook output priority 0; policy drop; }"
}

qubes() {
  ## Not yet required. Just so Whonix-Workstation firewall can be more similar
  ## to Whonix-Gateway firewall.
  true
}

qubes_dns() {
  local counter
  counter=0

  ## Using '2>/dev/null' because 'qubesdb-read' DNS would fail in Qubes R4
  ## TemplateVMs, because these are non-networked by default.

  if qubes_primary_dns="$(qubesdb-read /qubes-primary-dns 2> /dev/null)"; then
    $nftables_cmd add rule inet filter output ip daddr "$qubes_primary_dns" udp dport 53 counter accept
    ## TODO: IPv6 test
    $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$qubes_primary_dns" udp dport 53 counter accept
    counter=$((counter + 1))
  fi

  if qubes_secondary_dns="$(qubesdb-read /qubes-secondary-dns 2> /dev/null)"; then
    $nftables_cmd add rule inet filter output ip daddr "$qubes_secondary_dns" udp dport 53 counter accept
    ## TODO: IPv6 test
    $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$qubes_secondary_dns" udp dport 53 counter accept
    counter=$((counter + 1))
  fi

  if [ "${counter:-}" -ge "2" ]; then
    output_cmd "OK: Qubes DNS firewall rules ok."
  else
    $nftables_cmd add rule inet filter output udp dport 53 counter accept
  fi
}

nft_input_rules() {
  ## Traffic on the loopback interface is accepted.
  $nftables_cmd add rule inet filter input iifname "lo" counter accept

  ## Established incoming connections are accepted.
  $nftables_cmd add rule inet filter input ct state established counter accept

  ## Allow all incoming connections on the virtual VPN network interface,
  ## when TUNNEL_FIREWALL_ENABLE mode is enabled.
  ## DISABLED BY DEFAULT.
  if [ "$TUNNEL_FIREWALL_ENABLE" = "true" ]; then
    $nftables_cmd add rule inet filter input iifname "$VPN_INTERFACE" counter accept
  fi

  if [ "$firewall_mode" = "timesync-fail-closed" ]; then
    true "firewall_mode is $firewall_mode, therefore not opening EXTERNAL_OPEN_PORTS."
  else
    local local_port_to_open
    if [ "${info_enabled}" = "1" ]; then
      output_cmd "INFO: Opening External TCP port(s): $(output_trim "${EXTERNAL_OPEN_PORTS:-"NONE"}")"
    fi
    for local_port_to_open in $EXTERNAL_OPEN_PORTS; do
      $nftables_cmd add rule inet filter input tcp dport "$local_port_to_open" counter accept
    done

    local local_udp_port_to_open
    if [ "${info_enabled}" = "1" ]; then
      output_cmd "INFO: Opening External UDP port(s): $(output_trim "${EXTERNAL_UDP_OPEN_PORTS:-"NONE"}")"
    fi
    for local_udp_port_to_open in $EXTERNAL_UDP_OPEN_PORTS; do
      $nftables_cmd add rule inet filter input udp dport "$local_udp_port_to_open" counter accept
    done

    if [ "$EXTERNAL_OPEN_ALL" = "true" ]; then
      if [ "${info_enabled}" = "1" ]; then
        output_cmd "INFO: EXTERNAL_OPEN_ALL='true', all external ports will be opened"
      fi
      $nftables_cmd add rule inet filter input counter accept
    fi
  fi
}

nft_input_defaults() {
  ## Log.
  #$nftables_cmd add rule inet filter input counter log prefix \"Whonix_blocked_input4:\"

  ## Required for Control Port Filter Proxy Connection.
  ## https://phabricator.whonix.org/T112
  $nftables_cmd add rule inet filter input ip protocol tcp counter reject with tcp reset

  ## Reject anything not explicitly allowed above.
  $nftables_cmd add rule inet filter input counter reject
}

nft_forward() {
  ## Log.
  #$nftables_cmd add rule inet filter forward counter log prefix \"Whonix_blocked_forward4:\"

  $nftables_cmd add rule inet filter forward counter drop
}

nft_output() {
  ## Prevent connections to Tor SocksPorts.
  ## https://phabricator.whonix.org/T533#11025
  if [ "$firewall_mode" = "timesync-fail-closed" ]; then
    local socks_port_item
    output_cmd "INFO: not opening Internal TCP ports $(output_trim "${INTERNAL_OPEN_PORTS}"), except 9108 for sdwdate, because firewall_mode=$firewall_mode"
    for socks_port_item in $INTERNAL_OPEN_PORTS; do
      true "socks_port_item: $socks_port_item"
      ## SOCKS_PORT_SDWDATE
      if [ "$socks_port_item" = "9108" ]; then
        ## Permit connections to SOCKS_PORT_SDWDATE 9108 even in
        ## 'timesync-fail-closed' mode. Otherwise, sdwdate could never
        ## succeed and firewall_mode could never change to 'full'.
        continue
      fi
      $nftables_cmd add rule inet filter output ip daddr 127.0.0.1 tcp dport "$socks_port_item" counter reject
      ## TODO: IPv6 test
      $nftables_cmd add rule inet filter output ip6 daddr ::1 tcp dport "$socks_port_item" counter reject
    done
  fi

  ## TODO: block Qubes UpdatesProxy in timesync-fail-closed firewall_mode

  ## Access to localhost is required even in timesync-fail-closed mode,
  ## otherwise breaks applications such as konsole and kwrite.
  $nftables_cmd add rule inet filter output oifname "lo" counter accept

  ## Allow outgoing traffic on VPN interface,
  ## if TUNNEL_FIREWALL_ENABLE mode is enabled.
  ## DISABLED BY DEFAULT.
  if [ "$TUNNEL_FIREWALL_ENABLE" = "true" ]; then
    if [ "$firewall_mode" = "timesync-fail-closed" ]; then
      true "firewall_mode is $firewall_mode, therefore prohibiting user $TUNNEL_USER traffic."
    else
      true "firewall_mode is $firewall_mode, therefore allowing user $TUNNEL_USER traffic."
      ## Connections to VPN servers are allowed,
      $nftables_cmd add rule inet filter output oifname "$VPN_INTERFACE" counter accept
      if [ -n "$TUNNEL_USER" ]; then
        $nftables_cmd add rule inet filter output skuid "$TUNNEL_USER" counter accept
      fi
    fi

    if [ "$TUNNEL_FIREWALL_ALLOW_SDWDATE_USER" = "true" ]; then
      if [ -n "$SDWDATE_USER" ]; then
        $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip daddr 127.0.0.1 counter accept
        $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip6 daddr ::1 counter accept
        $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip daddr "$GATEWAY_IP" counter accept
        $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip6 daddr "::ffff:$GATEWAY_IP" counter accept
        $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip daddr "$GATEWAY_IP_HARDCODED" counter accept
        $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip6 daddr "::ffff:$GATEWAY_IP_HARDCODED" counter accept
        ## TODO: IPv6 test
      fi
    fi

    if [ "$TUNNEL_FIREWALL_ALLOW_NOTUNNEL_USER" = "true" ]; then
      if [ -n "$NOTUNNEL_USER" ]; then
        $nftables_cmd add rule inet filter output skuid "$NOTUNNEL_USER" ip daddr 127.0.0.1 counter accept
        $nftables_cmd add rule inet filter output skuid "$NOTUNNEL_USER" ip6 daddr ::1 counter accept
        $nftables_cmd add rule inet filter output skuid "$NOTUNNEL_USER" ip daddr "$GATEWAY_IP" counter accept
        $nftables_cmd add rule inet filter output skuid "$NOTUNNEL_USER" ip6 daddr "::ffff:$GATEWAY_IP" counter accept
        $nftables_cmd add rule inet filter output skuid "$NOTUNNEL_USER" ip daddr "$GATEWAY_IP_HARDCODED" counter accept
        $nftables_cmd add rule inet filter output skuid "$NOTUNNEL_USER" ip6 daddr "::ffff:$GATEWAY_IP_HARDCODED" counter accept
        ## TODO: IPv6 test
      fi
    fi

    ## Accept outgoing connections to local network.
    if [ "$TUNNEL_FIREWALL_ALLOW_LOCAL_NET" = "true" ]; then
      if [ "$firewall_mode" = "timesync-fail-closed" ]; then
        true
      else
        local local_net_item
        for local_net_item in $LOCAL_NET; do
          $nftables_cmd add rule inet filter output ip daddr "$local_net_item" counter accept
          $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$local_net_item" counter accept
          ## TODO: IPv6 test
        done
      fi
    fi

    if [ "$TUNNEL_FIREWALL_ALLOW_CONTROL_PORT_FILTER_PROXY" = "true" ]; then
      $nftables_cmd add rule inet filter output ip daddr 127.0.0.1 tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip6 daddr ::1 tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP_HARDCODED" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP_HARDCODED" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      ## TODO: IPv6 test
    fi

    if [ "$TUNNEL_FIREWALL_ALLOW_TB_UPDATER" = "true" ]; then
      if [ "$firewall_mode" = "timesync-fail-closed" ]; then
        true
      else
        ## SOCKS_PORT_TBB_DOWNLOAD
        $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP" tcp dport 9115 counter accept
        $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP" tcp dport 9115 counter accept
        $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP_HARDCODED" tcp dport 9115 counter accept
        $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP_HARDCODED" tcp dport 9115 counter accept
        ## TODO: IPv6 test
      fi
    fi

    if [ "$TUNNEL_FIREWALL_ALLOW_SYSTEMCHECK" = "true" ]; then
      if [ "$firewall_mode" = "timesync-fail-closed" ]; then
        true
      else
        if [ -n "$SYSTEMCHECK_USER" ]; then
          $nftables_cmd add rule inet filter output skuid "$SYSTEMCHECK_USER" inet daddr 127.0.0.1 counter accept
          $nftables_cmd add rule inet filter output skuid "$SYSTEMCHECK_USER" inet daddr "$GATEWAY_IP" counter accept
          $nftables_cmd add rule inet filter output skuid "$SYSTEMCHECK_USER" inet daddr "$GATEWAY_IP_HARDCODED" counter accept
        fi
      fi
    fi
  else
    if [ "$firewall_mode" = "timesync-fail-closed" ]; then
      true "firewall_mode is $firewall_mode, therefore prohibiting DNS traffic."
    else
      true "firewall_mode is $firewall_mode, therefore allowing DNS traffic."
      ## Allow Whonix-Workstation to query Whonix-Gateway for DNS.
      $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP" udp dport 53 counter accept
      $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP" udp dport 53 counter accept
      $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP_HARDCODED" udp dport 53 counter accept
      $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP_HARDCODED" udp dport 53 counter accept
      ## TODO: IPv6 test
      if test -f /usr/share/qubes/marker-vm; then
        qubes_dns
      fi
    fi

    ## Not sure about the next one. UDP is not supported by Tor, why not
    ## block any outgoing UDP. Might have unwanted side effects when tunneling
    ## UDP over Tor.
    ## https://www.whonix.org/wiki/Tunnel_UDP_over_Tor
    ##
    ## All other non-TCP protocol traffic gets rejected.
    ## iptables knows 7 different protocols and all.
    ## (tcp, udp, udplite, icmp, esp, ah, sctp or all)
    ##
    ## IP HARDCODED but no need to change since comment only.
    ##
    ## (1) ping torproject.org
    ##     4 packets transmitted, 0 received, 100% packet loss, time 3000ms
    ##
    ## (2) ping torproject.org
    ##     From 10.152.152.11 icmp_seq=1 Destination Port Unreachable
    ##     0 packets transmitted, 0 received, +100 errors
    ##
    ## The next rule ensures, that only tcp can leave and achieves the desired result from (2).
    if [ "$firewall_allow_udp" = "true" ]; then
      true "Allowing UDP."
    else
      $nftables_cmd add rule inet filter output ip protocol != tcp counter reject
    fi

    if [ "$firewall_mode" = "timesync-fail-closed" ]; then
      true "firewall_mode is $firewall_mode, therefore prohibiting all outgoing traffic."

      ## Allow sdwdate talking to localhost and Tor in Whonix firewall timesync-fail-closed mode.
      ## Otherwise in Whonix firewall full mode this rule is redundant.

      local allowed_user_list_item
      for allowed_user_list_item in $SDWDATE_USER $SYSTEMCHECK_USER; do
        $nftables_cmd add rule inet filter output skuid "$allowed_user_list_item" ip daddr 127.0.0.1 counter accept
        $nftables_cmd add rule inet filter output skuid "$allowed_user_list_item" ip6 daddr ::1 counter accept
        $nftables_cmd add rule inet filter output skuid "$allowed_user_list_item" ip daddr "$GATEWAY_IP" counter accept
        $nftables_cmd add rule inet filter output skuid "$allowed_user_list_item" ip6 daddr "::ffff:$GATEWAY_IP" counter accept
        $nftables_cmd add rule inet filter output skuid "$allowed_user_list_item" ip daddr "$GATEWAY_IP_HARDCODED" counter accept
        $nftables_cmd add rule inet filter output skuid "$allowed_user_list_item" ip6 daddr "::ffff:$GATEWAY_IP_HARDCODED" counter accept
        ## TODO: IPv6 test
      done

      $nftables_cmd add rule inet filter output ip daddr 127.0.0.1 tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip6 daddr ::1 tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip daddr "$GATEWAY_IP_HARDCODED" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      $nftables_cmd add rule inet filter output ip6 daddr "::ffff:$GATEWAY_IP_HARDCODED" tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
      ## TODO: IPv6 test
    else
      if [ -z "$outgoing_allow_ip_list" ]; then
        true "firewall_mode is $firewall_mode and outgoing_allow_ip_list is empty, therefore allowing all outgoing traffic."
        $nftables_cmd add rule inet filter output counter accept
      else
        local outgoing_allow_ip_item
        for outgoing_allow_ip_item in $outgoing_allow_ip_list; do
          true "firewall_mode is $firewall_mode and outgoing_allow_ip_list is set, allowing IP: $outgoing_allow_ip_list"
          $nftables_cmd add rule inet filter output ip protocol tcp ip daddr "$outgoing_allow_ip_item" counter accept
          $nftables_cmd add rule inet filter output ip6 protocol tcp ip6 daddr "::ffff:$outgoing_allow_ip_item" counter accept
          ## TODO: IPv6 test
        done
      fi
    fi

    ## Log.
    #$nftables_cmd add rule inet filter output counter log prefix \"Whonix_blocked_output4:\"

    ## Reject all other outgoing traffic.
    $nftables_cmd add rule inet filter output counter reject
  fi
}

variable_list="
CONTROL_PORT_FILTER_PROXY_PORT
EXTERNAL_OPEN_ALL
EXTERNAL_OPEN_PORTS
EXTERNAL_UDP_OPEN_PORTS
EXT_IF
GATEWAY_IP
GATEWAY_IP_HARDCODED
INTERNAL_OPEN_PORTS
INT_IF
LOCAL_NET
NOTUNNEL_USER
NO_REJECT_INVALID_OUTGOING_PACKAGES
SDWDATE_USER
SYSTEMCHECK_USER
TUNNEL_FIREWALL_ALLOW_CONTROL_PORT_FILTER_PROXY
TUNNEL_FIREWALL_ALLOW_LOCAL_NET
TUNNEL_FIREWALL_ALLOW_NOTUNNEL_USER
TUNNEL_FIREWALL_ALLOW_SDWDATE_USER
TUNNEL_FIREWALL_ALLOW_SYSTEMCHECK
TUNNEL_FIREWALL_ALLOW_TB_UPDATER
TUNNEL_FIREWALL_ENABLE
TUNNEL_USER
UPDATESPROXYCHECK_USER
VPN_FIREWALL
VPN_INTERFACE
firewall_allow_udp
firewall_mode
info_enabled
outgoing_allow_ip_list
source_only
"

main() {
  get_options "${@}"
  if [ "$source_only" = "1" ]; then
    return 0
  fi
  init
  source_config_folder
  variables_defaults
  set -f
  nft_script_header
  firewall_mode_detection
  print_variables
  nft_defaults
  nft_drop_invalid_incoming_packages
  qubes
  nft_input_rules
  nft_input_defaults
  nft_forward
  nft_reject_invalid_outgoing_packages
  nft_output
  nft_check
  nft_load
  status_files
  end
}

main "${@}"
