#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=network-common
source "$SCRIPT_DIR/network-common"
network_lock

[[ ${CFPP_ARMED:-0} == 1 ]] || fail "network changes are not armed; set CFPP_ARMED=1 only for an approved rollout"

for command in ip iptables ipset grep awk sort sed mktemp chmod mv rm python3; do
  command -v "$command" >/dev/null || fail "required command not found: $command"
done

preflight_policy_table

current_route_localnet=$(<"$CFPP_ROUTE_LOCALNET_PATH")
if [[ "$current_route_localnet" != "$CFPP_ROUTE_LOCALNET_PREVIOUS" && "$current_route_localnet" != 1 ]]; then
  fail "route_localnet changed from the recorded baseline"
fi

interface_has_address "$CFPP_INGRESS_INTERFACE" "$CFPP_INGRESS_ADDRESS" || fail "$CFPP_INGRESS_ADDRESS is not configured on $CFPP_INGRESS_INTERFACE"

# Resolve topology and reject route/chain conflicts before any mutation.
DOCKER_ROUTES=$(docker_bridge_routes)
preflight_docker_routes "$DOCKER_ROUTES"
for pair in "mangle:$CFPP_PRE_CHAIN" "mangle:$CFPP_OUT_CHAIN" "nat:$CFPP_NAT_CHAIN" "filter:$CFPP_INPUT_CHAIN"; do
  preflight_owned_chain "${pair%%:*}" "${pair#*:}"
done

for temporary_set in "$CFPP_TRUSTED_V4_NEXT" "$CFPP_DESTINATION_V4_NEXT"; do
  if ipset list -n | grep -Fxq "$temporary_set"; then
    fail "temporary ipset $temporary_set already exists"
  fi
done
if ! ipset list -n | grep -Fxq "$CFPP_TRUSTED_V4_SET"; then
  ipset create "$CFPP_TRUSTED_V4_SET" hash:net family inet
fi
ipset list "$CFPP_TRUSTED_V4_SET" | grep -Fxq 'Type: hash:net' || fail "ipset $CFPP_TRUSTED_V4_SET has an unexpected type"
ipset list "$CFPP_TRUSTED_V4_SET" | grep -Eq '^Header: family inet( |$)' || fail "ipset $CFPP_TRUSTED_V4_SET has an unexpected family"
if ! ipset list -n | grep -Fxq "$CFPP_DESTINATION_V4_SET"; then
  ipset create "$CFPP_DESTINATION_V4_SET" hash:ip family inet
fi
ipset list "$CFPP_DESTINATION_V4_SET" | grep -Fxq 'Type: hash:ip' || fail "ipset $CFPP_DESTINATION_V4_SET has an unexpected type"
ipset list "$CFPP_DESTINATION_V4_SET" | grep -Eq '^Header: family inet( |$)' || fail "ipset $CFPP_DESTINATION_V4_SET has an unexpected family"
next_sets_exist=0
cleanup_next_set() {
  if (( next_sets_exist == 1 )); then
    ipset destroy "$CFPP_TRUSTED_V4_NEXT" 2>/dev/null || true
    ipset destroy "$CFPP_DESTINATION_V4_NEXT" 2>/dev/null || true
  fi
}
trap cleanup_next_set EXIT
ipset create "$CFPP_TRUSTED_V4_NEXT" hash:net family inet
next_sets_exist=1
ipset create "$CFPP_DESTINATION_V4_NEXT" hash:ip family inet
cidr_count=0
while read -r cidr _; do
  [[ -n ${cidr:-} && $cidr != \#* ]] || continue
  [[ $cidr != *:* ]] || continue
  ipset add "$CFPP_TRUSTED_V4_NEXT" "$cidr"
  ((cidr_count += 1))
done <"$CFPP_TRUSTED_CIDRS_FILE"
(( cidr_count > 0 )) || fail "trusted CIDR file contains no IPv4 prefixes"

destination_count=0
origin_is_destination=0
while read -r address _; do
  [[ -n ${address:-} && $address != \#* ]] || continue
  [[ $address =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || fail "invalid IPv4 destination address: $address"
  ipset add "$CFPP_DESTINATION_V4_NEXT" "$address"
  if [[ $address == "$CFPP_INGRESS_ADDRESS" ]]; then
    origin_is_destination=1
  fi
  ((destination_count += 1))
done <"$CFPP_DESTINATION_ADDRESSES_FILE"
(( destination_count > 0 )) || fail "destination address file contains no IPv4 addresses"
(( origin_is_destination == 1 )) || fail "destination address file does not contain ingress address $CFPP_INGRESS_ADDRESS"

ipset swap "$CFPP_TRUSTED_V4_NEXT" "$CFPP_TRUSTED_V4_SET"
ipset swap "$CFPP_DESTINATION_V4_NEXT" "$CFPP_DESTINATION_V4_SET"
ipset destroy "$CFPP_TRUSTED_V4_NEXT"
ipset destroy "$CFPP_DESTINATION_V4_NEXT"
next_sets_exist=0

ensure_owned_chain() {
	local table=$1
	local chain=$2
	if iptables -t "$table" -S "$chain" >/dev/null 2>&1; then
		local foreign
		foreign=$(iptables -t "$table" -S "$chain" | foreign_chain_rules)
		[[ -z "$foreign" ]] || fail "chain $chain contains non-gateway rules"
	else
		iptables -t "$table" -N "$chain"
	fi
}

ensure_rule() {
	local table=$1
	local chain=$2
	shift 2
	if ! iptables -t "$table" -C "$chain" "$@" 2>/dev/null; then
		iptables -t "$table" -A "$chain" "$@"
	fi
}

place_first_rule() {
	local table=$1
	local chain=$2
	shift 2
	while iptables -t "$table" -C "$chain" "$@" 2>/dev/null; do
		iptables -t "$table" -D "$chain" "$@"
	done
	iptables -t "$table" -I "$chain" 1 "$@"
}

ensure_owned_chain mangle "$CFPP_PRE_CHAIN"
ensure_owned_chain mangle "$CFPP_OUT_CHAIN"
ensure_owned_chain nat "$CFPP_NAT_CHAIN"
ensure_owned_chain filter "$CFPP_INPUT_CHAIN"
iptables -t mangle -F "$CFPP_PRE_CHAIN"
iptables -t mangle -F "$CFPP_OUT_CHAIN"
iptables -t nat -F "$CFPP_NAT_CHAIN"
iptables -t filter -F "$CFPP_INPUT_CHAIN"
ensure_rule mangle "$CFPP_PRE_CHAIN" -p tcp -m connmark --mark "$CFPP_MARK_MASK" -m comment --comment "$CFPP_COMMENT_PREFIX:docker-reply-restore" -j CONNMARK --restore-mark
ensure_rule mangle "$CFPP_OUT_CHAIN" -p tcp -m mark --mark "$CFPP_MARK_MASK" -m comment --comment "$CFPP_COMMENT_PREFIX:docker-output-save" -j CONNMARK --save-mark
ensure_rule mangle "$CFPP_PRE_CHAIN" -p tcp -m mark --mark "$CFPP_MARK_MASK" -m comment --comment "$CFPP_COMMENT_PREFIX:save" -j CONNMARK --save-mark
ensure_rule mangle "$CFPP_OUT_CHAIN" -p tcp -m connmark --mark "$CFPP_MARK_MASK" -m comment --comment "$CFPP_COMMENT_PREFIX:restore" -j CONNMARK --restore-mark
ensure_rule mangle PREROUTING -m comment --comment "$CFPP_COMMENT_PREFIX:hook-pre" -j "$CFPP_PRE_CHAIN"
ensure_rule mangle OUTPUT -m comment --comment "$CFPP_COMMENT_PREFIX:hook-out" -j "$CFPP_OUT_CHAIN"
for interface in "${CFPP_INGRESS_DEVICES[@]}"; do
  ensure_rule nat "$CFPP_NAT_CHAIN" -i "$interface" -p tcp -m multiport --dports "$CFPP_CAPTURE_PORTS" -m set --match-set "$CFPP_TRUSTED_V4_SET" src -m set --match-set "$CFPP_DESTINATION_V4_SET" dst -m comment --comment "$CFPP_COMMENT_PREFIX:redirect" -j DNAT --to-destination "$CFPP_INGRESS_ADDRESS:$CFPP_INGRESS_PORT"
done
place_first_rule nat PREROUTING -m comment --comment "$CFPP_COMMENT_PREFIX:hook-nat" -j "$CFPP_NAT_CHAIN"
for interface in "${CFPP_INGRESS_DEVICES[@]}"; do
  ensure_rule filter "$CFPP_INPUT_CHAIN" -i "$interface" -d "$CFPP_INGRESS_CIDR" -p tcp --dport "$CFPP_INGRESS_PORT" -m set --match-set "$CFPP_TRUSTED_V4_SET" src -m comment --comment "$CFPP_COMMENT_PREFIX:trusted-input" -j ACCEPT
done
place_first_rule filter INPUT -m comment --comment "$CFPP_COMMENT_PREFIX:hook-input" -j "$CFPP_INPUT_CHAIN"

if ! rule_is_installed; then
  ip -4 rule add priority "$CFPP_RULE_PRIORITY" fwmark "$CFPP_MARK_MASK" lookup "$CFPP_ROUTE_TABLE"
fi
if ! route_is_installed; then
  ip -4 route add local 0.0.0.0/0 dev lo table "$CFPP_ROUTE_TABLE"
fi

if [[ "$current_route_localnet" != 1 ]]; then
  printf '1\n' >"$CFPP_ROUTE_LOCALNET_PATH"
fi

sync_docker_routes "$DOCKER_ROUTES"
save_docker_routes "$DOCKER_ROUTES"

"$SCRIPT_DIR/network-check"
