Passed
Push — develop ( d91dd3...b816f7 )
by Nikolay
05:31
created

Firewall::checkFail2ban()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 0
1
<?php
2
/**
3
 * Copyright © MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Alexey Portnov, 4 2020
7
 */
8
9
namespace MikoPBX\Core\System;
10
11
use MikoPBX\Common\Models\{Fail2BanRules, FirewallRules, NetworkFilters};
12
use MikoPBX\Core\System\Configs\SyslogConf;
13
use Phalcon\Di;
14
use Phalcon\Di\Injectable;
15
use SQLite3;
16
17
class Firewall extends Injectable
18
{
19
    public const FILTER_PATH = '/etc/fail2ban/filter.d';
20
    public const FAIL2BAN_DB_PATH = '/var/lib/fail2ban/fail2ban.sqlite3';
21
22
    private bool $firewall_enable;
23
    private bool $fail2ban_enable;
24
25
    /**
26
     * Firewall constructor.
27
     */
28
    public function __construct()
29
    {
30
        $mikoPBXConfig = new MikoPBXConfig();
31
32
        $firewall_enable       = $mikoPBXConfig->getGeneralSettings('PBXFirewallEnabled');
33
        $this->firewall_enable = ($firewall_enable === '1');
34
35
        $fail2ban_enable       = $mikoPBXConfig->getGeneralSettings('PBXFail2BanEnabled');
36
        $this->fail2ban_enable = ($fail2ban_enable === '1');
37
    }
38
39
    /**
40
     * Apply iptable settings and restart firewall
41
     */
42
    public static function reloadFirewall(): void
43
    {
44
        $pid_file = '/var/run/service_reload_firewall.pid';
45
        if (file_exists($pid_file)) {
46
            $old_pid = file_get_contents($pid_file);
47
            $process = Util::getPidOfProcess("^{$old_pid}");
48
            if ($process !== '') {
49
                return; // another restart process exists
50
            }
51
        }
52
        file_put_contents($pid_file, getmypid());
53
54
        $firewall = new Firewall();
55
        $firewall->applyConfig();
56
        unlink($pid_file);
57
    }
58
59
    /**
60
     * Apply iptable settings
61
     **/
62
    public function applyConfig(): void
63
    {
64
        self::fail2banStop();
65
        $this->dropAllRules();
66
        self::fail2banMakeDirs();
67
68
        if ($this->firewall_enable) {
69
            $arr_command   = [];
70
            $arr_command[] = $this->getIptablesInputRule('', '-m conntrack --ctstate ESTABLISHED,RELATED');
71
            // Добавляем разрешения на сервисы.
72
            $this->addFirewallRules($arr_command);
73
            // Все остальное запрещаем.
74
            $arr_command[] = $this->getIptablesInputRule('', '', 'DROP');
75
76
            // Кастомизация правил firewall.
77
            $arr_commands_custom = [];
78
            $out                 = [];
79
            Util::fileWriteContent('/etc/firewall_additional', '');
80
81
            $catPath     = Util::which('cat');
82
            $grepPath    = Util::which('grep');
83
            $busyboxPath = Util::which('busybox');
84
            $awkPath     = Util::which('awk');
85
            Util::mwExec(
86
                "{$catPath} /etc/firewall_additional | {$grepPath} -v '|' | {$grepPath} -v '&'| {$grepPath} '^iptables' | {$busyboxPath} {$awkPath} -F ';' '{print $1}'",
87
                $arr_commands_custom
88
            );
89
            if (Util::isSystemctl()) {
90
                Util::mwMkdir('/etc/iptables');
91
                file_put_contents('/etc/iptables/iptables.mikopbx', implode("\n", $arr_command));
92
                file_put_contents(
93
                    '/etc/iptables/iptables.mikopbx',
94
                    "\n" . implode("\n", $arr_commands_custom),
95
                    FILE_APPEND
96
                );
97
                $systemctlPath = Util::which('systemctl');
98
                Util::mwExec("{$systemctlPath} restart mikopbx_iptables");
99
            } else {
100
                Util::mwExecCommands($arr_command, $out, 'firewall');
101
                Util::mwExecCommands($arr_commands_custom, $out, 'firewall_additional');
102
            }
103
        }
104
        if ($this->fail2ban_enable) {
105
            // Настройка правил бана.
106
            $this->writeConfig();
107
            self::fail2banStart();
108
        } else {
109
            self::fail2banStop();
110
        }
111
    }
112
113
    /**
114
     * Завершение работы fail2ban;
115
     */
116
    public static function fail2banStop(): void
117
    {
118
        if (Util::isSystemctl()) {
119
            $systemctlPath = Util::which('systemctl');
120
            Util::mwExec("{$systemctlPath} stop fail2ban");
121
        } else {
122
            $fail2banPath = Util::which('fail2ban-client');
123
            Util::mwExec("{$fail2banPath} -x stop");
124
        }
125
    }
126
127
    /**
128
     *    Удаление всех правил Firewall.
129
     */
130
    private function dropAllRules(): void
131
    {
132
        $iptablesPath = Util::which('iptables');
133
        Util::mwExec("{$iptablesPath} -F");
134
        Util::mwExec("{$iptablesPath} -X");
135
    }
136
137
    /**
138
     * Создает служебные директории и ссылки на файлы fail2ban
139
     *
140
     * @return string
141
     */
142
    public static function fail2banMakeDirs(): string
143
    {
144
        $res_file = self::FAIL2BAN_DB_PATH;
145
        $filename = basename($res_file);
146
147
        $old_dir_db = '/cf/fail2ban';
148
        $dir_db     = '/var/spool/fail2ban';
149
        $di         = Di::getDefault();
150
        if ($di !== null) {
151
            $dir_db = $di->getShared('config')->path('core.fail2banDbDir');
152
        }
153
        Util::mwMkdir($dir_db);
154
        // Создаем рабочие каталоги.
155
        $db_bd_dir = dirname($res_file);
156
        Util::mwMkdir($db_bd_dir);
157
158
        $create_link = false;
159
160
        // Символическая ссылка на базу данных.
161
        if (@filetype($res_file) !== 'link') {
162
            @unlink($res_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

162
            /** @scrutinizer ignore-unhandled */ @unlink($res_file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
163
            $create_link = true;
164
        } elseif (readlink($res_file) === "$old_dir_db/$filename") {
165
            @unlink($res_file);
166
            $create_link = true;
167
            if (file_exists("$old_dir_db/$filename")) {
168
                // Перемещаем файл в новое местоположение.
169
                $mvPath = Util::which('mv');
170
                Util::mwExec("{$mvPath} '$old_dir_db/$filename' '$dir_db/$filename'");
171
            }
172
        }
173
174
        if ($create_link === true) {
175
            @symlink("$dir_db/$filename", $res_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for symlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

175
            /** @scrutinizer ignore-unhandled */ @symlink("$dir_db/$filename", $res_file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
176
        }
177
178
        return $res_file;
179
    }
180
181
    /**
182
     * Формирует строку правила iptables
183
     *
184
     * @param string $dport
185
     * @param string $other_data
186
     * @param string $action
187
     *
188
     * @return string
189
     */
190
    private function getIptablesInputRule($dport = '', $other_data = '', $action = 'ACCEPT'): string
191
    {
192
        $data_port = '';
193
        if (trim($dport) !== '') {
194
            $data_port = '--dport ' . $dport;
195
        }
196
        $other_data = trim($other_data);
197
198
        return "iptables -A INPUT $other_data $data_port -j $action";
199
    }
200
201
    /**
202
     * Генератор правил iptables.
203
     *
204
     * @param $arr_command
205
     */
206
    private function addFirewallRules(&$arr_command): void
207
    {
208
        /** @var \MikoPBX\Common\Models\FirewallRules $result */
209
        /** @var \MikoPBX\Common\Models\FirewallRules $rule */
210
        /** @var \MikoPBX\Common\Models\FirewallRules $rule */
211
        $result = FirewallRules::find();
212
        foreach ($result as $rule) {
213
            if ($rule->portfrom !== $rule->portto && trim($rule->portto) !== '') {
214
                $port = "{$rule->portfrom}:{$rule->portto}";
215
            } else {
216
                $port = $rule->portfrom;
217
            }
218
            /** @var \MikoPBX\Common\Models\NetworkFilters $network_filter */
219
            $network_filter = NetworkFilters::findFirst($rule->networkfilterid);
220
            if ($network_filter === null) {
221
                Util::sysLogMsg('Firewall', "network_filter_id not found {$rule->networkfilterid}");
222
                continue;
223
            }
224
            if ('0.0.0.0/0' === $network_filter->permit && $rule->action !== 'allow') {
225
                continue;
226
            }
227
            $other_data = "-p {$rule->protocol}";
228
            $other_data .= ($network_filter === null) ? '' : ' -s ' . $network_filter->permit;
229
            if ($rule->protocol === 'icmp') {
230
                $port       = '';
231
                $other_data .= ' --icmp-type echo-request';
232
            }
233
234
            $action        = ($rule->action === 'allow') ? 'ACCEPT' : 'DROP';
235
            $arr_command[] = $this->getIptablesInputRule($port, $other_data, $action);
236
        }
237
        // Разрешим все локальные подключения.
238
        $arr_command[] = $this->getIptablesInputRule('', '-s 127.0.0.1 ', 'ACCEPT');
239
    }
240
241
    /**
242
     * Записываем конфиг для fail2ban. Описываем правила блокировок.
243
     */
244
    private function writeConfig(): void
245
    {
246
        $user_whitelist = '';
247
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
248
        $res = Fail2BanRules::findFirst("id = '1'");
249
        if ($res !== null) {
250
            $max_retry     = $res->maxretry;
251
            $find_time     = $res->findtime;
252
            $ban_time      = $res->bantime;
253
            $whitelist     = $res->whitelist;
254
            $arr_whitelist = explode(' ', $whitelist);
255
            foreach ($arr_whitelist as $ip_string) {
256
                if (Verify::isIpAddress($ip_string)) {
257
                    $user_whitelist .= "$ip_string ";
258
                }
259
            }
260
            $net_filters = NetworkFilters::find("newer_block_ip = '1'");
261
            foreach ($net_filters as $filter) {
262
                $user_whitelist .= "{$filter->permit} ";
263
            }
264
265
            $user_whitelist = trim($user_whitelist);
266
        } else {
267
            $max_retry = '10';
268
            $find_time = '1800';
269
            $ban_time  = '43200';
270
        }
271
        $this->generateJails();
272
        $jails  = [
273
            'dropbear'    => 'iptables-allports[name=SSH, protocol=all]',
274
            'mikoajam'    => 'iptables-allports[name=MIKOAJAM, protocol=all]',
275
            'mikopbx-www' => 'iptables-allports[name=HTTP, protocol=all]',
276
        ];
277
        $config = "[DEFAULT]\n" .
278
            "ignoreip = 127.0.0.1 {$user_whitelist}\n\n";
279
280
        $syslog_file = SyslogConf::getSyslogFile();
281
282
        foreach ($jails as $jail => $action) {
283
            $config .= "[{$jail}]\n" .
284
                "enabled = true\n" .
285
                "backend = process\n" .
286
                "logpath = {$syslog_file}\n" .
287
                // "logprocess = logread -f\n".
288
                "maxretry = {$max_retry}\n" .
289
                "findtime = {$find_time}\n" .
290
                "bantime = {$ban_time}\n" .
291
                "logencoding = utf-8\n" .
292
                "action = {$action}\n\n";
293
        }
294
295
        $log_dir = System::getLogDir() . '/asterisk/';
296
        $config  .= "[asterisk_security_log]\n" .
297
            "enabled = true\n" .
298
            "filter = asterisk\n" .
299
            "action = iptables-allports[name=ASTERISK, protocol=all]\n" .
300
            "logencoding = utf-8\n" .
301
            "maxretry = {$max_retry}\n" .
302
            "findtime = {$find_time}\n" .
303
            "bantime = {$ban_time}\n" .
304
            "logpath = {$log_dir}security_log\n\n";
305
306
        $config .= "[asterisk_error]\n" .
307
            "enabled = true\n" .
308
            "filter = asterisk\n" .
309
            "action = iptables-allports[name=ASTERISK_ERROR, protocol=all]\n" .
310
            "maxretry = {$max_retry}\n" .
311
            "findtime = {$find_time}\n" .
312
            "bantime = {$ban_time}\n" .
313
            "logencoding = utf-8\n" .
314
            "logpath = {$log_dir}error\n\n";
315
316
        $config .= "[asterisk_public]\n" .
317
            "enabled = true\n" .
318
            "filter = asterisk\n" .
319
            "action = iptables-allports[name=ASTERISK_PUBLIC, protocol=all]\n" .
320
            "maxretry = {$max_retry}\n" .
321
            "findtime = {$find_time}\n" .
322
            "bantime = {$ban_time}\n" .
323
            "logencoding = utf-8\n" .
324
            "logpath = {$log_dir}messages\n\n";
325
326
        Util::fileWriteContent('/etc/fail2ban/jail.local', $config);
327
    }
328
329
    /**
330
     * Создаем дополнительные правила.
331
     */
332
    private function generateJails(): void
333
    {
334
        $filterPath = self::FILTER_PATH;
335
336
        $conf = "[INCLUDES]\n" .
337
            "before = common.conf\n" .
338
            "[Definition]\n" .
339
            "_daemon = [\S\W\s]+web_auth\n" .
340
            'failregex = ^%(__prefix_line)sFrom:\s<HOST>\sUserAgent:(\S|\s)*Wrong password$' . "\n" .
341
            '            ^(\S|\s)*nginx:\s+\d+/\d+/\d+\s+(\S|\s)*status\s+403(\S|\s)*client:\s+<HOST>(\S|\s)*' . "\n" .
342
            "ignoreregex =\n";
343
        file_put_contents("{$filterPath}/mikopbx-www.conf", $conf);
344
345
        $conf = "[INCLUDES]\n" .
346
            "before = common.conf\n" .
347
            "[Definition]\n" .
348
            "_daemon = (authpriv.warn )?dropbear\n" .
349
            'prefregex = ^%(__prefix_line)s<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$' . "\n" .
350
            'failregex = ^[Ll]ogin attempt for nonexistent user (\'.*\' )?from <HOST>:\d+$' . "\n" .
351
            '            ^[Bb]ad (PAM )?password attempt for .+ from <HOST>(:\d+)?$' . "\n" .
352
            '            ^[Ee]xit before auth \(user \'.+\', \d+ fails\): Max auth tries reached - user \'.+\' from <HOST>:\d+\s*$' . "\n" .
353
            "ignoreregex =\n";
354
        file_put_contents("{$filterPath}/dropbear.conf", $conf);
355
356
        // Add module JAIL conf
357
       $this->generateModulesJails();
358
    }
359
360
    /**
361
     * Generate Additional modules jails
362
     */
363
    public function generateModulesJails(): void
364
    {
365
        $filterPath = self::FILTER_PATH;
366
        $additionalModules = $this->di->getShared('pbxConfModules');
367
        $rmPath            = Util::which('rm');
368
        Util::mwExec("{$rmPath} -rf {$filterPath}/module_*.conf");
369
        foreach ($additionalModules as $appClass) {
370
            if (method_exists($appClass, 'generateFail2BanJails')) {
371
                $content = $appClass->generateFail2BanJails();
372
                if ( ! empty($content)) {
373
                    $moduleUniqueId = $appClass->moduleUniqueId;
374
                    file_put_contents("{$filterPath}/module_{$moduleUniqueId}.conf", $content);
375
                }
376
            }
377
        }
378
    }
379
380
    /**
381
     * Старт firewall.
382
     */
383
    public static function fail2banStart(): void
384
    {
385
        if (Util::isSystemctl()) {
386
            $systemctlPath = Util::which('systemctl');
387
            Util::mwExec("{$systemctlPath} restart fail2ban");
388
389
            return;
390
        }
391
        // Чистим битые строки, не улдаленные после отмены бана.
392
        self::cleanFail2banDb();
393
        Util::killByName('fail2ban-server');
394
        $fail2banPath = Util::which('fail2ban-client');
395
        $cmd_start    = "{$fail2banPath} -x start";
396
        $command      = "($cmd_start;) > /dev/null 2>&1 &";
397
        Util::mwExec($command);
398
    }
399
400
    public static function cleanFail2banDb(): void
401
    {
402
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
403
        $res = Fail2BanRules::findFirst("id = '1'");
404
        if ($res !== null) {
405
            $ban_time = $res->bantime;
406
        } else {
407
            $ban_time = '43800';
408
        }
409
        $path_db = self::FAIL2BAN_DB_PATH;
410
        $db      = new SQLite3($path_db);
411
        $db->busyTimeout(3000);
412
        if (false === self::tableBanExists($db)) {
413
            return;
414
        }
415
        $q = 'DELETE' . ' from bans WHERE (timeofban+' . $ban_time . ')<' . time();
416
        $db->query($q);
417
    }
418
419
    /**
420
     * Проверка существования таблицы ban в базе данных.
421
     *
422
     * @param SQLite3 $db
423
     *
424
     * @return bool
425
     */
426
    public static function tableBanExists($db): bool
427
    {
428
        $q_check      = 'SELECT' . ' name FROM sqlite_master WHERE type = "table" AND name="bans"';
429
        $result_check = $db->query($q_check);
430
431
        return (false !== $result_check && $result_check->fetchArray(SQLITE3_ASSOC) !== false);
432
    }
433
434
    /**
435
     * Проверка запущен ли fail2ban.
436
     */
437
    public static function checkFail2ban(): void
438
    {
439
        $firewall = new Firewall();
440
        if ($firewall->fail2ban_enable
441
            && ! $firewall->fail2banIsRunning()) {
442
            self::fail2banStart();
443
        }
444
    }
445
446
    /**
447
     * Проверка статуса fail2ban
448
     *
449
     * @return bool
450
     */
451
    private function fail2banIsRunning(): bool
452
    {
453
        $fail2banPath = Util::which('fail2ban-client');
454
        $res_ping     = Util::mwExec("{$fail2banPath} ping");
455
        $res_stat     = Util::mwExec("{$fail2banPath} status");
456
457
        $result = false;
458
        if ($res_ping === 0 && $res_stat === 0) {
459
            $result = true;
460
        }
461
462
        return $result;
463
    }
464
}
465