#!/usr/bin/php
<?php

declare(strict_types=1);

$program = 'firewalld';
$version = '2025.02.15';

define('STATE_READY', 0);
define('STATE_RELOADING', 1);
define('STATE_STOPPING', 2);
define('TIMER_PRESET', 10);

if (!function_exists('sd_notify')) {
    function sd_notify(string $str): void
    {
        echo "{$str}\n";
    }
}

/**
 * Issue sd_notify to alert systemd of current daemon state
 *
 * @param int $state
 * @return void
 */
function notify_state(int $state): void
{
    switch ($state) {
        case STATE_READY:
            sd_notify('READY=1');
            break;
        case STATE_RELOADING:
            sd_notify('RELOADING=1');
            break;
        case STATE_STOPPING:
            sd_notify('STOPPING=1');
            break;
        default:
            break;
    }
}

/**
 * Issue sd_notify to alert systemd of current status
 *
 * @param mixed &$last
 * @param bool $status
 * @return void
 */
function notify_status(mixed &$last, bool $status): void
{
    if ($status) {
        if (is_null($last) || (is_bool($last) && !$last)) {
            sd_notify('STATUS=Firewall Up');
        }
    } else {
        if (is_null($last) || (is_bool($last) && $last)) {
            sd_notify('STATUS=Firewall Down');
        }
    }
    $last = $status;
}

/**
 * Runs systemctl with supplied verb for all services symlinked in
 * /etc/firewalld/service.d
 *
 * @param string $verb
 * @return void
 */
function servicectl(string $verb): void
{
    $allowed = ['start', 'stop', 'reload', 'restart'];
    if (!in_array($verb, $allowed, true)) {
        printf("Unknown or un-supported verb '%s'\n", $verb);
        return;
    }
    $glob = glob('/etc/firewalld/service.d/*.service');
    if ($glob === false || (is_array($glob) && count($glob) === 0)) {
        return;
    }
    foreach ($glob as $service) {
        $service = basename($service);
        $command = sprintf("systemctl %s %s", $verb, $service);
        echo $command."\n";
        @exec($command, $output, $retval);
        printf("Return value %d\n", $retval);
    }
}

/**
 * Run the external firewall iptables script
 *
 * @return bool
 */
function firewall_up(): bool
{
    printf("Bringing up firewall\n");
    $firewall_up_cmd = '/usr/lib/firewalld/iptables.sh';
    @exec($firewall_up_cmd, $output, $retval);
    printf("Return value %d\n", $retval);

    return ($retval === 0);
}

/**
 * Run the external firewall ipset script
 *
 * @return void
 */
function ipset(): void
{
    printf("Running ipset script\n");
    $ipset_cmd = '/usr/lib/firewalld/ipset.sh';
    @exec($ipset_cmd, $output, $retval);
    printf("Return value %d\n", $retval);
}

/**
 * Returns true if supplied ip address is local
 *
 * @param string $ip
 * @return bool
 */
function is_local_ip(string $ip): bool
{
    $local_addr = [
        [ip2long('10.0.0.0'), 24],
        [ip2long('127.0.0.0'), 24],
        [ip2long('169.254.0.0'), 16],
        [ip2long('172.16.0.0'), 20],
        [ip2long('192.168.0.0'), 16],
    ];

    // Check for invalid address
    if (strlen($ip) === 0) {
        return false;
    }
    $ip = ip2long($ip);
    if ($ip === false) {
        return false;
    }

    // Check for local address
    foreach ($local_addr as $l) {
        $mask = ~((1 << $l[1])-1);
        if (($ip & $mask) === $l[0]) {
            return true;
        }
    }

    return false;
}

/**
 * Return network interface information as associative array
 *
 * @return array
 */
function get_ifaces(): array
{
    $ifaces = net_get_interfaces();
    if ($ifaces === false) {
        printf("Failed to get network interfaces\n");
    }
    return (is_array($ifaces) ? $ifaces : []);
}

/**
 * Check and return ip address for provided interface
 *
 * @param string $iface
 * @param string $cur_addr
 * @param string $prev_addr
 * @return bool
 */
function check_ip_addr(array $ifaces, string $iface, string &$cur_addr, string $prev_addr): bool
{
    $cur_addr = '';
    if (strlen($iface) === 0) {
        return false;
    }

    if (!array_key_exists($iface, $ifaces)) {
        if ($prev_addr !== '') {
            printf("Interface '%s' does not exist\n", $iface);
        }
        return false;
    }
    if (array_key_exists('up', $ifaces[$iface]) && $ifaces[$iface]['up'] === false) {
        if ($prev_addr !== '') {
            printf("Interface '%s' is down\n", $iface);
        }
        return false;
    }
    if (isset($ifaces[$iface]['unicast'][1]['address'])) {
        $cur_addr = $ifaces[$iface]['unicast'][1]['address'];
    }

    // Check for empty response
    if (strlen($cur_addr) === 0) {
        if ($prev_addr !== '') {
            printf("Failed to detect address of interface '%s'\n", $iface);
        }
        return false;
    }

    // Check for invalid address
    if (!filter_var($cur_addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        if ($prev_addr !== '') {
            printf("Invalid address on interface '%s'\n", $iface);
        }
        $cur_addr = '';
        return false;
    }

    // Return valid address
    if ($prev_addr === '') {
        printf("Interface '%s' address '%s'\n", $iface, $cur_addr);
    }

    return true;
}

/**
 * Signal handler function
 *
 * @param int $signo
 * @param mixed $siginfo
 * @return void
 */
function sig_handler(int $signo, mixed $siginfo): void
{
    global $shutdown;
    global $reload;

    switch ($signo) {
    case SIGTERM:  // Initiate shutdown
        printf("Caught SIGTERM\n");
        $shutdown = true;
        break;
    case SIGINT:
        printf("Caught SIGINT\n");
        $shutdown = true;
        break;
    case SIGHUP:
        printf("Caught SIGHUP\n");
        $reload = true;
        break;
    default:  // All other signals
        break;
    }
}

// Command line only
if (php_sapi_name() !== 'cli') {
    fwrite(STDERR, "This script can only be ran from the command line\n");
    exit(1);
}

// Make sure we are running as root
if (posix_geteuid() !== 0) {
    fwrite(STDERR, "This script needs to be ran as root!\n");
    exit(1);
}

error_reporting(E_ALL);
set_time_limit(0);

// Check for another instance pid file
$pid_file = "/run/firewalld/{$program}.pid";
if (@file_exists($pid_file)) {
    fwrite(STDERR, "Another instance is running\n");
    exit(1);
}

// Create pid file
$pid_handle = @fopen($pid_file, 'w');
if ($pid_handle === false) {
    fwrite(STDERR, "Unable to create pid file\n");
    exit(1);
}
fwrite($pid_handle, posix_getpid()."\n");
fclose($pid_handle);
unset($pid_handle);

// Install signal handlers
$shutdown = false;
$reload = false;
pcntl_async_signals(true);
pcntl_signal(SIGTERM, 'sig_handler');
pcntl_signal(SIGINT, 'sig_handler');
pcntl_signal(SIGHUP, 'sig_handler');

// Startup message
printf("Starting %s version %s\n", $program, $version);
notify_state(STATE_READY);

// Main loop
notify_status($last, false);
$update = false;
$timer = 0;
$ifaces = [
    'red0' => ['ready' => false, 'cur_addr' => '', 'last_addr' => ''],
    'green0' => ['ready' => false, 'cur_addr' => '', 'last_addr' => ''],
];
$ignorelocal = (isset($_SERVER['IGNORELOCALIP']) && $_SERVER['IGNORELOCALIP'] === 'yes');

do {
    sleep(1);
    if (!$reload && $timer > 0) {
        $timer--;
        continue;
    }

    $_ifaces = get_ifaces();
    foreach ($ifaces as $iface => &$state) {
        $state['ready'] = check_ip_addr($_ifaces, $iface, $state['cur_addr'], $state['last_addr']);
        if ($state['cur_addr'] !== $state['last_addr']) {
            $update = true;
        }
        $state['last_addr'] = $state['cur_addr'];
    }

    if ($update || $reload) {
        if ($reload) {
            notify_state(STATE_RELOADING);
        }
        ipset();
        $ifup = ($ifaces['green0']['ready'] && $ifaces['red0']['ready']);
        notify_status($last, $ifup && firewall_up());
        if ($last && ($ignorelocal || !is_local_ip($ifaces['red0']['cur_addr']))) {
            servicectl('start');
        } else {
            servicectl('stop');
        }
        if ($reload) {
            notify_state(STATE_READY);
        }
        if ($ifup) {
            $timer = TIMER_PRESET;
        }
        $update = false;
        $reload = false;
    } else {
        $timer = TIMER_PRESET;
    }
} while (!$shutdown);

notify_state(STATE_STOPPING);
sd_notify('STATUS=Exiting');
printf("Closing log file and shutting down\n");
@unlink('/run/firewalld/firewall-up');
@unlink($pid_file);

exit(0);
