566 lines
19 KiB
PHP
566 lines
19 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
use PDO;
|
|
use Exception;
|
|
use RuntimeException;
|
|
use InvalidArgumentException;
|
|
|
|
class FirewallManager
|
|
{
|
|
public const JAIL_LOCAL_PATH = '/etc/fail2ban/jail.local';
|
|
public const NGINX_CONF_PATH = '/etc/nginx/sites-available/nexus-pbx';
|
|
public const WEB_FILTER_PATH = '/etc/fail2ban/filter.d/nexus-web.conf';
|
|
|
|
/**
|
|
* Récupère la configuration actuelle du pare-feu et de Fail2ban depuis SQLite.
|
|
*/
|
|
public static function getSettings(): array
|
|
{
|
|
$db = Database::getConnection();
|
|
$stmt = $db->query("SELECT
|
|
firewall_enabled, admin_https_port, certbot_strict_port80,
|
|
fail2ban_sip_maxretry, fail2ban_sip_findtime, fail2ban_sip_bantime,
|
|
fail2ban_web_maxretry, fail2ban_web_bantime, fail2ban_whitelist_ips
|
|
FROM settings WHERE id = 1");
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
return [
|
|
'firewall_enabled' => 1,
|
|
'admin_https_port' => 8443,
|
|
'certbot_strict_port80' => 0,
|
|
'fail2ban_sip_maxretry' => 3,
|
|
'fail2ban_sip_findtime' => 600,
|
|
'fail2ban_sip_bantime' => 3600,
|
|
'fail2ban_web_maxretry' => 3,
|
|
'fail2ban_web_bantime' => 3600,
|
|
'fail2ban_whitelist_ips' => '127.0.0.1/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 82.122.84.60',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'firewall_enabled' => (int)($row['firewall_enabled'] ?? 1),
|
|
'admin_https_port' => (int)($row['admin_https_port'] ?? 8443),
|
|
'certbot_strict_port80' => (int)($row['certbot_strict_port80'] ?? 0),
|
|
'fail2ban_sip_maxretry' => (int)($row['fail2ban_sip_maxretry'] ?? 3),
|
|
'fail2ban_sip_findtime' => (int)($row['fail2ban_sip_findtime'] ?? 600),
|
|
'fail2ban_sip_bantime' => (int)($row['fail2ban_sip_bantime'] ?? 3600),
|
|
'fail2ban_web_maxretry' => (int)($row['fail2ban_web_maxretry'] ?? 3),
|
|
'fail2ban_web_bantime' => (int)($row['fail2ban_web_bantime'] ?? 3600),
|
|
'fail2ban_whitelist_ips' => (string)($row['fail2ban_whitelist_ips'] ?? '127.0.0.1/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 82.122.84.60'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Retourne l'état complet en direct de Fail2ban et les IPs bannies.
|
|
* Utilise un cache rapide de 4 secondes pour soulager le tableau de bord temps réel.
|
|
*/
|
|
public static function getStatus(bool $forceFresh = false): array
|
|
{
|
|
$cacheFile = '/tmp/nexus_fw_status_cache.json';
|
|
if (!$forceFresh && file_exists($cacheFile)) {
|
|
$mtime = @filemtime($cacheFile);
|
|
if ($mtime && (time() - $mtime) < 4) {
|
|
$cached = json_decode((string)file_get_contents($cacheFile), true);
|
|
if (is_array($cached)) {
|
|
return $cached;
|
|
}
|
|
}
|
|
}
|
|
|
|
$settings = self::getSettings();
|
|
|
|
// 1. Détection service Fail2ban (Statut Systemd + vérification socket ping)
|
|
$sysActive = trim((string)@shell_exec('systemctl is-active fail2ban 2>/dev/null')) === 'active';
|
|
$pingCode = -1;
|
|
$pingOut = [];
|
|
if ($sysActive) {
|
|
exec('timeout 2 sudo -n /usr/bin/fail2ban-client ping 2>&1', $pingOut, $pingCode);
|
|
}
|
|
$pingOk = ($pingCode === 0 && str_contains(implode(' ', $pingOut), 'Server replied: pong'));
|
|
$isRunning = $sysActive || $pingOk;
|
|
|
|
$jails = [];
|
|
$bannedList = [];
|
|
$totalBannedCount = 0;
|
|
|
|
if ($isRunning && $pingOk) {
|
|
exec('timeout 2 sudo -n /usr/bin/fail2ban-client status 2>&1', $statOut);
|
|
$statStr = implode("\n", $statOut);
|
|
|
|
// Extraction des prisons actives
|
|
if (preg_match('/Jail list:\s+(.+)$/m', $statStr, $m)) {
|
|
$jailNames = array_map('trim', explode(',', $m[1]));
|
|
foreach ($jailNames as $jName) {
|
|
if ($jName === '') continue;
|
|
$jStatus = self::getJailDetails($jName);
|
|
$jails[$jName] = $jStatus;
|
|
|
|
foreach ($jStatus['banned_ips'] as $bIp) {
|
|
$bannedList[] = [
|
|
'ip' => $bIp,
|
|
'jail' => $jName,
|
|
'jail_label' => self::formatJailName($jName),
|
|
];
|
|
$totalBannedCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// IP actuelle du client
|
|
$currentIp = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
|
if (str_contains($currentIp, ',')) {
|
|
$currentIp = trim(explode(',', $currentIp)[0]);
|
|
}
|
|
|
|
$status = [
|
|
'is_running' => $isRunning,
|
|
'settings' => $settings,
|
|
'jails' => $jails,
|
|
'banned_ips' => $bannedList,
|
|
'total_banned' => $totalBannedCount,
|
|
'client_ip' => $currentIp,
|
|
];
|
|
|
|
@file_put_contents($cacheFile, json_encode($status));
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Débannit une adresse IP d'une prison ou de toutes les prisons.
|
|
*/
|
|
public static function unbanIp(string $ip, string $jail = ''): array
|
|
{
|
|
$ip = trim($ip);
|
|
if ($ip === '' || (!filter_var($ip, FILTER_VALIDATE_IP) && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
|
|
throw new InvalidArgumentException("Adresse IP invalide : {$ip}");
|
|
}
|
|
|
|
$jail = trim($jail);
|
|
$unbannedFrom = [];
|
|
|
|
if ($jail !== '' && $jail !== 'all') {
|
|
$cmd = "timeout 2 sudo -n /usr/bin/fail2ban-client set " . escapeshellarg($jail) . " unbanip " . escapeshellarg($ip) . " 2>&1";
|
|
exec($cmd, $out, $code);
|
|
if ($code === 0) {
|
|
$unbannedFrom[] = $jail;
|
|
}
|
|
} else {
|
|
// Débannir de toutes les prisons connues
|
|
foreach (['asterisk', 'nexus-web', 'sshd'] as $j) {
|
|
$cmd = "timeout 2 sudo -n /usr/bin/fail2ban-client set " . escapeshellarg($j) . " unbanip " . escapeshellarg($ip) . " 2>&1";
|
|
exec($cmd, $out, $code);
|
|
if ($code === 0) {
|
|
$unbannedFrom[] = $j;
|
|
}
|
|
}
|
|
}
|
|
|
|
@unlink('/tmp/nexus_fw_status_cache.json');
|
|
|
|
return [
|
|
'success' => count($unbannedFrom) > 0,
|
|
'message' => count($unbannedFrom) > 0
|
|
? "L'adresse IP {$ip} a été débannie avec succès (" . implode(', ', $unbannedFrom) . ")."
|
|
: "L'adresse IP {$ip} n'a pas été trouvée dans les listes de bannissement actives.",
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Bannit manuellement une adresse IP.
|
|
*/
|
|
public static function banIp(string $ip, string $jail = 'asterisk'): array
|
|
{
|
|
$ip = trim($ip);
|
|
if ($ip === '' || (!filter_var($ip, FILTER_VALIDATE_IP) && !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
|
|
throw new InvalidArgumentException("Adresse IP invalide : {$ip}");
|
|
}
|
|
|
|
$jail = trim($jail) ?: 'asterisk';
|
|
$cmd = "timeout 2 sudo -n /usr/bin/fail2ban-client set " . escapeshellarg($jail) . " banip " . escapeshellarg($ip) . " 2>&1";
|
|
exec($cmd, $out, $code);
|
|
@unlink('/tmp/nexus_fw_status_cache.json');
|
|
|
|
return [
|
|
'success' => $code === 0,
|
|
'message' => $code === 0
|
|
? "L'adresse IP {$ip} a été bannie sur la règle {$jail}."
|
|
: "Échec du bannissement : " . implode("\n", $out),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Enregistre les paramètres du pare-feu, régénère jail.local et la configuration Nginx.
|
|
*/
|
|
public static function saveSettings(array $data): array
|
|
{
|
|
$db = Database::getConnection();
|
|
|
|
$strictPort80 = !empty($data['certbot_strict_port80']) ? 1 : 0;
|
|
$firewallEnabled = isset($data['firewall_enabled']) ? (!empty($data['firewall_enabled']) ? 1 : 0) : 1;
|
|
$adminPort = max(1, min(65535, (int)($data['admin_https_port'] ?? 8443)));
|
|
$sipMaxRetry = max(1, min(20, (int)($data['fail2ban_sip_maxretry'] ?? 3)));
|
|
$sipFindTime = max(60, min(86400, (int)($data['fail2ban_sip_findtime'] ?? 600)));
|
|
$sipBanTime = max(60, min(31536000, (int)($data['fail2ban_sip_bantime'] ?? 3600)));
|
|
$webMaxRetry = max(1, min(20, (int)($data['fail2ban_web_maxretry'] ?? 3)));
|
|
$webBanTime = max(60, min(31536000, (int)($data['fail2ban_web_bantime'] ?? 3600)));
|
|
|
|
$whitelist = trim((string)($data['fail2ban_whitelist_ips'] ?? ''));
|
|
$cleanTokens = array_values(array_filter(preg_split('/\s+/', $whitelist)));
|
|
|
|
// Toujours préserver l'adresse de bouclage locale pour les processus internes
|
|
if (!in_array('127.0.0.1/8', $cleanTokens, true)) {
|
|
array_unshift($cleanTokens, '127.0.0.1/8');
|
|
}
|
|
if (!in_array('::1', $cleanTokens, true)) {
|
|
$cleanTokens[] = '::1';
|
|
}
|
|
|
|
$finalWhitelist = implode(' ', array_unique($cleanTokens));
|
|
|
|
// 1. Sauvegarde dans la base SQLite
|
|
$stmt = $db->prepare("UPDATE settings SET
|
|
firewall_enabled = :fe,
|
|
admin_https_port = :ap,
|
|
certbot_strict_port80 = :sp80,
|
|
fail2ban_sip_maxretry = :smr,
|
|
fail2ban_sip_findtime = :sft,
|
|
fail2ban_sip_bantime = :sbt,
|
|
fail2ban_web_maxretry = :wmr,
|
|
fail2ban_web_bantime = :wbt,
|
|
fail2ban_whitelist_ips = :wl,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = 1");
|
|
|
|
$stmt->execute([
|
|
':fe' => $firewallEnabled,
|
|
':ap' => $adminPort,
|
|
':sp80' => $strictPort80,
|
|
':smr' => $sipMaxRetry,
|
|
':sft' => $sipFindTime,
|
|
':sbt' => $sipBanTime,
|
|
':wmr' => $webMaxRetry,
|
|
':wbt' => $webBanTime,
|
|
':wl' => $finalWhitelist,
|
|
]);
|
|
|
|
// 2. Génération et application du fichier /etc/fail2ban/jail.local
|
|
self::generateJailLocal(
|
|
$finalWhitelist,
|
|
$sipMaxRetry,
|
|
$sipFindTime,
|
|
$sipBanTime,
|
|
$webMaxRetry,
|
|
$webBanTime,
|
|
$adminPort
|
|
);
|
|
|
|
// 3. Rechargement / Redémarrage propre de Fail2ban
|
|
exec('timeout 3 sudo -n /usr/bin/fail2ban-client reload 2>&1', $f2bOut, $f2bCode);
|
|
if ($f2bCode !== 0) {
|
|
exec('timeout 5 sudo -n /usr/bin/systemctl restart fail2ban 2>&1');
|
|
}
|
|
|
|
// 4. Mise à jour de la configuration Nginx
|
|
self::applyNginxConfig($adminPort, $strictPort80);
|
|
|
|
@unlink('/tmp/nexus_fw_status_cache.json');
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Configuration Pare-feu & Fail2ban appliquée avec succès ! Nginx et Fail2ban rechargés.',
|
|
'settings' => self::getSettings(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Écrit le fichier /etc/fail2ban/jail.local
|
|
*/
|
|
public static function generateJailLocal(
|
|
string $whitelist,
|
|
int $sipMaxRetry,
|
|
int $sipFindTime,
|
|
int $sipBanTime,
|
|
int $webMaxRetry,
|
|
int $webBanTime,
|
|
int $adminPort
|
|
): void {
|
|
$portsWeb = "http,https,{$adminPort}";
|
|
|
|
$conf = <<<EOF
|
|
# Fichier généré automatiquement par NexusPBX
|
|
# Ne pas modifier manuellement - éditable depuis l'interface Web
|
|
|
|
[DEFAULT]
|
|
ignoreip = {$whitelist}
|
|
findtime = {$sipFindTime}
|
|
bantime = {$sipBanTime}
|
|
maxretry = {$sipMaxRetry}
|
|
usedns = no
|
|
|
|
[sshd]
|
|
enabled = true
|
|
port = ssh
|
|
mode = aggressive
|
|
logpath = %(sshd_log)s
|
|
backend = %(sshd_backend)s
|
|
maxretry = 3
|
|
bantime = 3600
|
|
|
|
[asterisk]
|
|
enabled = true
|
|
filter = asterisk
|
|
logpath = /var/log/asterisk/messages
|
|
maxretry = {$sipMaxRetry}
|
|
findtime = {$sipFindTime}
|
|
bantime = {$sipBanTime}
|
|
port = 5060,5061
|
|
protocol = udp
|
|
|
|
[nexus-web]
|
|
enabled = true
|
|
filter = nexus-web
|
|
logpath = /var/log/nexus_security.log
|
|
maxretry = {$webMaxRetry}
|
|
findtime = 600
|
|
bantime = {$webBanTime}
|
|
port = {$portsWeb}
|
|
protocol = tcp
|
|
|
|
EOF;
|
|
|
|
file_put_contents(self::JAIL_LOCAL_PATH, $conf);
|
|
@chgrp(self::JAIL_LOCAL_PATH, 'asterisk');
|
|
@chmod(self::JAIL_LOCAL_PATH, 0664);
|
|
}
|
|
|
|
/**
|
|
* Met à jour Nginx pour séparer le port 80 (Certbot/ACME uniquement si strict) et écouter sur le port d'admin.
|
|
*/
|
|
public static function applyNginxConfig(int $adminHttpsPort, int $strictPort80): void
|
|
{
|
|
$phpSock = '/run/php/php8.4-fpm.sock';
|
|
if (!file_exists($phpSock)) {
|
|
$found = glob('/run/php/php*-fpm.sock');
|
|
if (!empty($found)) {
|
|
$phpSock = $found[0];
|
|
}
|
|
}
|
|
|
|
// Configuration Port 80 (HTTP)
|
|
if ($strictPort80 === 1) {
|
|
// Mode Strict : Le port 80 sert UNIQUEMENT aux challenges ACME Certbot
|
|
$port80Block = <<<EOF
|
|
server {
|
|
listen 80 default_server;
|
|
listen [::]:80 default_server;
|
|
server_name _;
|
|
|
|
# Challenge ACME Let's Encrypt (Certbot) UNIQUEMENT
|
|
location ^~ /.well-known/acme-challenge/ {
|
|
root /var/www/nexus-pbx/public;
|
|
try_files \$uri =404;
|
|
}
|
|
|
|
# Tout le reste est strictement bloqué sur le port 80
|
|
location / {
|
|
return 404;
|
|
}
|
|
}
|
|
EOF;
|
|
} else {
|
|
// Mode Normal : Port 80 sert l'application (et les challenges ACME)
|
|
$port80Block = <<<EOF
|
|
server {
|
|
listen 80 default_server;
|
|
listen [::]:80 default_server;
|
|
server_name _;
|
|
|
|
client_max_body_size 512M;
|
|
|
|
root /var/www/nexus-pbx/public;
|
|
index index.php;
|
|
|
|
access_log /var/log/nginx/nexus-pbx.access.log;
|
|
error_log /var/log/nginx/nexus-pbx.error.log;
|
|
|
|
location / {
|
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
|
}
|
|
|
|
location ~* \.(?:css|js)$ {
|
|
expires -1;
|
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
|
try_files \$uri =404;
|
|
}
|
|
|
|
location ~ \.php$ {
|
|
include snippets/fastcgi-php.conf;
|
|
fastcgi_pass unix:{$phpSock};
|
|
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
|
}
|
|
|
|
location ~ /\.(?!well-known).* {
|
|
deny all;
|
|
}
|
|
}
|
|
EOF;
|
|
}
|
|
|
|
// Configuration HTTPS : écoute sur port 443 ET sur le port admin custom si différent de 443
|
|
$listenCustom = "";
|
|
if ($adminHttpsPort !== 443 && $adminHttpsPort > 0) {
|
|
$listenCustom = "\n listen {$adminHttpsPort} ssl;\n listen [::]:{$adminHttpsPort} ssl;";
|
|
}
|
|
|
|
$httpsBlock = <<<EOF
|
|
server {
|
|
listen 443 ssl default_server;
|
|
listen [::]:443 ssl default_server;{$listenCustom}
|
|
server_name _;
|
|
|
|
ssl_certificate /etc/asterisk/keys/active_cert.crt;
|
|
ssl_certificate_key /etc/asterisk/keys/active_key.key;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
ssl_prefer_server_ciphers on;
|
|
|
|
client_max_body_size 512M;
|
|
|
|
root /var/www/nexus-pbx/public;
|
|
index index.php;
|
|
|
|
access_log /var/log/nginx/nexus-pbx.ssl.access.log;
|
|
error_log /var/log/nginx/nexus-pbx.ssl.error.log;
|
|
|
|
location / {
|
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
|
}
|
|
|
|
location ~* \.(?:css|js)$ {
|
|
expires -1;
|
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
|
try_files \$uri =404;
|
|
}
|
|
|
|
location ~ \.php$ {
|
|
include snippets/fastcgi-php.conf;
|
|
fastcgi_pass unix:{$phpSock};
|
|
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
|
}
|
|
|
|
location ~ /\.(?!well-known).* {
|
|
deny all;
|
|
}
|
|
}
|
|
EOF;
|
|
|
|
// Configuration Port 8080 : Annuaire XML pour téléphones IP (Yealink, Fanvil, Grandstream, etc.)
|
|
$phonebookBlock = <<<EOF
|
|
server {
|
|
listen 8080;
|
|
listen [::]:8080;
|
|
server_name _;
|
|
root /var/www/nexus-pbx/public;
|
|
|
|
# Annuaire XML pour postes IP (phonebook.php)
|
|
location = /phonebook.php {
|
|
include snippets/fastcgi-php.conf;
|
|
fastcgi_pass unix:{$phpSock};
|
|
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
|
}
|
|
|
|
# Avatars / Photos de contacts pour téléphones IP
|
|
location ^~ /avatars/ {
|
|
try_files \$uri =404;
|
|
}
|
|
|
|
location / {
|
|
return 404;
|
|
}
|
|
}
|
|
EOF;
|
|
|
|
$fullNginxConfig = $port80Block . "\n\n" . $phonebookBlock . "\n\n" . $httpsBlock . "\n";
|
|
file_put_contents(self::NGINX_CONF_PATH, $fullNginxConfig);
|
|
@chgrp(self::NGINX_CONF_PATH, 'asterisk');
|
|
@chmod(self::NGINX_CONF_PATH, 0664);
|
|
|
|
// Test de syntaxe
|
|
exec('sudo -n /usr/sbin/nginx -t 2>&1', $tOut, $tCode);
|
|
if ($tCode !== 0) {
|
|
throw new RuntimeException("Erreur de syntaxe Nginx : " . implode("\n", $tOut));
|
|
}
|
|
|
|
// Rechargement Nginx sans coupure
|
|
exec('sudo -n /usr/bin/systemctl reload nginx 2>&1');
|
|
}
|
|
|
|
private static function getJailDetails(string $jail): array
|
|
{
|
|
exec('timeout 2 sudo -n /usr/bin/fail2ban-client status ' . escapeshellarg($jail) . ' 2>&1', $out);
|
|
$str = implode("\n", $out);
|
|
|
|
$currentlyFailed = 0;
|
|
$totalFailed = 0;
|
|
$currentlyBanned = 0;
|
|
$totalBanned = 0;
|
|
$bannedIps = [];
|
|
|
|
if (preg_match('/Currently failed:\s+(\d+)/', $str, $m)) {
|
|
$currentlyFailed = (int)$m[1];
|
|
}
|
|
if (preg_match('/Total failed:\s+(\d+)/', $str, $m)) {
|
|
$totalFailed = (int)$m[1];
|
|
}
|
|
|
|
// Intégrer également les événements consignés dans le journal physique
|
|
if ($jail === 'asterisk') {
|
|
$logIncidents = is_readable('/var/log/asterisk/security.log')
|
|
? (int)shell_exec("grep -c -E 'ChallengeResponseFailed|InvalidAccountID|FailedAuth' /var/log/asterisk/security.log 2>/dev/null")
|
|
: 0;
|
|
$totalFailed = max($totalFailed, $logIncidents);
|
|
} elseif ($jail === 'nexus-web') {
|
|
$logIncidents = is_readable('/var/log/nexus_security.log')
|
|
? (int)shell_exec("grep -c 'FailedLogin' /var/log/nexus_security.log 2>/dev/null")
|
|
: 0;
|
|
$totalFailed = max($totalFailed, $logIncidents);
|
|
}
|
|
if (preg_match('/Currently banned:\s+(\d+)/', $str, $m)) {
|
|
$currentlyBanned = (int)$m[1];
|
|
}
|
|
if (preg_match('/Total banned:\s+(\d+)/', $str, $m)) {
|
|
$totalBanned = (int)$m[1];
|
|
}
|
|
if (preg_match('/Banned IP list:\s*(.*)$/m', $str, $m)) {
|
|
$rawIps = trim($m[1]);
|
|
if ($rawIps !== '') {
|
|
$bannedIps = array_values(array_filter(preg_split('/\s+/', $rawIps)));
|
|
}
|
|
}
|
|
|
|
return [
|
|
'name' => $jail,
|
|
'currently_failed' => $currentlyFailed,
|
|
'total_failed' => $totalFailed,
|
|
'currently_banned' => $currentlyBanned,
|
|
'total_banned' => $totalBanned,
|
|
'banned_ips' => $bannedIps,
|
|
];
|
|
}
|
|
|
|
private static function formatJailName(string $jail): string
|
|
{
|
|
return match ($jail) {
|
|
'asterisk' => 'Téléphonie SIP (PJSIP)',
|
|
'nexus-web' => 'Interface Web Admin',
|
|
'sshd' => 'Accès Console SSH',
|
|
default => ucfirst($jail),
|
|
};
|
|
}
|
|
}
|