#!/bin/bash

# install or uninstall options to the script.
OPTION=$1

FIREWALLCMD="firewall-cmd --quiet"
FIREWALLCMD_DIRECT="${FIREWALLCMD} --direct"
IPTABLES=iptables

aembit_agent_proxy_user_name=aembit_agent_proxy
aembit_group_id=$(id --group "${aembit_agent_proxy_user_name}")
agent_proxy_port=38080
agent_dns_port=8053
container_cidr=$AEMBIT_DOCKER_CONTAINER_CIDR
# https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16
azure_platform_resources_ip="168.63.129.16"

# Rule Chains
aembit_raw_PREROUTING=aembit_raw_PREROUTING
aembit_raw_OUTPUT=aembit_raw_OUTPUT
aembit_nat_OUTPUT=aembit_nat_OUTPUT
aembit_nat_PREROUTING=aembit_nat_PREROUTING
aembit_filter_INPUT=aembit_filter_INPUT

# Create all aembit rule chains.
create_all_aembit_chains() {
    add_ipv4_chain raw "${aembit_raw_PREROUTING}"
    add_ipv4_chain raw "${aembit_raw_OUTPUT}"
    add_ipv4_chain nat "${aembit_nat_PREROUTING}"
    add_ipv4_chain nat "${aembit_nat_OUTPUT}"
    add_ipv4_chain filter "${aembit_filter_INPUT}"
}

# Link all aembit rule chains to the corresponding tables
link_all_aembit_chains() {
    add_ipv4_rule raw PREROUTING -j "${aembit_raw_PREROUTING}"
    add_ipv4_rule raw OUTPUT -j "${aembit_raw_OUTPUT}"
    add_ipv4_rule nat PREROUTING -j "${aembit_nat_PREROUTING}"
    add_ipv4_rule nat OUTPUT -j "${aembit_nat_OUTPUT}"
    add_ipv4_rule filter INPUT -j "${aembit_filter_INPUT}"
}

# Remove the rule to link aembit rule chains and delete them.
unlink_and_delete_aembit_chains() {
    remove_ipv4_rule raw PREROUTING -j "${aembit_raw_PREROUTING}"
    remove_ipv4_chain raw "${aembit_raw_PREROUTING}"

    remove_ipv4_rule raw OUTPUT -j "${aembit_raw_OUTPUT}"
    remove_ipv4_chain raw "${aembit_raw_OUTPUT}"

    remove_ipv4_rule nat PREROUTING -j "${aembit_nat_PREROUTING}"
    remove_ipv4_chain nat "${aembit_nat_PREROUTING}"

    remove_ipv4_rule nat OUTPUT -j "${aembit_nat_OUTPUT}"
    remove_ipv4_chain nat "${aembit_nat_OUTPUT}"

    remove_ipv4_rule filter INPUT -j "${aembit_filter_INPUT}"
    remove_ipv4_chain filter "${aembit_filter_INPUT}"

    if firewalld_is_active_and_running; then
        # Remove firewalld specific special rich rule for accepting untracked dns responses
        fwd_remove_rich_rule source-port port=53 protocol=udp accept
    fi
}

# check if firewalld is active and running
firewalld_is_active_and_running() {
    if [ -n "${firewalld_status+x}" ]; then
	    return "${firewalld_status}"
    fi

    # Check if firewalld is installed
    if ! command -v firewalld &> /dev/null; then
	    echo "firewalld not installed."
	    firewalld_status=1
	    return "${firewalld_status}"
    fi

    # Check if firewalld is active and running
    if systemctl is-active --quiet firewalld && systemctl is-enabled --quiet firewalld; then
            echo "firewalld is active and running."
	    firewalld_status=0
	    return "${firewalld_status}"
    else
            echo "firewalld is either not active or not running."
	    firewalld_status=1
	    return "${firewalld_status}"
    fi
}

# Add ipv4 chain using firewalld direct
# args: table, chain name
function fwd_direct_add_ipv4_chain() {
    local table=$1
    local chain=$2
    ${FIREWALLCMD_DIRECT} --add-chain ipv4 "${table}" "${chain}"
}

# Add ipv4 rule using firewalld direct
# args: table, chain name, iptable rule
function fwd_direct_add_ipv4_rule() {
    local table=$1
    local chain=$2
    shift
    shift
    ${FIREWALLCMD_DIRECT} --add-rule ipv4 "${table}" "${chain}" 0 "$@"
}

# Remove ipv4 rule using firewalld direct
# args: table, chain name, iptable rule
function fwd_direct_remove_ipv4_rule() {
    local table=$1
    local chain=$2
    shift
    shift
    ${FIREWALLCMD_DIRECT} --remove-rule ipv4 "${table}" "${chain}" 0 "$@"
}

# Remove ipv4 chain using firewalld direct
# args: table, chain name
function fwd_direct_remove_ipv4_chain() {
    local table=$1
    local chain=$2
    ${FIREWALLCMD_DIRECT} --remove-rules ipv4 "${table}" "${chain}"
    ${FIREWALLCMD_DIRECT} --remove-chain ipv4 "${table}" "${chain}"
}

# Add firewalld rich rule
function fwd_add_rich_rule() {
    rule="rule $*"
    ${FIREWALLCMD} --add-rich-rule="${rule}"
}

# Remove firewalld rich rule
function fwd_remove_rich_rule() {
    rule="rule $*"
    ${FIREWALLCMD} --remove-rich-rule="${rule}"
}

# Iptables - Add ipv4 chain
# args: table, chain name
function iptables_add_ipv4_chain() {
    local table=$1
    local chain=$2
    ${IPTABLES} -t "${table}" -N "${chain}"
}

# Add ipv4 rule using firewalld direct
# args: table, chain name, iptable rule
function iptables_add_ipv4_rule() {
    local table=$1
    local chain=$2
    shift
    shift
    ${IPTABLES} -t "${table}" -A "${chain}" "$@"
}

# Remove ipv4 rule using firewalld direct
# args: table, chain name, iptable rule
function iptables_remove_ipv4_rule() {
    local table=$1
    local chain=$2
    shift
    shift
    ${IPTABLES} -t "${table}" -D "${chain}" "$@"
}

# Remove ipv4 chain using firewalld direct
# args: table, chain name
function iptables_remove_ipv4_chain() {
    local table=$1
    local chain=$2
    # Delete all rules
    ${IPTABLES} -t "${table}" -F "${chain}"
    # Delete the chain
    ${IPTABLES} -t "${table}" -X "${chain}"
}

function add_ipv4_chain() {
    if firewalld_is_active_and_running; then
        fwd_direct_add_ipv4_chain "$@"
    else
        iptables_add_ipv4_chain "$@"
    fi
}

function remove_ipv4_chain() {
    if firewalld_is_active_and_running; then
        fwd_direct_remove_ipv4_chain "$@"
    else
        iptables_remove_ipv4_chain "$@"
    fi
}

function add_ipv4_rule() {
    if firewalld_is_active_and_running; then
        fwd_direct_add_ipv4_rule "$@"
    else
        iptables_add_ipv4_rule "$@"
    fi
}

function remove_ipv4_rule() {
    if firewalld_is_active_and_running; then
        fwd_direct_remove_ipv4_rule "$@"
    else
        iptables_remove_ipv4_rule "$@"
    fi
}

function save_and_reload_daemon() {
    if firewalld_is_active_and_running; then
	    # save firewalld rules and reload the daemon
	    ${FIREWALLCMD} --runtime-to-permanent
	    ${FIREWALLCMD} --reload
    fi
}

function dns_proxy_rules() {
    # We encountered a bug where egress traffic from the Agent Proxy's DNS proxy will
    # be routed back to itself. This rule prevents this bug.
    # This was happening for the following reasons:
    #
    # 1. The Client Workload and the Agent Proxy operate on shared networking infrastructure.
    #    This means that they have the same IP address.
    # 2. When the Client Workload initially connects, the DNS NAT rule in this file would
    #    overwrite the destination IP and port to point at the Agent Proxy.
    # 3. After an iptables rule is evaluated, it is saved as a "connection" by conntrack.
    #    It is not evaluated again, unless the connection in conntrack expires.
    #    This means that the connection is saved in conntrack as:
    #        - Original direction: <CW/AP IP>:<EPHEMERAL PORT X> -> <EXTERNAL DNS IP>:53
    #        - Reply direction: 127.0.0.1:8053 -> <CW/AP IP>:<EPHEMERAL PORT X>
    # 4. Since the Agent Proxy requests an ephemeral port on the host to handle the
    #    outgoing DNS request, sometimes it is assigned a port that was previously
    #    used and released by the Client Workload. However, since the NAT rule was already
    #    evaluated, the mapping from step #3 was already saved, meaning that the outbound
    #    DNS is sent to the Agent Proxy, forming a loop.
    #
    # To avoid this loop, in this line we set the NOTRACK target on outgoing DNS packets on
    # the "raw" table, which is evaluated prior to conntrack. This causes conntrack to not
    # try to associate the packet with any connection, even if one already exists.
    #
    # See https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html#STATEMACHINE
    # for more information on conntrack.
    add_ipv4_rule raw "${aembit_raw_OUTPUT}" -p udp --dport 53 -m owner --gid-owner "${aembit_group_id}" -j CT --notrack

    # The story for this rule is extremely similar to the above (only occurring in the VM environment).
    #
    # 1. CW used an ephemeral port to communicate with ResolveD (running on 127.0.0.53:53), and it's added to conntrack
    # 2. CW releases this port, and it becomes immediately available
    # 3. AP was accidentally given this port by the system when it tried to do DNS resolution as part of connecting to
    # a server workload.
    # 4. AP request to 127.0.0.53:53 is NOTRACK (per the rule above)
    # 5. ResolveD (127.0.0.53:53) response to AP hits a match in conntrack and NAT (for some reason, decides to modify
    # resolved port), and this packet is unreachable.
    #
    # As a result, we are adding this rule to make sure that traffic from ResolveD is not NATed.
    # TODO: move it along with systemd-resolved rule few lines below.
    add_ipv4_rule raw "${aembit_raw_OUTPUT}" -p udp -s 127.0.0.53 --sport 53 -j CT --notrack

    # Added due to a bug in RHEL-8.6 which causes these packets to be source NAT'ed as soon as any NAT rule is confiured.
    add_ipv4_rule raw "${aembit_raw_PREROUTING}" -p udp --sport 53 -j CT --notrack

    # firewalld by default disallows packets for a connection that is untracked in its filter INPUT chain.
    # Trace log for such rejects (see the last rule in filter_INPUT chain below) -
    #   "trace id c124d79a inet firewalld filter_INPUT rule reject with icmpx type admin-prohibited (verdict drop)"
    # Snippet of the default filter_INPUT rules.
    # chain filter_INPUT {
    #                type filter hook input priority filter + 10; policy accept;
    #                ct state { established, related } accept
    #                ct status dnat accept
    #                iifname "lo" accept
    #                jump filter_INPUT_POLICIES_pre
    #                jump filter_INPUT_ZONES_SOURCE
    #                jump filter_INPUT_ZONES
    #                jump filter_INPUT_POLICIES_post
    #                ct state { invalid } drop
    #                reject with icmpx type admin-prohibited
    # }
    # This would add the rule to `filter_IN_public_allow` chain which is called from filter_INPUT through filter_INPUT_ZONES
    # iptables equivalent rule would be `iptables -t filter -A INPUT -p udp --sport 53 -m conntrack --ctstate UNTRACKED -j ACCEPT`
    if firewalld_is_active_and_running; then
        fwd_add_rich_rule source-port port=53 protocol=udp accept
    fi

    # Ignore outbound traffic from systemd-resolved.
    # TODO: In case systemd-resolved received DNS request via DBus-API, this request should not be ignored.
    resolve_d_user_id=$(id --user systemd-resolve);
    if [ -n "${resolve_d_user_id}" ]; then
        add_ipv4_rule nat "${aembit_nat_OUTPUT}" -p udp --dport 53 -m owner --uid-owner "${resolve_d_user_id}" -j RETURN
    fi

    # REDIRECT DNS to Agent Proxy's DNS port.
    add_ipv4_rule nat "${aembit_nat_OUTPUT}" -p udp --dport 53 -j REDIRECT --to "${agent_dns_port}"
}

# Filter rules to allow TCP traffic to the AP.
function allow_tcp_traffic_to_ap() {
    # Allow local traffic to be sent to the Agent Proxy traffic port over loopback.
    add_ipv4_rule filter "${aembit_filter_INPUT}" -p tcp --dport "${agent_proxy_port}" -i lo -j ACCEPT

    if [ -n "$container_cidr" ]; then
        # Allow traffic from local containers to be sent to the Agent proxy traffic port.
        add_ipv4_rule filter "${aembit_filter_INPUT}" -p tcp -s  "${container_cidr}" --dport "${agent_proxy_port}" -j ACCEPT
    fi
}

# Default filter rule to block external traffic to the AP.
function default_block_tcp_traffic_to_ap() {
    add_ipv4_rule filter "${aembit_filter_INPUT}" -p tcp --dport "${agent_proxy_port}" -j REJECT
}

# Redirect outbound TCP traffic to the AP.
function redirect_outbound_tcp_traffic_to_ap() {
    # Rules to ignore outbound traffic rules to avoid redirectig them to the AP.

    # Ignore traffic from AP.
    add_ipv4_rule nat "${aembit_nat_OUTPUT}" -m owner --gid-owner "${aembit_group_id}" --suppl-groups -j RETURN

    # Ignore TCP loopback.
    add_ipv4_rule nat "${aembit_nat_OUTPUT}" -p tcp -o lo -j RETURN

    # Ignore outbound traffic to the Azure communication channel.
    # See https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16.
    add_ipv4_rule nat "${aembit_nat_OUTPUT}" -p tcp --destination "${azure_platform_resources_ip}" -j RETURN

    # Redirect all new TCP connections to the Agent Proxy.
    # We use the "--syn" flag to forward new connections only, since,
    # prior to adding NAT rules, conntrack is not loaded. Once we add NAT rules,
    # conntrack is loaded, but mistakenly identifies existing connections as new
    # connections, redirecting them erroneously to the Agent Proxy mid-connection.
    add_ipv4_rule nat "${aembit_nat_OUTPUT}" -p tcp --syn -j REDIRECT --to-port "${agent_proxy_port}"
}

# Redirect TCP traffic from containers to the AP.
function redirect_container_traffic_to_ap() {
    # REDIRECT tcp traffic from container CIDR to the AP
    add_ipv4_rule nat "${aembit_nat_PREROUTING}" -p tcp --syn -s "${container_cidr}" -j REDIRECT --to-port "${agent_proxy_port}"
}

# TCP related rules.
function tcp_proxy_rules() {
    #
    # filter rules
    #
    allow_tcp_traffic_to_ap

    # Disallow the rest of the traffic (coming from external interfaces) to be sent to the Agent Proxy traffic port.
    default_block_tcp_traffic_to_ap

    # Rules to redirect traffic to AP.

    # Redirect outbound TCP traffic to AP.
    redirect_outbound_tcp_traffic_to_ap

    # Redirect pre-routing TCP traffic from local containers.
    if [ -n "$container_cidr" ]; then
        redirect_container_traffic_to_ap
    fi
}

# Install chain rules for steering traffic to the Agent Proxy.
# These function wrappers would in turn add the iptables or firewalld specific rules
# based upon whether firewalld is active and running.
function install_traffic_steering_rules_to_ap() {
    create_all_aembit_chains

    dns_proxy_rules

    tcp_proxy_rules

    link_all_aembit_chains

    save_and_reload_daemon
}

# Uninstall chain rules for steering traffic to the Agent Proxy.
# These function wrappers would in turn remove the iptables or firewalld specific rules
# based upon whether firewalld is active and running.
function uninstall_traffic_steering_rules_to_ap() {
    unlink_and_delete_aembit_chains
    save_and_reload_daemon
}

case "${OPTION}" in
    install)
        echo "Installing traffic steering rules to the Agent Proxy."
        install_traffic_steering_rules_to_ap
        ;;
    uninstall)
        echo "Uninstalling traffic steering rules."
        uninstall_traffic_steering_rules_to_ap
        ;;
    *)
        echo "Invalid option - ${OPTION}"
        ;;
esac

