#!/bin/bash

## Copyright (C) 2014 - 2015 Jason Mehring <nrgaway@gmail.com>
## 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

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

  [ -n "${ALLOW_GATEWAY_ROOT_USER:-}" ] || ALLOW_GATEWAY_ROOT_USER=0
  [ -n "${ALLOW_GATEWAY_USER_USER:-}" ] || ALLOW_GATEWAY_USER_USER=0
  [ -n "${CONTROL_PORT_FILTER_PROXY_ENABLE:-}" ] || CONTROL_PORT_FILTER_PROXY_ENABLE=1
  [ -n "${DIR_PORT:-}" ] || DIR_PORT=80
  [ -n "${GATEWAY_ALLOW_INCOMING_DIR_PORT:-}" ] || GATEWAY_ALLOW_INCOMING_DIR_PORT=0
  [ -n "${GATEWAY_ALLOW_INCOMING_ICMP:-}" ] || GATEWAY_ALLOW_INCOMING_ICMP=0
  [ -n "${GATEWAY_ALLOW_INCOMING_ICMP_FRAG_NEEDED:-}" ] || GATEWAY_ALLOW_INCOMING_ICMP_FRAG_NEEDED=1
  [ -n "${GATEWAY_ALLOW_INCOMING_OR_PORT:-}" ] || GATEWAY_ALLOW_INCOMING_OR_PORT=0
  [ -n "${GATEWAY_ALLOW_INCOMING_RELATED_STATE:-}" ] || GATEWAY_ALLOW_INCOMING_RELATED_STATE=""
  [ -n "${GATEWAY_ALLOW_INCOMING_SSH:-}" ] || GATEWAY_ALLOW_INCOMING_SSH=0
  [ -n "${GATEWAY_TRANSPARENT_DNS:-}" ] || GATEWAY_TRANSPARENT_DNS=0
  [ -n "${GATEWAY_TRANSPARENT_TCP:-}" ] || GATEWAY_TRANSPARENT_TCP=0
  [ -n "${GATEWAY_TRANSPARENT_UDP:-}" ] || GATEWAY_TRANSPARENT_UDP=0
  [ -n "${OR_PORT:-}" ] || OR_PORT=443
  [ -n "${WORKSTATION_ALLOW_SOCKSIFIED:-}" ] || WORKSTATION_ALLOW_SOCKSIFIED=1
  [ -n "${WORKSTATION_TRANSPARENT_DNS:-}" ] || WORKSTATION_TRANSPARENT_DNS=1
  [ -n "${WORKSTATION_TRANSPARENT_TCP:-}" ] || WORKSTATION_TRANSPARENT_TCP=1
  [ -n "${WORKSTATION_TRANSPARENT_UDP:-}" ] || WORKSTATION_TRANSPARENT_UDP=0

  ## The following ports are used
  ## - here in /usr/bin/whonix_firewall (package: whonix-gw-firewall)
  ## - by Tor in /usr/share/tor/tor-service-defaults-torrc (package: anon-gw-anonymizer-config)
  ##
  ## The following applications will be separated, preventing identity
  ## correlation through circuit sharing.

  ## Transparent Proxy Ports for Whonix-Workstation
  [ -n "${TRANS_PORT_WORKSTATION:-}" ] || TRANS_PORT_WORKSTATION="9040"
  [ -n "${DNS_PORT_WORKSTATION:-}" ] || DNS_PORT_WORKSTATION="5300"

  ## Transparent Proxy Ports for Whonix-Gateway
  [ -n "${TRANS_PORT_GATEWAY:-}" ] || TRANS_PORT_GATEWAY="9041"
  [ -n "${DNS_PORT_GATEWAY:-}" ] || DNS_PORT_GATEWAY="5400"

  [ -n "${GATEWAY_ALLOW_INCOMING_FLASHPROXY:-}" ] || GATEWAY_ALLOW_INCOMING_FLASHPROXY="0"
  [ -n "${FLASHPROXY_PORT:-}" ] || FLASHPROXY_PORT="9000"

  if [ ! "$WORKSTATION_ALLOW_SOCKSIFIED" = "1" ]; then
    output_cmd "INFO: WORKSTATION_ALLOW_SOCKSIFIED='${WORKSTATION_ALLOW_SOCKSIFIED}', Socks Ports will not be opened"
    return 0
  fi

  NO_NAT_USERS+=" $CLEARNET_USER"
  NO_NAT_USERS+=" $TUNNEL_USER"

  ## No NAT for account "user".
  ## DISABLED BY DEFAULT. For testing/debugging only.
  if [ "$ALLOW_GATEWAY_USER_USER" = "1" ]; then
    if [ -z "$USER_USER" ]; then
      output_cmd "INFO: USER_USER is empty. Not adding USER_USER to NO_NAT_USERS."
    else
      NO_NAT_USERS+=" $USER_USER"
    fi
  fi

  ## No NAT for account "root".
  ## DISABLED BY DEFAULT. For testing/debugging only.
  if [ "$ALLOW_GATEWAY_ROOT_USER" = "1" ]; then
    NO_NAT_USERS+=" $ROOT_USER"
  fi

  ## No NAT for Tor itself,
  ## unless VPN_FIREWALL mode is enabled.
  if [ "$VPN_FIREWALL" != "1" ]; then
    NO_NAT_USERS+=" $TOR_USER"
  fi

  if test -f /usr/share/qubes/marker-vm; then
    [ -n "${INT_IF:-}" ] || INT_IF="vif*"
    [ -n "${INT_TIF:-}" ] || INT_TIF="vif*"
  fi

  ## Internal interface
  [ -n "${INT_IF:-}" ] || INT_IF="eth1"
  ## Internal "tunnel" interface, usually the same as the Internal interface
  ## unless using vpn tunnels # between workstations and gateway
  [ -n "${INT_TIF:-}" ] || INT_TIF="eth1"

  if [ -z "${NON_TOR_GATEWAY:-}" ]; then
    if test -f /usr/share/qubes/marker-vm; then
      NON_TOR_GATEWAY=""
    else
      ## 10.0.2.2/24: VirtualBox DHCP
      ## 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.
      NON_TOR_GATEWAY="\
            127.0.0.0/8 \
            192.168.0.0/24 \
            192.168.1.0/24 \
            10.152.152.0/24 \
            10.0.2.2/24 \
         "
    fi
  fi

  if [ -z "${NON_TOR_GATEWAY_IP6:-}" ]; then
    if test -f /usr/share/qubes/marker-vm; then
      NON_TOR_GATEWAY_IP6=""
    else
      ## 10.0.2.2/24: VirtualBox DHCP
      ## 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.
      NON_TOR_GATEWAY_IP6="\
            ::1/128 \
            fd19:c33d:88bc::0/96 \
            ::ffff:127.0.0.0/104 \
            ::ffff:192.168.0.0/120 \
            ::ffff:192.168.1.0/120 \
            ::ffff:10.152.152.0/120 \
            ::ffff:10.0.2.2/120 \
         "
    fi
  fi

  ## Destinations you do not routed through VPN, only for Whonix-Gateway.
  if [ -z "${LOCAL_NET:-}" ]; then
    if test -f /usr/share/qubes/marker-vm; then
      LOCAL_NET="\
            127.0.0.0/8 \
            10.137.0.0/16 \
            10.138.0.0/16 \
         "
    else
      ## 10.0.2.2/24: VirtualBox DHCP
      ## 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.
      LOCAL_NET="\
            127.0.0.0/8 \
            192.168.0.0/24 \
            192.168.1.0/24 \
            10.152.152.0/24 \
            10.0.2.2/24 \
         "
    fi
  fi


  ## Destinations you do not routed through VPN, only for Whonix-Gateway.
  if [ -z "${LOCAL_NET_IP6:-}" ]; then
    if test -f /usr/share/qubes/marker-vm; then
      LOCAL_NET_IP6="\
            ::1/128 \
            fd09:24ef:4179::a8a:0/112 \
            fd09:24ef:4179::a89:0/112 \
            ::ffff:127.0.0.0/104 \
            ::ffff:10.137.0.0/112 \
            ::ffff:10.138.0.0/112 \
         "
    else
      ## 10.0.2.2/24: VirtualBox DHCP
      ## 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.
      LOCAL_NET_IP6="\
            ::1/128 \
            fd19:c33d:88bc::0/96 \
            ::ffff:127.0.0.0/104 \
            ::ffff:192.168.0.0/120 \
            ::ffff:192.168.1.0/120 \
            ::ffff:10.152.152.0/120 \
            ::ffff:10.0.2.2/120 \
         "
    fi
  fi

  if [ -z "${WORKSTATION_DEST_SOCKSIFIED:-}" ]; then
    ## 10.152.152.10 - Non-Qubes-Whonix-Gateway IP
    ##
    ## 10.137.0.0/8  - persistent Qubes-Whonix-Gateway IP range
    ## 10.138.0.0/8  - DispVM Qubes-Whonix-Gateway IP range
    if test -f /usr/share/qubes/marker-vm; then
      ## https://forums.whonix.org/t/whonix-gateway-not-reachable/7484/16
      ## Qubes-Whonix:
      ## IP HARDCODED. IP 10.152.152.10 is hardcoded in some places.
      WORKSTATION_DEST_SOCKSIFIED="\
        10.137.0.0/16 \
        10.138.0.0/16 \
        10.152.152.10 \
      "
    else
      ## Non-Qubes-Whonix:
      ## 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.
      WORKSTATION_DEST_SOCKSIFIED="10.152.152.10"
    fi
  fi

  if [ -z "${WORKSTATION_DEST_SOCKSIFIED_IPV6:-}" ]; then
    ## fd19:c33d:88bc::10 - Non-Qubes-Whonix-Gateway IP
    ##
    ## fd09:24ef:4179::a89:0/112  - persistent Qubes-Whonix-Gateway IP range
    ## fd09:24ef:4179::a8a:0/112  - DispVM Qubes-Whonix-Gateway IP range
    if test -f /usr/share/qubes/marker-vm; then
      ## https://forums.whonix.org/t/whonix-gateway-not-reachable/7484/16
      ## Qubes-Whonix:
      ## IP HARDCODED. IP 10.152.152.10 is hardcoded in some places.
      WORKSTATION_DEST_SOCKSIFIED_IPV6="\
        fd09:24ef:4179::a8a:0/112 \
        fd09:24ef:4179::a89:0/112 \
        ::ffff:10.137.0.0/112 \
        ::ffff:10.139.0.0/112 \
        ::ffff:10.152.152.10 \
        fd19:c33d:88bc::10 \
      "
    else
      ## Non-Qubes-Whonix:
      ## 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.
      WORKSTATION_DEST_SOCKSIFIED_IPV6="fd19:c33d:88bc::10"
    fi
  fi
}

nft_defaults() {
  ## Flush old rules.

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

  $nftables_cmd delete table inet nat
  $nftables_cmd delete table inet filter
  $nftables_cmd delete table ip6 nat

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

  ## 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; }"

  $nftables_cmd "add chain inet nat prerouting { type nat hook prerouting priority -100; }"
  $nftables_cmd "add chain inet nat output { type nat hook output priority -100; }"

  $nftables_cmd "add chain ip6 nat output { type nat hook output priority -100; }"
}

qubes() {
  qubes_gateway
}

nft_input_rules(){
  nft_input_rules_gateway
}

nft_input_defaults() {
  nft_input_defaults_gateway
}

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

  ## Reject everything.
  $nftables_cmd add rule inet filter forward counter reject
}

nft_output() {
  ## Allow outgoing traffic on VPN interface,
  ## if VPN_FIREWALL mode is enabled.
  ## DISABLED BY DEFAULT.
  if [ "$VPN_FIREWALL" = "1" ]; then
    $nftables_cmd add rule inet filter output oifname "$VPN_INTERFACE" counter accept
  fi

  local no_nat_user_item
  for no_nat_user_item in $NO_NAT_USERS; do
    $nftables_cmd add rule inet nat output skuid "$no_nat_user_item" counter return
  done

  if [ "$firewall_mode" = "full" ]; then
    ## Redirect of Gateway DNS traffic to DNS_PORT_GATEWAY.
    ## DISABLED BY DEFAULT. default. Using SocksPort instead.
    if [ "$GATEWAY_TRANSPARENT_DNS" = "1" ]; then
      $nftables_cmd add rule inet nat output udp dport 53 counter redirect to :"$DNS_PORT_GATEWAY"
    fi

    ## Exclude connections to local network, Whonix-Workstation, VirtualBox from being redirected through Tor,
    ## unless VPN_FIREWALL mode is enabled.
    ## ENABLED BY DEFAULT.
    if [ ! "$VPN_FIREWALL" = "1" ]; then
      local non_tor_gateway_item
      for non_tor_gateway_item in $NON_TOR_GATEWAY; do
        $nftables_cmd add rule inet nat output ip daddr "$non_tor_gateway_item" counter return
      done
      for non_tor_gateway_item in $NON_TOR_GATEWAY_IP6; do
        $nftables_cmd add rule inet nat output ip6 daddr "$non_tor_gateway_item" counter return
      done
    fi

    ## Redirect all Gateway TCP traffic to TRANS_PORT_GATEWAY.
    ## DISABLED BY DEFAULT. Using SocksPort instead.
    if [ "$GATEWAY_TRANSPARENT_TCP" = "1" ]; then
      $nftables_cmd add rule inet nat output tcp flags "&" "(fin|syn|rst|ack)" == syn counter redirect to :"$TRANS_PORT_GATEWAY"
    fi
  fi

  ## Existing connections are accepted.
  $nftables_cmd add rule inet filter output ct state established counter accept

  # Accept ICMPv6 neighbor discovery.
  $nftables_cmd add rule inet filter output icmpv6 type "{ nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect }" counter accept

  if [ "$firewall_mode" = "full" ]; then
    ## Accept outgoing connections to local network, Whonix-Workstation and VirtualBox,
    ## unless VPN_FIREWALL mode is enabled.
    ## ENABLED BY DEFAULT.
    if [ ! "$VPN_FIREWALL" = "1" ]; then
      local non_tor_gateway_item
      for non_tor_gateway_item in $NON_TOR_GATEWAY; do
        $nftables_cmd add rule inet filter output ip daddr "$non_tor_gateway_item" counter accept
      done
      for non_tor_gateway_item in $NON_TOR_GATEWAY_IP6; do
        $nftables_cmd add rule inet filter output ip6 daddr "$non_tor_gateway_item" counter accept
      done
    fi
  fi

  if [ "$firewall_mode" = "full" ]; then
    ## Accept outgoing connections to local network,
    ## when VPN_FIREWALL mode is enabled.
    ## DISABLED BY DEFAULT.
    if [ "$VPN_FIREWALL" = "1" ]; then
      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
      done
      for local_net_item in $LOCAL_NET_IP6; do
        $nftables_cmd add rule inet filter output ip6 daddr "$local_net_item" counter accept
      done
    fi
  fi

  ## Prevent connections to Tor SocksPorts.
  ## https://forums.whonix.org/t/iptables-block-network-access-until-sdwdate-succeeded/18872
  if [ "$firewall_mode" = "timesync-fail-closed" ]; then
    output_cmd "INFO: not opening Internal TCP port(s): $(output_trim "${INTERNAL_OPEN_PORTS}"), except 9108 for sdwdate, because firewall_mode=${firewall_mode}"
    local socks_port_item
    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

  ## 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

  local no_nat_user_item
  for no_nat_user_item in $NO_NAT_USERS; do
    $nftables_cmd add rule inet filter output skuid "$no_nat_user_item" counter accept
  done

  if [ "$firewall_mode" = "timesync-fail-closed" ]; then
    ## Allow sdwdate talking to localhost and Tor in Whonix firewall timesync-fail-closed mode.
    ## Otherwise in Whonix firewall full mode this rule is redundant.
    if [ -n "$SDWDATE_USER" ]; then
      $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip daddr 127.0.0.1 counter accept
      ## TODO: IPV6 test
      $nftables_cmd add rule inet filter output skuid "$SDWDATE_USER" ip6 daddr ::1 counter accept
    fi

    if [ -n "$SYSTEMCHECK_USER" ]; then
      $nftables_cmd add rule inet filter output skuid "$SYSTEMCHECK_USER" ip daddr 127.0.0.1 counter accept
      ## TODO: IPV6 test
      $nftables_cmd add rule inet filter output skuid "$SYSTEMCHECK_USER" ip6 daddr ::1 counter accept
    fi

    $nftables_cmd add rule inet filter output ip daddr 127.0.0.1 tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
    ## TODO: IPV6 test
    $nftables_cmd add rule inet filter output ip6 daddr ::1 tcp dport "$CONTROL_PORT_FILTER_PROXY_PORT" counter accept
  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
}

variable_list="
ALLOW_GATEWAY_ROOT_USER
ALLOW_GATEWAY_USER_USER
CLEARNET_USER
CONTROL_PORT_FILTER_PROXY_ENABLE
CONTROL_PORT_FILTER_PROXY_PORT
DIR_PORT
DNS_PORT_GATEWAY
DNS_PORT_WORKSTATION
EXTERNAL_OPEN_ALL
EXTERNAL_OPEN_PORTS
EXTERNAL_UDP_OPEN_PORTS
EXT_IF
FLASHPROXY_PORT
GATEWAY_ALLOW_INCOMING_DIR_PORT
GATEWAY_ALLOW_INCOMING_FLASHPROXY
GATEWAY_ALLOW_INCOMING_ICMP
GATEWAY_ALLOW_INCOMING_ICMP_FRAG_NEEDED
GATEWAY_ALLOW_INCOMING_OR_PORT
GATEWAY_ALLOW_INCOMING_RELATED_STATE
GATEWAY_ALLOW_INCOMING_SSH
GATEWAY_TRANSPARENT_DNS
GATEWAY_TRANSPARENT_TCP
GATEWAY_TRANSPARENT_UDP
INTERNAL_OPEN_PORTS
INT_IF
INT_TIF
LOCAL_NET
LOCAL_NET_IP6
NON_TOR_GATEWAY
NON_TOR_GATEWAY_IP6
NO_NAT_USERS
NO_REJECT_INVALID_OUTGOING_PACKAGES
OR_PORT
ROOT_USER
SDWDATE_USER
SYSTEMCHECK_USER
TOR_USER
TRANS_PORT_GATEWAY
TRANS_PORT_WORKSTATION
TUNNEL_FIREWALL_ENABLE
TUNNEL_USER
USER_USER
VPN_FIREWALL
VPN_INTERFACE
WORKSTATION_ALLOW_SOCKSIFIED
WORKSTATION_DEST_SOCKSIFIED
WORKSTATION_DEST_SOCKSIFIED_IPV6
WORKSTATION_TRANSPARENT_DNS
WORKSTATION_TRANSPARENT_TCP
WORKSTATION_TRANSPARENT_UDP
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 "${@}"
