Passed
Push — develop ( fe499f...dd242c )
by Nikolay
05:43 queued 12s
created

Firewall::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 1
nc 1
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 Phalcon\Di;
13
use SQLite3;
14
15
class Firewall
16
{
17
    /**
18
     * @var \Phalcon\Di\DiInterface|null
19
     */
20
    private $di;
21
22
    private bool $firewall_enable;
23
    private bool $fail2ban_enable;
24
25
    /**
26
     * Firewall constructor.
27
     */
28
    public function __construct()
29
    {
30
        $this->di     = Di::getDefault();
31
32
        $config = new MikoPBXConfig();
33
34
        $firewall_enable       = $config->getGeneralSettings('PBXFirewallEnabled');
35
        $this->firewall_enable = ($firewall_enable === '1');
36
37
        $fail2ban_enable       = $config->getGeneralSettings('PBXFail2BanEnabled');
38
        $this->fail2ban_enable = ($fail2ban_enable === '1');
39
40
    }
41
42
    /**
43
     * Рестарт firewall.
44
     *
45
     * @return array
46
     */
47
    public static function reloadFirewall(): array
48
    {
49
        $result = [];
50
51
        $pid_file = '/var/run/service_reloadFirewall.pid';
52
        if (file_exists($pid_file)) {
53
            $old_pid = file_get_contents($pid_file);
54
            $process = Util::getPidOfProcess("^{$old_pid}");
55
            if ($process !== '') {
56
                $result['result'] = 'ERROR';
57
                $result['data']   = 'Another restart process has not yet completed';
58
59
                return $result;
60
            }
61
        }
62
        file_put_contents($pid_file, getmypid());
63
64
        $firewall = new Firewall();
65
        $firewall->applyConfig();
66
67
        unlink($pid_file);
68
        $result['result'] = 'Success';
69
70
        return $result;
71
    }
72
73
    /**
74
     *    Установка правил Firewall.
75
     **/
76
    public function applyConfig(): void
77
    {
78
        self::fail2banStop();
79
        $this->dropAllRules();
80
        self::fail2banMakeDirs();
81
82
        if ($this->firewall_enable) {
83
            $arr_command   = [];
84
            $arr_command[] = $this->getIptablesInputRule('', '-m conntrack --ctstate ESTABLISHED,RELATED');
85
            // Добавляем разрешения на сервисы.
86
            $this->addFirewallRules($arr_command);
87
            // Все остальное запрещаем.
88
            $arr_command[] = $this->getIptablesInputRule('', '', 'DROP');
89
90
            // Кастомизация правил firewall.
91
            $arr_commands_custom = [];
92
            $out                 = [];
93
            Util::fileWriteContent('/etc/firewall_additional', '');
94
95
            $catPath = Util::which('cat');
96
            $grepPath = Util::which('grep');
97
            $busyboxPath = Util::which('busybox');
98
            $awkPath = Util::which('awk');
99
            Util::mwExec(
100
                "{$catPath} /etc/firewall_additional | {$grepPath} -v '|' | {$grepPath} -v '&'| {$grepPath} '^iptables' | {$busyboxPath} {$awkPath} -F ';' '{print $1}'",
101
                $arr_commands_custom
102
            );
103
            if (Util::isSystemctl()) {
104
                Util::mwMkdir('/etc/iptables');
105
                file_put_contents('/etc/iptables/iptables.mikopbx', implode("\n", $arr_command));
106
                file_put_contents(
107
                    '/etc/iptables/iptables.mikopbx',
108
                    "\n" . implode("\n", $arr_commands_custom),
109
                    FILE_APPEND
110
                );
111
                $systemctlPath = Util::which('systemctl');
112
                Util::mwExec("{$systemctlPath} restart mikopbx_iptables");
113
            } else {
114
                Util::mwExecCommands($arr_command, $out, 'firewall');
115
                Util::mwExecCommands($arr_commands_custom, $out, 'firewall_additional');
116
            }
117
        }
118
        if ($this->fail2ban_enable) {
119
            // Настройка правил бана.
120
            $this->writeConfig();
121
            self::fail2banStart();
122
        } else {
123
            self::fail2banStop();
124
        }
125
    }
126
127
    /**
128
     * Завершение работы fail2ban;
129
     */
130
    public static function fail2banStop(): void
131
    {
132
        if (Util::isSystemctl()) {
133
            $systemctlPath = Util::which('systemctl');
134
            Util::mwExec("{$systemctlPath} stop fail2ban");
135
        } else {
136
            $fail2banPath = Util::which('fail2ban-client');
137
            Util::mwExec("{$fail2banPath} -x stop");
138
        }
139
    }
140
141
    /**
142
     *    Удаление всех правил Firewall.
143
     */
144
    private function dropAllRules(): void
145
    {
146
        $iptablesPath = Util::which('iptables');
147
        Util::mwExec("{$iptablesPath} -F");
148
        Util::mwExec("{$iptablesPath} -X");
149
    }
150
151
    /**
152
     * Создает служебные директории и ссылки на файлы fail2ban
153
     *
154
     * @return string
155
     */
156
    public static function fail2banMakeDirs(): string
157
    {
158
        $res_file = self::fail2banGetDbPath();
159
        $filename = basename($res_file);
160
161
        $old_dir_db = '/cf/fail2ban';
162
        $dir_db = '/var/spool/fail2ban';
163
        $di     = Di::getDefault();
164
        if ($di !== null) {
165
            $dir_db = $di->getShared('config')->path('core.fail2banDbDir');
166
        }
167
        Util::mwMkdir($dir_db);
168
        // Создаем рабочие каталоги.
169
        $db_bd_dir = dirname($res_file);
170
        Util::mwMkdir($db_bd_dir);
171
172
        $create_link = false;
173
174
        // Символическая ссылка на базу данных.
175
        if (@filetype($res_file) !== 'link') {
176
            @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

176
            /** @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...
177
            $create_link = true;
178
        } elseif (readlink($res_file) === "$old_dir_db/$filename") {
179
            @unlink($res_file);
180
            $create_link = true;
181
            if (file_exists("$old_dir_db/$filename")) {
182
                // Перемещаем файл в новое местоположение.
183
                $mvPath = Util::which('mv');
184
                Util::mwExec("{$mvPath} '$old_dir_db/$filename' '$dir_db/$filename'");
185
            }
186
        }
187
188
        if ($create_link === true) {
189
            @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

189
            /** @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...
190
        }
191
192
        return $res_file;
193
    }
194
195
    /**
196
     * Возвращает путь к файлу базы данных fail2ban.
197
     *
198
     * @return string
199
     */
200
    public static function fail2banGetDbPath(): string
201
    {
202
        return '/var/lib/fail2ban/fail2ban.sqlite3';
203
    }
204
205
    /**
206
     * Формирует строку правила iptables
207
     *
208
     * @param string $dport
209
     * @param string $other_data
210
     * @param string $action
211
     *
212
     * @return string
213
     */
214
    private function getIptablesInputRule($dport = '', $other_data = '', $action = 'ACCEPT'): string
215
    {
216
        $data_port = '';
217
        if (trim($dport) !== '') {
218
            $data_port = '--dport ' . $dport;
219
        }
220
        $other_data = trim($other_data);
221
222
        return "iptables -A INPUT $other_data $data_port -j $action";
223
    }
224
225
    /**
226
     * Генератор правил iptables.
227
     *
228
     * @param $arr_command
229
     */
230
    private function addFirewallRules(&$arr_command): void
231
    {
232
        /** @var \MikoPBX\Common\Models\FirewallRules $result */
233
        /** @var \MikoPBX\Common\Models\FirewallRules $rule */
234
        /** @var \MikoPBX\Common\Models\FirewallRules $rule */
235
        $result = FirewallRules::find();
236
        foreach ($result as $rule) {
237
            if ($rule->portfrom !== $rule->portto && trim($rule->portto) !== '') {
238
                $port = "{$rule->portfrom}:{$rule->portto}";
239
            } else {
240
                $port = $rule->portfrom;
241
            }
242
            /** @var \MikoPBX\Common\Models\NetworkFilters $network_filter */
243
            $network_filter = NetworkFilters::findFirst($rule->networkfilterid);
244
            if ($network_filter === null) {
245
                Util::sysLogMsg('Firewall', "network_filter_id not found {$rule->networkfilterid}");
246
                continue;
247
            }
248
            if ('0.0.0.0/0' === $network_filter->permit && $rule->action !== 'allow') {
249
                continue;
250
            }
251
            $other_data = "-p {$rule->protocol}";
252
            $other_data .= ($network_filter === null) ? '' : ' -s ' . $network_filter->permit;
253
            if ($rule->protocol === 'icmp') {
254
                $port       = '';
255
                $other_data .= ' --icmp-type echo-request';
256
            }
257
258
            $action        = ($rule->action === 'allow') ? 'ACCEPT' : 'DROP';
259
            $arr_command[] = $this->getIptablesInputRule($port, $other_data, $action);
260
        }
261
        // Разрешим все локальные подключения.
262
        $arr_command[] = $this->getIptablesInputRule('', '-s 127.0.0.1 ', 'ACCEPT');
263
    }
264
265
    /**
266
     * Записываем конфиг для fail2ban. Описываем правила блокировок.
267
     */
268
    private function writeConfig(): void
269
    {
270
        $user_whitelist = '';
271
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
272
        $res = Fail2BanRules::findFirst("id = '1'");
273
        if ($res !== null) {
274
            $max_retry     = $res->maxretry;
275
            $find_time     = $res->findtime;
276
            $ban_time      = $res->bantime;
277
            $whitelist     = $res->whitelist;
278
            $arr_whitelist = explode(' ', $whitelist);
279
            foreach ($arr_whitelist as $ip_string) {
280
                if (Verify::isIpAddress($ip_string)) {
281
                    $user_whitelist .= "$ip_string ";
282
                }
283
            }
284
            $net_filters = NetworkFilters::find("newer_block_ip = '1'");
285
            foreach ($net_filters as $filter) {
286
                $user_whitelist .= "{$filter->permit} ";
287
            }
288
289
            $user_whitelist = trim($user_whitelist);
290
        } else {
291
            $max_retry = '10';
292
            $find_time = '1800';
293
            $ban_time  = '43200';
294
        }
295
        $this->generateJails();
296
        $jails  = [
297
            'dropbear'    => 'iptables-allports[name=SSH, protocol=all]',
298
            'mikoajam'    => 'iptables-allports[name=MIKOAJAM, protocol=all]',
299
            'mikopbx-www' => 'iptables-allports[name=HTTP, protocol=all]',
300
        ];
301
        $config = "[DEFAULT]\n" .
302
            "ignoreip = 127.0.0.1 {$user_whitelist}\n\n";
303
304
        $syslog_file = System::getSyslogFile();
305
306
        foreach ($jails as $jail => $action) {
307
            $config .= "[{$jail}]\n" .
308
                "enabled = true\n" .
309
                "backend = process\n" .
310
                "logpath = {$syslog_file}\n" .
311
                // "logprocess = logread -f\n".
312
                "maxretry = {$max_retry}\n" .
313
                "findtime = {$find_time}\n" .
314
                "bantime = {$ban_time}\n" .
315
                "logencoding = utf-8\n" .
316
                "action = {$action}\n\n";
317
        }
318
319
        $log_dir = System::getLogDir() . '/asterisk/';
320
        $config  .= "[asterisk_security_log]\n" .
321
            "enabled = true\n" .
322
            "filter = asterisk\n" .
323
            "action = iptables-allports[name=ASTERISK, protocol=all]\n" .
324
            "logencoding = utf-8\n" .
325
            "maxretry = {$max_retry}\n" .
326
            "findtime = {$find_time}\n" .
327
            "bantime = {$ban_time}\n" .
328
            "logpath = {$log_dir}security_log\n\n";
329
330
        $config .= "[asterisk_error]\n" .
331
            "enabled = true\n" .
332
            "filter = asterisk\n" .
333
            "action = iptables-allports[name=ASTERISK_ERROR, protocol=all]\n" .
334
            "maxretry = {$max_retry}\n" .
335
            "findtime = {$find_time}\n" .
336
            "bantime = {$ban_time}\n" .
337
            "logencoding = utf-8\n" .
338
            "logpath = {$log_dir}error\n\n";
339
340
        $config .= "[asterisk_public]\n" .
341
            "enabled = true\n" .
342
            "filter = asterisk\n" .
343
            "action = iptables-allports[name=ASTERISK_PUBLIC, protocol=all]\n" .
344
            "maxretry = {$max_retry}\n" .
345
            "findtime = {$find_time}\n" .
346
            "bantime = {$ban_time}\n" .
347
            "logencoding = utf-8\n" .
348
            "logpath = {$log_dir}messages\n\n";
349
350
        Util::fileWriteContent('/etc/fail2ban/jail.local', $config);
351
    }
352
353
    /**
354
     * Создаем дополнительные правила.
355
     */
356
    private function generateJails(): void
357
    {
358
        $conf = "[INCLUDES]\n" .
359
            "before = common.conf\n" .
360
            "[Definition]\n" .
361
            "_daemon = [\S\W\s]+web_auth\n" .
362
            'failregex = ^%(__prefix_line)sFrom:\s<HOST>\sUserAgent:(\S|\s)*Wrong password$' . "\n" .
363
            '            ^(\S|\s)*nginx:\s+\d+/\d+/\d+\s+(\S|\s)*status\s+403(\S|\s)*client:\s+<HOST>(\S|\s)*' . "\n" .
364
            "ignoreregex =\n";
365
        file_put_contents('/etc/fail2ban/filter.d/mikopbx-www.conf', $conf);
366
367
        $conf = "[INCLUDES]\n" .
368
            "before = common.conf\n" .
369
            "[Definition]\n" .
370
            "_daemon = (authpriv.warn )?dropbear\n" .
371
            'prefregex = ^%(__prefix_line)s<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$' . "\n" .
372
            'failregex = ^[Ll]ogin attempt for nonexistent user (\'.*\' )?from <HOST>:\d+$' . "\n" .
373
            '            ^[Bb]ad (PAM )?password attempt for .+ from <HOST>(:\d+)?$' . "\n" .
374
            '            ^[Ee]xit before auth \(user \'.+\', \d+ fails\): Max auth tries reached - user \'.+\' from <HOST>:\d+\s*$' . "\n" .
375
            "ignoreregex =\n";
376
        file_put_contents('/etc/fail2ban/filter.d/dropbear.conf', $conf);
377
378
        // Add module JAIL conf
379
        $additionalModules = $this->di->getShared('pbxConfModules');
0 ignored issues
show
Bug introduced by
The method getShared() does not exist on null. ( Ignorable by Annotation )

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

379
        /** @scrutinizer ignore-call */ 
380
        $additionalModules = $this->di->getShared('pbxConfModules');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
380
        foreach ($additionalModules as $appClass) {
381
           if (method_exists($appClass, 'generateFail2BanJails')){
382
                $content = $appClass->generateFail2BanJails();
383
                if (!empty($content)){
384
                    $moduleUniqueId = $appClass->moduleUniqueId;
385
                    file_put_contents("/etc/fail2ban/filter.d/{$moduleUniqueId}.conf", $content);
386
                }
387
           }
388
        }
389
390
    }
391
392
    /**
393
     * Старт firewall.
394
     */
395
    public static function fail2banStart(): void
396
    {
397
        if (Util::isSystemctl()) {
398
            $systemctlPath = Util::which('systemctl');
399
            Util::mwExec("{$systemctlPath} restart fail2ban");
400
401
            return;
402
        }
403
        // Чистим битые строки, не улдаленные после отмены бана.
404
        self::cleanFail2banDb();
405
        Util::killByName('fail2ban-server');
406
        $fail2banPath = Util::which('fail2ban-client');
407
        $cmd_start = "{$fail2banPath} -x start";
408
        $command   = "($cmd_start;) > /dev/null 2>&1 &";
409
        Util::mwExec($command);
410
    }
411
412
    public static function cleanFail2banDb(): void
413
    {
414
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
415
        $res = Fail2BanRules::findFirst("id = '1'");
416
        if ($res !== null) {
417
            $ban_time = $res->bantime;
418
        } else {
419
            $ban_time = '43800';
420
        }
421
        $path_db = self::fail2banGetDbPath();
422
        $db      = new SQLite3($path_db);
423
        $db->busyTimeout(3000);
424
        if (false === self::tableBanExists($db)) {
425
            return;
426
        }
427
        $q = 'DELETE' . ' from bans WHERE (timeofban+' . $ban_time . ')<' . time();
428
        $db->query($q);
429
    }
430
431
    /**
432
     * Проверка существования таблицы ban в базе данных.
433
     *
434
     * @param SQLite3 $db
435
     *
436
     * @return bool
437
     */
438
    public static function tableBanExists($db): bool
439
    {
440
        $q_check      = 'SELECT' . ' name FROM sqlite_master WHERE type = "table" AND name="bans"';
441
        $result_check = $db->query($q_check);
442
443
        return (false !== $result_check && $result_check->fetchArray(SQLITE3_ASSOC) !== false);
444
    }
445
446
    /**
447
     * Проверка запущен ли fail2ban.
448
     */
449
    public static function checkFail2ban(): void
450
    {
451
        $firewall = new Firewall();
452
        if ($firewall->fail2ban_enable
453
            && ! $firewall->fail2banIsRunning()) {
454
            self::fail2banStart();
455
        }
456
    }
457
458
    /**
459
     * Проверка статуса fail2ban
460
     *
461
     * @return bool
462
     */
463
    private function fail2banIsRunning(): bool
464
    {
465
        $fail2banPath = Util::which('fail2ban-client');
466
        $res_ping = Util::mwExec("{$fail2banPath} ping");
467
        $res_stat = Util::mwExec("{$fail2banPath} status");
468
469
        $result = false;
470
        if ($res_ping === 0 && $res_stat === 0) {
471
            $result = true;
472
        }
473
474
        return $result;
475
    }
476
}
477