Files
fusionpbx/app/event_guard/resources/classes/event_guard_service.php
markjcrane 5a928b9082 Event Guard for now default to iptables for Linux
- Can change this value using Default Settings
  - Category: system
  - Subcategory: firewall_name
  - Type: text
  - Value: nftables
  - Enabled: true
- Description: Firewall Options: pf, nftables, iptables

Additional firewalls will be supported in the future.
2026-01-29 06:13:04 -07:00

518 lines
15 KiB
PHP

<?php
/**
* Description goes here for event_guard service
*/
class event_guard_service extends service {
/**
* database object
* @var database
*/
private $database;
/**
* settings object
* @var settings
*/
private $settings;
/**
* hostname variable
* @var string
*/
private $hostname;
/**
* firewall object
* @var event_guard_interface
*/
private $firewall;
/**
* socket object
* @var event_socket
*/
private $socket;
/**
* Reloads settings from database, config file and websocket server.
*
* @return void
*/
protected function reload_settings(): void {
// Re-read the config file to get any possible changes
parent::$config->read();
// Connect to the database
$this->database = new database(['config' => parent::$config]);
// Get the settings using global defaults
$this->settings = new settings(['database' => $this->database]);
// Set the php operating system
$php_os = strtolower(PHP_OS);
// Set the firewall name
if ($php_os == 'freebsd') {
$firewall_name = $this->settings->get('system','firewall_name', 'pf');
}
if ($php_os == 'linux') {
$firewall_name = $this->settings->get('system','firewall_name', 'iptables');
}
if (empty($firewall_name)) {
throw new Exception("No firewall name specified in settings");
}
// Get the settings using global defaults
$class_name = 'event_guard_'.$firewall_name;
$this->firewall = new $class_name($this->settings);
if (!($this->firewall instanceof event_guard_interface)) {
throw new Exception("Must be an event_guard_interface firewall");
}
// Get the hostname
$this->hostname = gethostname();
// Connect to event socket
$this->socket = new event_socket;
if ($this->socket->connect()) {
// Loop through the switch events
$cmd = "event json ALL";
$result = $this->socket->request($cmd);
$this->debug('subscribe to ALL events '. print_r($result, true));
// Filter for specific events
$cmd = "filter Event-Name CUSTOM";
$result = $this->socket->request($cmd);
$this->debug('subscribe to CUSTOM events '. print_r($result, true));
}
else {
$this->warning('Unable to connect to event socket');
}
}
public function run(): int {
// Reload the settings
$this->reload_settings();
// Service work is handled here
while ($this->running) {
// Initialize the array for switch events
$json_array = [];
// Make sure the database connection is available
while (!$this->database->is_connected()) {
// Connect to the database
$this->database->connect();
// Reload settings after connection to the database
$this->settings = new settings(['database' => $this->database]);
// Sleep for a moment
sleep(1);
}
// Reconnect to event socket
if (!$this->socket->connected()) {
$this->warning('Not connected to even socket');
if ($this->socket->connect()) {
$cmd = "event json ALL";
$result = $this->socket->request($cmd);
$this->debug('subscribe to ALL events '. print_r($result, true));
$cmd = "filter Event-Name CUSTOM";
$result = $this->socket->request($cmd);
$this->debug('subscribe to CUSTOM events '. print_r($result, true));
$this->info('Re-connected to event socket');
}
else {
// Unable to connect to event socket
$this->warning('Unable to connect to event socket');
// Sleep and then attempt to reconnect
sleep(1);
continue;
}
}
// Read the socket
$json_response = $this->socket->read_event();
// Decode the response
if (isset($json_response) && $json_response != '') {
$json_array = json_decode($json_response['$'], true);
unset($json_response);
}
// Debug the event array
$this->debug('Event array '. print_r($json_array, true));
// Registration failed - block IP address unless they are registered
if (is_array($json_array) && $json_array['Event-Subclass'] == 'sofia::register_failure') {
//not registered so block the address
if (!$this->allow_access($json_array['network-ip'])) {
$this->block_add($json_array['network-ip'], 'sip-auth-fail', $json_array);
}
}
// Sendevent CUSTOM event_guard:unblock
if (is_array($json_array) && $json_array['Event-Subclass'] == 'event_guard:unblock') {
//check the database for pending requests
$sql = "select event_guard_log_uuid, log_date, filter, ip_address, extension, user_agent ";
$sql .= "from v_event_guard_logs ";
$sql .= "where log_status = 'pending' ";
$sql .= "and hostname = :hostname ";
//$this->debug($sql." ".$this->hostname);
$parameters['hostname'] = $this->hostname;
$event_guard_logs = $this->database->select($sql, $parameters, 'all');
if (is_array($event_guard_logs)) {
$x = 0;
foreach($event_guard_logs as $row) {
//unblock the ip address
$this->block_delete($row['ip_address'], $row['filter']);
//debug info
$this->info("unblocked: [ip_address: ".$row['ip_address'].", filter: ".$row['filter'].", to-user: ".$row['extension'].", to-host: ".$row['hostname'].", line: ".__line__);
//log the blocked ip address to the database
$array['event_guard_logs'][$x]['event_guard_log_uuid'] = $row['event_guard_log_uuid'];
$array['event_guard_logs'][$x]['log_date'] = 'now()';
$array['event_guard_logs'][$x]['log_status'] = 'unblocked';
$x++;
}
if (is_array($array)) {
$p = permissions::new();
$p->add('event_guard_log_edit', 'temp');
$this->database->save($array, false);
$p->delete('event_guard_log_edit', 'temp');
unset($array);
}
}
}
// Registration to the IP address
if (is_array($json_array) && $json_array['Event-Subclass'] == 'sofia::pre_register') {
if (isset($json_array['to-host'])) {
$is_valid_ip = filter_var($json_array['to-host'], FILTER_VALIDATE_IP);
if ($is_valid_ip) {
//if not registered block the address
if (!$this->allow_access($json_array['network-ip'])) {
$this->block_add($json_array['network-ip'], 'sip-auth-ip', $json_array);
}
//debug info
$this->debug("network-ip ".$json_array['network-ip'].", to-host ".$json_array['to-host']);
}
}
}
// Debug information
//if (($json_array['Event-Subclass'] == 'sofia::register_failure' || $json_array['Event-Subclass'] == 'sofia::pre_register')) {
//echo "\n";
//print_r($json_array);
//echo "event_name: ".$json_array['Event-Name']."\n";
//echo "event_type: ".$json_array['event_type']."\n";
//echo "event_subclass: ".$json_array['Event-Subclass']."\n";
//echo "status: ".$json_array['status']."\n";
//echo "network_ip: ".$json_array['network-ip']."\n";
//echo "channel_state: ".$json_array['Channel-State']."\n";
//echo "channel_call_state: ".$json_array['Channel-Call-State']."\n";
//echo "call_direction: ".$json_array['Call-Direction']."\n";
//echo "channel_call_uuid: ".$json_array['Channel-Call-UUID']."\n";
//echo "answer_state: ".$json_array['Answer-State']."\n";
//echo "hangup_cause: ".$json_array['Hangup-Cause']."\n";
//echo "to-host: $json_array['to-host']\n";
//echo "\n";
//}
// Sleep for 100 ms
usleep(100000);
}
return 0;
}
protected static function display_version(): void {
echo "1.1\n";
}
protected static function set_command_options() {
}
/**
* Execute a block command for nftables, iptables or pf based on the firewall type.
*
* @param string $ip_address The IP address to block
* @param string $filter The filter name for nftables, iptables or pf
* @param array $event The event data containing 'to-user' and 'to-host'
*
* @return boolean True if the block command was executed successfully, false otherwise
*/
public function block_add(string $ip_address, string $filter, array $event) : bool {
//invalid ip address
if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
return false;
}
//block the IP address
$result = $this->firewall->block_add($ip_address, $filter);
if ($result) {
//log the blocked ip address to the log
$this->warning("blocked: [ip_address: ".$ip_address.", filter: ".$filter.", to-user: ".$event['to-user'].", to-host: ".$event['to-host'].", line: ".__line__."]");
//log the blocked ip address to the database
$array = [];
$array['event_guard_logs'][0]['event_guard_log_uuid'] = uuid();
$array['event_guard_logs'][0]['hostname'] = gethostname();
$array['event_guard_logs'][0]['log_date'] = 'now()';
$array['event_guard_logs'][0]['filter'] = $filter;
$array['event_guard_logs'][0]['ip_address'] = $ip_address;
$array['event_guard_logs'][0]['extension'] = $event['to-user'].'@'.$event['to-host'];
$array['event_guard_logs'][0]['user_agent'] = $event['user-agent'];
$array['event_guard_logs'][0]['log_status'] = 'blocked';
$p = permissions::new();
$p->add('event_guard_log_add', 'temp');
$this->database->save($array, false);
$p->delete('event_guard_log_add', 'temp');
//send debug information to the console
$this->info("blocked address " . $ip_address . ", line " . __line__);
}
//return the result
return $result;
}
public function block_delete(string $ip_address, string $filter) : bool {
//invalid ip address
if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
return false;
}
//unblock the IP address
$result = $this->firewall->block_delete($ip_address, $filter);
if ($result) {
//send debug information to the console
$this->info("Unblock address " . $ip_address . ", line " . __line__);
}
//return the result
return $result;
}
public function block_exists(string $ip_address, string $filter) : bool {
//invalid ip address
if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
return false;
}
//check if the address is blocked
$result = $this->firewall->block_exists($ip_address, $filter);
//send debug information to the console
$this->debug("Address Exists " . $ip_address . ", line " . __line__);
//return the result
return $result;
}
/**
* Determine if access is allowed for a given IP address.
*
* This method checks the IP address is inside the cache, user logs, event guard logs, access controls,
* and registration to determine if access should be allowed. If the IP address is found
* in the access control list, user logs with result success, or valid registrations
* is found then the address is automatically allowed.
*
* @param string $ip_address The IP address to check for access.
*
* @return boolean True if access is allowed, false otherwise.
*/
private function allow_access($ip_address) {
//invalid ip address
if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
return false;
}
//check the cache to see if the address is allowed
$cache = new cache;
if ($cache->get("switch:allowed:".$ip_address) === 'true') {
//debug info
$this->debug("address: ".$ip_address." allowed by: cache");
//return boolean true
return true;
}
//allow access for addresses with authentication status success
if ($this->allow_user_log_success($ip_address)) {
//save address to the cache as allowed
$cache->set("switch:allowed:".$ip_address, 'true');
//debug info
$this->debug("address: ".$ip_address." allowed by: user logs");
//return boolean true
return true;
}
//allow access for addresses that have been unblocked
/*
if (event_guard_log_allowed($ip_address)) {
//save address to the cache as allowed
$cache->set("switch:allowed:".$ip_address, 'true');
//debug info
$this->debug("address: ".$ip_address." allowed by: unblocked");
//return boolean true
return true;
}
*/
//allow access if the cidr address is allowed
if ($this->allow_access_control($ip_address)) {
//save address to the cache as allowed
$cache->set("switch:allowed:".$ip_address, 'true');
//debug info
$this->debug("address: ".$ip_address." allowed by: access controls");
//return boolean true
return true;
}
//allow if there is a registration from the same IP address
if ($this->allow_registered($ip_address)) {
//save address to the cache as allowed
$cache->set("switch:allowed:".$ip_address, 'true');
//debug info
$this->debug("address: ".$ip_address." allowed by: registration");
//return boolean true
return true;
}
//return
return false;
}
/**
* Checks if the given IP address is authorized by any access control node.
*
* @param string $ip_address The IP address to check for authorization.
*
* @return bool True if the IP address is authorized, false otherwise.
*/
private function allow_access_control($ip_address) {
//invalid ip address
if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
return false;
}
//get the access control allowed nodes
$sql = "select access_control_node_uuid, access_control_uuid, node_cidr, node_description ";
$sql .= "from v_access_control_nodes ";
$sql .= "where node_type = 'allow' ";
$sql .= "and length(node_cidr) > 0 ";
$parameters = null;
$allowed_nodes = $this->database->select($sql, $parameters, 'all');
//default authorized to false
$allowed = false;
//use the ip address to get the authorized nodes
if (is_array($allowed_nodes)) {
foreach($allowed_nodes as $row) {
if (check_cidr($row['node_cidr'], $ip_address)) {
//debug info
// print_r($row);
// $this->debug("Authorized: ".$ip_address);
//set the allowed to true
$allowed = true;
//exit the loop
break;
}
}
}
//return
return $allowed;
}
/**
* Determines if a user's IP address is allowed based on their login history.
*
* @param string $ip_address The IP address to check for access.
*
* @return bool True if the IP address is allowed, false otherwise.
*/
private function allow_user_log_success($ip_address) {
//invalid ip address
if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
return false;
}
//check to see if the address was authenticated successfully
$sql = "select count(user_log_uuid) ";
$sql .= "from v_user_logs ";
$sql .= "where remote_address = :remote_address ";
$sql .= "and result = 'success' ";
$sql .= "and timestamp > NOW() - INTERVAL '8 days' ";
$parameters['remote_address'] = $ip_address;
$user_log_count = $this->database->select($sql, $parameters, 'column');
//debug info
$this->debug("address ".$ip_address." count ".$user_log_count);
//default authorized to false
$allowed = false;
//use the ip address to get the authorized nodes
if (!empty($user_log_count) && $user_log_count > 0) {
$allowed = true;
}
//return
return $allowed;
}
/**
* Checks if the given IP address is registered on the network.
*
* @param string $ip_address The IP address to check for registration.
*
* @return bool True if the IP address is registered, false otherwise.
*/
private function allow_registered($ip_address) {
//invalid ip address
if (!filter_var($ip_address, FILTER_VALIDATE_IP)) {
return false;
}
$registered = false;
$command = "fs_cli -x 'show registrations as json' ";
$result = shell_exec($command);
$array = json_decode($result, true);
if (is_array($array['rows'])) {
foreach ($array['rows'] as $row) {
if ($row['network_ip'] == $ip_address) {
$registered = true;
}
}
}
//return registered boolean
return $registered;
}
}