Passed
Push — develop ( 5bdc5b...331e75 )
by Портнов
04:29
created

Fail2BanConf::generateConf()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 16
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System\Configs;
21
22
use MikoPBX\Common\Models\Fail2BanRules;
23
use MikoPBX\Common\Models\NetworkFilters;
24
use MikoPBX\Common\Models\PbxSettings;
25
use MikoPBX\Common\Models\PbxSettingsConstants;
26
use MikoPBX\Common\Providers\PBXConfModulesProvider;
27
use MikoPBX\Core\System\Processes;
28
use MikoPBX\Core\System\System;
29
use MikoPBX\Core\System\Util;
30
use MikoPBX\Core\System\Verify;
31
use MikoPBX\Modules\Config\SystemConfigInterface;
32
use Phalcon\Di;
33
use Phalcon\Di\Injectable;
34
use Phalcon\Text;
35
use SQLite3;
36
37
/**
38
 * Class Fail2BanConf
39
 *
40
 * Represents the Fail2Ban configuration.
41
 *
42
 * @package MikoPBX\Core\System\Configs
43
 */
44
class Fail2BanConf extends Injectable
45
{
46
    private const FILTER_PATH     = '/etc/fail2ban/filter.d';
47
    private const ACTION_PATH     = '/etc/fail2ban/action.d';
48
    private const JAILS_DIR       = '/etc/fail2ban/jail.d';
49
    private const PID_FILE        = '/var/run/fail2ban/fail2ban.pid';
50
    public const FAIL2BAN_DB_PATH = '/var/lib/fail2ban/fail2ban.sqlite3';
51
52
    public bool $fail2ban_enable;
53
    private array $allPbxSettings;
54
55
    /**
56
     * Fail2Ban constructor.
57
     */
58
    public function __construct()
59
    {
60
        $this->allPbxSettings  = PbxSettings::getAllPbxSettings();
61
        $fail2ban_enable       = $this->allPbxSettings['PBXFail2BanEnabled'];
62
        $this->fail2ban_enable = ($fail2ban_enable === '1');
63
    }
64
65
    /**
66
     * Check fail2ban service and restart it died
67
     */
68
    public static function checkFail2ban(): void
69
    {
70
        $fail2ban = new self();
71
        if ($fail2ban->fail2ban_enable
72
            && ! $fail2ban->fail2banIsRunning()) {
73
            $fail2ban->fail2banStart();
74
        }
75
    }
76
77
    /**
78
     * Check fail2ban service status
79
     *
80
     * @return bool
81
     */
82
    private function fail2banIsRunning(): bool
83
    {
84
        $fail2banPath = Util::which('fail2ban-client');
85
        $res_ping     = Processes::mwExec("$fail2banPath ping");
86
        $res_stat     = Processes::mwExec("$fail2banPath status");
87
88
        $result = false;
89
        if ($res_ping === 0 && $res_stat === 0) {
90
            $result = true;
91
        }
92
93
        return $result;
94
    }
95
96
    /**
97
     * Start fail2ban service
98
     */
99
    public function fail2banStart(): void
100
    {
101
        if (Util::isSystemctl()) {
102
            $systemctlPath = Util::which('systemctl');
103
            Processes::mwExec("$systemctlPath restart fail2ban");
104
            return;
105
        }
106
        // T2SDE or Docker
107
        Processes::killByName('fail2ban-server');
108
        $fail2banPath = Util::which('fail2ban-client');
109
        $cmd_start    = "$fail2banPath -x start";
110
        $command      = "($cmd_start;) > /dev/null 2>&1 &";
111
        Processes::mwExec($command);
112
    }
113
114
    /**
115
     * Applies iptables settings and restart firewall
116
     */
117
    public static function reloadFail2ban(): void
118
    {
119
        $fail2banPath = Util::which('fail2ban-client');
120
        if(file_exists(self::PID_FILE)){
121
            $pid = file_get_contents(self::PID_FILE);
122
        }else{
123
            $pid = Processes::getPidOfProcess('fail2ban-server');
124
        }
125
        $fail2ban = new self();
126
        if($fail2ban->fail2ban_enable && !empty($pid)){
127
            $fail2ban->generateConf();
128
            $fail2ban->generateModulesFilters();
129
            $fail2ban->generateModulesJailsLocal();
130
            // Reload the configuration without restarting Fail2Ban.
131
            Processes::mwExecBg("$fail2banPath reload");
132
        }
133
    }
134
135
    /**
136
     * Generating the fail2ban.conf config
137
     * @return void
138
     */
139
    private function generateConf():void
140
    {
141
        $log_dir = System::getLogDir() . '/fail2ban/';
142
        $lofFileName = "$log_dir/fail2ban.log";
143
        Util::mwMkdir($log_dir);
144
        $conf = '['.'Definition'.']'.PHP_EOL.
145
                'loglevel = INFO'.PHP_EOL.
146
                'logtarget = FILE'.PHP_EOL.
147
                "syslogsocket = $lofFileName".PHP_EOL.
148
                'socket = /var/run/fail2ban/fail2ban.sock'.PHP_EOL.
149
                'pidfile = /var/run/fail2ban/fail2ban.pid'.PHP_EOL.
150
                'dbfile = /var/lib/fail2ban/fail2ban.sqlite3'.PHP_EOL.
151
                'dbpurgeage = 1d'.PHP_EOL;
152
        Util::fileWriteContent('/etc/fail2ban/fail2ban.conf', $conf);
153
        if(!file_exists($lofFileName)){
154
            file_put_contents($lofFileName, '');
155
        }
156
    }
157
158
    /**
159
     * Rotates the fail2ban log files.
160
     */
161
    public static function logRotate(): void
162
    {
163
        $di           = Di::getDefault();
164
        $fail2banPath = Util::which('fail2ban-client');
165
166
        if ($di === null) {
167
            return;
168
        }
169
        $max_size    = 10;
170
        $log_dir     = System::getLogDir() . '/fail2ban/';
171
        $text_config = $log_dir."fail2ban.log {
172
    nocreate
173
    nocopytruncate
174
    delaycompress
175
    nomissingok
176
    start 0
177
    rotate 9
178
    size {$max_size}M
179
    missingok
180
    noolddir
181
    postrotate
182
        $fail2banPath set logtarget {$log_dir}fail2ban.log > /dev/null 2> /dev/null
183
    endscript
184
    create 640 www www 
185
}";
186
        $varEtcDir  = $di->getShared('config')->path('core.varEtcDir');
187
        $path_conf   = $varEtcDir . '/fail2ban_logrotate.conf';
188
        file_put_contents($path_conf, $text_config);
189
        $mb10 = $max_size * 1024 * 1024;
190
191
        $options = '';
192
        if (Util::mFileSize("{$log_dir}fail2ban.log") > $mb10) {
193
            $options = '-f';
194
        }
195
        $logrotatePath = Util::which('logrotate');
196
        Processes::mwExecBg("$logrotatePath $options '$path_conf' > /dev/null 2> /dev/null");
197
    }
198
199
200
    /**
201
     * Checks whether BANS table exists in DB or not
202
     *
203
     * @param SQLite3 $db
204
     *
205
     * @return bool
206
     */
207
    public function tableBanExists(SQLite3 $db): bool
208
    {
209
        $q_check      = 'SELECT name FROM sqlite_master WHERE type = "table" AND name="bans"';
210
        $result_check = $db->query($q_check);
211
212
        return (false !== $result_check && $result_check->fetchArray(SQLITE3_ASSOC) !== false);
213
    }
214
215
    /**
216
     * Shutdown fail2ban service
217
     */
218
    public function fail2banStop(): void
219
    {
220
        if (Util::isSystemctl()) {
221
            $systemctlPath = Util::which('systemctl');
222
            Processes::mwExec("$systemctlPath stop fail2ban");
223
        } else {
224
            $fail2banPath = Util::which('fail2ban-client');
225
            Processes::mwExec("$fail2banPath -x stop");
226
        }
227
    }
228
229
    /**
230
     * Creates the necessary directories and files for Fail2Ban.
231
     *
232
     * @return string Returns the path to the Fail2Ban database file.
233
     */
234
    public function fail2banMakeDirs(): string
235
    {
236
        $res_file = self::FAIL2BAN_DB_PATH;
237
        $filename = basename($res_file);
238
239
        $old_dir_db = '/cf/fail2ban';
240
        $dir_db     = $this->di->getShared('config')->path('core.fail2banDbDir');
241
        if (empty($dir_db)) {
242
            $dir_db = '/var/spool/fail2ban';
243
        }
244
        Util::mwMkdir($dir_db);
245
246
        // Create working directories.
247
        $db_bd_dir = dirname($res_file);
248
        Util::mwMkdir($db_bd_dir);
249
250
        $create_link = false;
251
252
        // Symbolic link to the database.
253
        if (file_exists($res_file)){
254
            if (filetype($res_file) !== 'link') {
255
                unlink($res_file);
256
                $create_link = true;
257
            } elseif (readlink($res_file) === "$old_dir_db/$filename") {
258
                unlink($res_file);
259
                $create_link = true;
260
                if (file_exists("$old_dir_db/$filename")) {
261
                    // Move the file to the new location.
262
                    $mvPath = Util::which('mv');
263
                    Processes::mwExec("$mvPath '$old_dir_db/$filename' '$dir_db/$filename'");
264
                }
265
            }
266
        }else{
267
            $sqlite3Path = Util::which('sqlite3');
268
            Processes::mwExec("$sqlite3Path $dir_db/$filename 'vacuum'");
269
            $create_link = true;
270
        }
271
272
        if ($create_link === true) {
273
            Util::createUpdateSymlink("$dir_db/$filename", $res_file);
274
        }
275
276
        return $res_file;
277
    }
278
279
    /**
280
     * Writes the Fail2Ban configuration to the jail.local file.
281
     */
282
    public function writeConfig(): void
283
    {
284
        // Initialize properties
285
        [$max_retry, $find_time, $ban_time, $user_whitelist] = $this->initProperty();
286
        $this->generateActions();
287
        $this->generateJails();
288
289
        // Define ports for different services
290
        $httpPorts = [
291
            $this->allPbxSettings['WEBPort'],
292
            $this->allPbxSettings['WEBHTTPSPort']
293
        ];
294
        $sshPort = [
295
            $this->allPbxSettings[PbxSettingsConstants::SSH_PORT],
296
        ];
297
        $asteriskPorts = [
298
            $this->allPbxSettings['SIPPort'],
299
            $this->allPbxSettings['TLS_PORT'],
300
            $this->allPbxSettings['IAXPort'],
301
            $this->allPbxSettings['RTPPortFrom'].':'.$this->allPbxSettings['RTPPortTo'],
302
            $this->allPbxSettings['AJAMPortTLS']
303
        ];
304
        $asteriskAMI = [
305
            $this->allPbxSettings['AMIPort'],
306
            $this->allPbxSettings['AJAMPort'],
307
        ];
308
309
        // Define jails and their corresponding actions
310
        $jails        = [
311
            'dropbear'    => 'miko-iptables-multiport-all[name=SSH, port="'.implode(',', $sshPort).'"]',
312
            'mikopbx-www' => 'miko-iptables-multiport-all[name=HTTP, port="'.implode(',', $httpPorts).'"]',
313
        ];
314
315
        $this->generateModulesJailsLocal($max_retry, $find_time, $ban_time);
316
317
        // Generate the Fail2Ban configuration
318
        $config       = "[DEFAULT]\n" .
319
            "ignoreip = 127.0.0.1 $user_whitelist\n\n";
320
321
        $syslog_file = SyslogConf::getSyslogFile();
322
323
        $commonParams = "enabled = true".PHP_EOL .
324
                        "maxretry = $max_retry\n" .
325
                        "findtime = $find_time\n" .
326
                        "bantime = $ban_time\n" .
327
                        "logencoding = utf-8".PHP_EOL;
328
329
        foreach ($jails as $jail => $action) {
330
            $config .= "[$jail]\n" .
331
                $commonParams.
332
                "logpath = $syslog_file\n" .
333
                "action = $action\n\n";
334
        }
335
336
        $log_dir = System::getLogDir() . '/asterisk/';
337
338
        // Add specific jail configurations for Asterisk logs
339
        $config  .= "[asterisk_security_log]\n" .
340
            "enabled = true\n" .
341
            $commonParams.
342
            'action = miko-iptables-multiport-all[name=ASTERISK, port="'.implode(',', $asteriskPorts).'"]'. PHP_EOL.
343
            "logpath = {$log_dir}security_log\n\n";
344
345
        $config .= "[asterisk_error]\n" .
346
            $commonParams.
347
            "filter = asterisk-main\n" .
348
            'action = miko-iptables-multiport-all[name=ASTERISK_ERROR, port="'.implode(',', $asteriskPorts).'"]'. PHP_EOL.
349
            "logpath = {$log_dir}error\n\n";
350
351
        $config .= "[asterisk_public]\n" .
352
            $commonParams.
353
            "filter = asterisk-main\n" .
354
            'action = miko-iptables-multiport-all[name=ASTERISK_PUBLIC, port="'.implode(',', $asteriskPorts).'"]'. PHP_EOL.
355
            "logpath = {$log_dir}messages\n\n";
356
357
        $config .= "[asterisk_ami]\n" .
358
            $commonParams.
359
            "filter = asterisk-ami\n" .
360
            'action = miko-iptables-multiport-all[name=ASTERISK_AMI, port="'.implode(',', $asteriskAMI).'"]'. PHP_EOL.
361
            "logpath = {$log_dir}messages\n\n";
362
363
        // Write the Fail2Ban configuration to the jail.local file
364
        Util::fileWriteContent('/etc/fail2ban/jail.local', $config);
365
    }
366
367
    /**
368
     * Generate the actions for iptables and write it to a configuration file.
369
     *
370
     * This function constructs the configuration string for iptables based on predefined commands.
371
     * These commands include start, stop, check, ban, and unban actions for the iptables firewall.
372
     * The configuration is written to a file named 'miko-iptables-multiport-all.conf'.
373
     *
374
     * @return void
375
     */
376
    private function generateActions(): void
377
    {
378
        // Define the path to the configuration file
379
        $path = self::ACTION_PATH;
380
381
        // Construct the configuration string
382
        $conf = "[INCLUDES]".PHP_EOL.
383
                "before = iptables.conf".PHP_EOL.
384
                "[Definition]".PHP_EOL.
385
                "actionstart = <iptables> -N f2b-<name>".PHP_EOL.
386
                "              <iptables> -A f2b-<name> -j <returntype>".PHP_EOL.
387
                "              <iptables> -I <chain> -p tcp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
388
                "              <iptables> -I <chain> -p udp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
389
                "actionstop = <iptables> -D <chain> -p tcp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
390
                "             <iptables> -D <chain> -p udp -m multiport --dports <port> -j f2b-<name>".PHP_EOL.
391
                "             <actionflush>".PHP_EOL.
392
                "             <iptables> -X f2b-<name>".PHP_EOL.
393
                "actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \\t]'".PHP_EOL.
394
                "actionban = <iptables> -I f2b-<name> 1 -s <ip> -p tcp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
395
                "            <iptables> -I f2b-<name> 1 -s <ip> -p udp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
396
                "actionunban = <iptables> -D f2b-<name> -s <ip> -p tcp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
397
                "              <iptables> -D f2b-<name> -s <ip> -p udp -m multiport --dports <port> -j <blocktype>".PHP_EOL.
398
                "[Init]".PHP_EOL.PHP_EOL;
399
400
        // Write the configuration string to the configuration file
401
        file_put_contents("$path/miko-iptables-multiport-all.conf", $conf);
402
    }
403
404
405
    /**
406
     * Generate the jail configurations for various services.
407
     *
408
     * This function constructs the jail configuration strings for various services like MikroPBX web interface,
409
     * Dropbear SSH server, Asterisk AMI, and Asterisk security. These jail configurations are written to
410
     * their respective files.
411
     *
412
     * @return void
413
     */
414
    private function generateJails(): void
415
    {
416
        // Define the path to the filter files
417
        $filterPath = self::FILTER_PATH;
418
419
        $commonConf = "[INCLUDES]" . PHP_EOL.
420
            "before = common.conf" . PHP_EOL .
421
            "[Definition]". PHP_EOL;
422
        // Construct the MikoPBX web interface configuration string
423
        $conf = $commonConf .
424
            "_daemon = [\S\W\s]+web_auth\n" .
425
            'failregex = \sFrom:\s<HOST>\sUserAgent:(\S|\s)*Wrong password$' . "\n" .
426
            '            ^(\S|\s)*nginx:\s+\d+/\d+/\d+\s+(\S|\s)*status\s+403(\S|\s)*client:\s+<HOST>(\S|\s)*' . "\n" .
427
            '            \[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$' . "\n" .
428
            "ignoreregex =\n";
429
430
        // Write the configuration to the MikoPBX web interface file
431
        file_put_contents("$filterPath/mikopbx-www.conf", $conf);
432
433
        // Construct the Dropbear SSH server configuration string
434
        $conf = $commonConf .
435
            "_daemon = (authpriv.warn )?dropbear\n" .
436
            'prefregex = ^%(__prefix_line)s<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$' . "\n" .
437
            'failregex = ^[Ll]ogin attempt for nonexistent user (\'.*\' )?from <HOST>:\d+$' . "\n" .
438
            '            ^[Bb]ad (PAM )?password attempt for .+ from <HOST>(:\d+)?$' . "\n" .
439
            '            ^[Ee]xit before auth \(user \'.+\', \d+ fails\): Max auth tries reached - user \'.+\' from <HOST>:\d+\s*$' . "\n" .
440
            "ignoreregex =\n";
441
442
        // Write the configuration to the Dropbear SSH server file
443
        file_put_contents("$filterPath/dropbear.conf", $conf);
444
445
        // Construct the Asterisk AMI configuration string
446
        $conf = $commonConf.
447
                "_daemon = asterisk".PHP_EOL.PHP_EOL.
448
                "__pid_re = (?:\[\d+\])".PHP_EOL.
449
                "_c_ooooo = (\[C-\d+[a-z]*\]?)".PHP_EOL.PHP_EOL.
450
                "log_prefix= (?:NOTICE|SECURITY)%(__pid_re)s:?%(_c_ooooo)s?:? \S+:\d*( in \w+:)?".PHP_EOL.PHP_EOL.
451
                "failregex = ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed to authenticate as '[^']*'\$".PHP_EOL.
452
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s(?:\sHost)? <HOST> failed to authenticate as '[^']*'\$".PHP_EOL.PHP_EOL.
453
                "ignoreregex =".PHP_EOL.PHP_EOL;
454
        // Write the configuration to the Asterisk AMI file
455
        file_put_contents("$filterPath/asterisk-ami.conf", $conf);
456
457
458
        // Construct the Asterisk security configuration string
459
460
        $conf = $commonConf.
461
                "_daemon = asterisk".PHP_EOL.PHP_EOL.
462
                "__pid_re = (?:\[\d+\])".PHP_EOL.
463
                "_c_ooooo = (\[C-\d+[a-z]*\]?)".PHP_EOL.PHP_EOL.
464
                "log_prefix= (?:NOTICE|SECURITY)%(__pid_re)s:?%(_c_ooooo)s?:? \S+:\d*( in \w+:)?".PHP_EOL.PHP_EOL.
465
                "failregex = ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Registration from '[^']*' failed for '<HOST>(:\d+)?' - (Wrong password|Username/auth name mismatch|No matching peer found|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error \(permit/deny\)|Not a local domain)\$".PHP_EOL.
466
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s No registration for peer '[^']*' \(from <HOST>\)\$".PHP_EOL.
467
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Host <HOST> failed MD5 authentication for '[^']*' \([^)]+\)\$".PHP_EOL.
468
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Failed to authenticate (user|device) [^@]+@<HOST>\S*\$".PHP_EOL.
469
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s (?:handle_request_subscribe: )?Sending fake auth rejection for (device|user) \d*<sip:[^@]+@<HOST>>;tag=\w+\S*\$".PHP_EOL.
470
                '            ^(%(__prefix_line)s|\[\]\s*WARNING%(__pid_re)s:?(?:\[C-[\da-f]*\])? )Ext\. s: "Rejecting unknown SIP connection from <HOST>"$'.PHP_EOL.
471
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s <HOST> tried to authenticate with nonexistent user".PHP_EOL.
472
                "			^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s Request (?:'[^']*' )?from '(?:[^']*|.*?)' failed for '<HOST>(?::\d+)?'\s\(callid: [^\)]*\) - (?:No matching endpoint found|Not match Endpoint(?: Contact)? ACL|(?:Failed|Error) to authenticate)\s*\$".PHP_EOL.
473
                "			^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s\s*(anonymous:\s)?Call\s(from)*\s*\((\w*:)?<HOST>:\d+\) to extension '\S*' rejected because extension not found in context 'public-direct-dial'\.\$".PHP_EOL.
474
                '            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s SecurityEvent="(?:FailedACL|InvalidAccountID|ChallengeResponseFailed|InvalidPassword)"(?:(?:,(?!RemoteAddress=)\w+="[^"]*")*|.*?),RemoteAddress="IPV[46]/[^/"]+/<HOST>/\d+"(?:,(?!RemoteAddress=)\w+="[^"]*")*$'.PHP_EOL.
475
                "            ^(%(__prefix_line)s|\[\]\s*)%(log_prefix)s ^hacking attempt detected '<HOST>'\$".PHP_EOL.PHP_EOL.
476
                'ignoreregex = Service="AMI"'.PHP_EOL.PHP_EOL;
477
478
        // Write the configuration to the Asterisk security file
479
        file_put_contents("$filterPath/asterisk-main.conf", $conf);
480
481
        // Generate the module filters
482
        $this->generateModulesFilters();
483
    }
484
485
    /**
486
     * Generate additional modules filter files
487
     */
488
    private function generateModulesFilters(): void
489
    {
490
        $filterPath        = self::FILTER_PATH;
491
        $rmPath            = Util::which('rm');
492
        Processes::mwExec("$rmPath -rf $filterPath/module_*.conf");
493
494
        // Add additional modules routes
495
        $additionalModulesJails = PBXConfModulesProvider::hookModulesMethod(SystemConfigInterface::GENERATE_FAIL2BAN_JAILS);
496
        foreach ($additionalModulesJails as $moduleUniqueId=>$moduleJailText) {
497
            $fileName = Text::uncamelize($moduleUniqueId,'_').'.conf';
498
            file_put_contents("$filterPath/$fileName", $moduleJailText);
499
        }
500
    }
501
502
    /**
503
     * Generate local jail configurations for the provided modules.
504
     *
505
     * This method creates configuration for each jail rule provided by the PBXConfModulesProvider. These
506
     * configurations are then written to their respective files in the jail directory.
507
     *
508
     * @param int $max_retry The maximum number of retries before a host is banned. Default is 0.
509
     * @param int $find_time The time frame in which a host makes unsuccessful login attempts before it is banned. Default is 0.
510
     * @param int $ban_time  The amount of time a host is banned. Default is 0.
511
     *
512
     * @return void
513
     */
514
    private function generateModulesJailsLocal(int $max_retry = 0, int $find_time = 0, int $ban_time = 0): void
515
    {
516
        // Initialize the properties if they are not provided.
517
        if($max_retry === 0){
518
            [$max_retry, $find_time, $ban_time] = $this->initProperty();
519
        }
520
521
        // Create the jail directory if it does not exist
522
        if(!is_dir(self::JAILS_DIR)){
523
            Util::mwMkdir(self::JAILS_DIR);
524
        }
525
526
        // Define the prefix and extension for the jail configuration files
527
        $prefix = 'pbx_';
528
        $extension = 'conf';
529
530
        // Delete all existing jail configuration files
531
        Processes::mwExec("rm -rf ".self::JAILS_DIR."/$prefix*.$extension");
532
533
        // Get the system log file
534
        $syslog_file = SyslogConf::getSyslogFile();
535
536
        // Fetch the jails provided by the modules
537
        $additionalModulesJails = PBXConfModulesProvider::hookModulesMethod(SystemConfigInterface::GENERATE_FAIL2BAN_JAILS);
538
539
        // Iterate over each jail rule provided by the modules
540
        foreach ($additionalModulesJails as $moduleUniqueId=>$moduleJailText) {
541
542
            // Convert the module's unique id to a file-friendly format
543
            $fileName = Text::uncamelize($moduleUniqueId,'_');
544
545
            // Construct the configuration string for the module
546
            $config = "[$fileName]\n" .
547
                "enabled = true\n" .
548
                "logpath = $syslog_file\n" .
549
                "maxretry = $max_retry\n" .
550
                "findtime = $find_time\n" .
551
                "bantime = $ban_time\n" .
552
                "logencoding = utf-8\n" .
553
                "action = iptables-allports[name=$moduleUniqueId, protocol=all]\n\n";
554
555
            // Write the configuration to the jail's configuration file
556
            file_put_contents(self::JAILS_DIR."/".$prefix."$fileName.$extension", $config);
557
        }
558
    }
559
560
    /**
561
     * Initialize fail2ban rule properties.
562
     *
563
     * This method retrieves fail2ban rule properties from the database and constructs a whitelist
564
     * of IPs which should not be banned. If the rule is not found, it assigns default values.
565
     *
566
     * @return array Contains max_retry, find_time, ban_time and user_whitelist.
567
     */
568
    private function initProperty(): array{
569
570
        // Initial empty whitelist.
571
        $user_whitelist = '';
572
573
        // Find the first rule with id '1'.
574
        /** @var Fail2BanRules $res */
575
        $res = Fail2BanRules::findFirst("id = '1'");
576
577
        // If rule exists, extract its properties.
578
        if ($res !== null) {
579
            $max_retry = (int) $res->maxretry;
580
            $find_time = (int) $res->findtime;
581
            $ban_time = (int) $res->bantime;
582
583
            // Explode whitelist IPs into array.
584
            $whitelist = (string) $res->whitelist;
585
            $arr_whitelist = explode(' ', $whitelist);
586
587
            // Verify and add each IP to user whitelist.
588
            foreach ($arr_whitelist as $ip_string) {
589
                if (Verify::isIpAddress($ip_string)) {
590
                    $user_whitelist .= "$ip_string ";
591
                }
592
            }
593
594
            // Fetch network filters where newer_block_ip = '1'.
595
            $net_filters = NetworkFilters::find("newer_block_ip = '1'");
596
597
            // Add each filter's permit IP to user whitelist.
598
            foreach ($net_filters as $filter) {
599
                $user_whitelist .= "$filter->permit ";
600
            }
601
602
            // Trim any trailing spaces from the user whitelist.
603
            $user_whitelist = trim($user_whitelist);
604
        } else {
605
            // If rule doesn't exist, use default values.
606
            $max_retry = 10;
607
            $find_time = 1800;
608
            $ban_time = 43200;
609
        }
610
611
        // Return an array of the properties.
612
        return array($max_retry, $find_time, $ban_time, $user_whitelist);
613
    }
614
615
}