Passed
Push — develop ( b5c301...121c4e )
by Портнов
04:48
created

Fail2BanConf::reloadFail2ban()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 15
rs 9.9
c 0
b 0
f 0
cc 4
nc 4
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright (C) 2017-2020 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\Core\System\MikoPBXConfig;
25
use MikoPBX\Core\System\Processes;
26
use MikoPBX\Core\System\System;
27
use MikoPBX\Core\System\Util;
28
use MikoPBX\Core\System\Verify;
29
use Phalcon\Di\Injectable;
30
use Phalcon\Text;
31
use SQLite3;
32
33
class Fail2BanConf extends Injectable
34
{
35
    private const FILTER_PATH = '/etc/fail2ban/filter.d';
36
    private const JAILS_DIR   = '/etc/fail2ban/jail.d';
37
    private const PID_FILE    = '/var/run/fail2ban/fail2ban.pid';
38
    public const FAIL2BAN_DB_PATH = '/var/lib/fail2ban/fail2ban.sqlite3';
39
40
    public bool $fail2ban_enable;
41
42
43
    /**
44
     * Fail2Ban constructor.
45
     */
46
    public function __construct()
47
    {
48
        $mikoPBXConfig         = new MikoPBXConfig();
49
        $fail2ban_enable       = $mikoPBXConfig->getGeneralSettings('PBXFail2BanEnabled');
50
        $this->fail2ban_enable = ($fail2ban_enable === '1');
51
    }
52
53
    /**
54
     * Check fail2ban service and restart it died
55
     */
56
    public static function checkFail2ban(): void
57
    {
58
        $fail2ban = new self();
59
        if ($fail2ban->fail2ban_enable
60
            && ! $fail2ban->fail2banIsRunning()) {
61
            $fail2ban->fail2banStart();
62
        }
63
    }
64
65
    /**
66
     * Check fail2ban service status
67
     *
68
     * @return bool
69
     */
70
    private function fail2banIsRunning(): bool
71
    {
72
        $fail2banPath = Util::which('fail2ban-client');
73
        $res_ping     = Processes::mwExec("{$fail2banPath} ping");
74
        $res_stat     = Processes::mwExec("{$fail2banPath} status");
75
76
        $result = false;
77
        if ($res_ping === 0 && $res_stat === 0) {
78
            $result = true;
79
        }
80
81
        return $result;
82
    }
83
84
    /**
85
     * Start fail2ban service
86
     */
87
    public function fail2banStart(): void
88
    {
89
        if (Util::isSystemctl()) {
90
            $systemctlPath = Util::which('systemctl');
91
            Processes::mwExec("{$systemctlPath} restart fail2ban");
92
93
            return;
94
        }
95
        Processes::killByName('fail2ban-server');
96
        $fail2banPath = Util::which('fail2ban-client');
97
        $cmd_start    = "{$fail2banPath} -x start";
98
        $command      = "($cmd_start;) > /dev/null 2>&1 &";
99
        Processes::mwExec($command);
100
    }
101
102
    /**
103
     * Applies iptables settings and restart firewall
104
     */
105
    public static function reloadFail2ban(): void
106
    {
107
        $fail2banPath = Util::which('fail2ban-client');
108
        if(file_exists(self::PID_FILE)){
109
            $pid = file_get_contents(self::PID_FILE);
110
        }else{
111
            $pid = Processes::getPidOfProcess('fail2ban-server');
112
        }
113
        $fail2ban = new self();
114
        if($fail2ban->fail2ban_enable && !empty($pid)){
115
            $countAdditionalModules = count($fail2ban->di->getShared('pbxConfModules'));
0 ignored issues
show
Unused Code introduced by
The assignment to $countAdditionalModules is dead and can be removed.
Loading history...
116
            $fail2ban->generateModulesFilters();
117
            $fail2ban->generateModulesJailsLocal();
118
            // Перезагрузка конфигов без рестарта конфига.
119
            Processes::mwExecBg("{$fail2banPath} reload");
120
        }
121
    }
122
123
    /**
124
     * Checks whether BANS table exists in DB or not
125
     *
126
     * @param SQLite3 $db
127
     *
128
     * @return bool
129
     */
130
    public function tableBanExists(SQLite3 $db): bool
131
    {
132
        $q_check      = 'SELECT name FROM sqlite_master WHERE type = "table" AND name="bans"';
133
        $result_check = $db->query($q_check);
134
135
        return (false !== $result_check && $result_check->fetchArray(SQLITE3_ASSOC) !== false);
136
    }
137
138
    /**
139
     * Shutdown fail2ban service
140
     */
141
    public function fail2banStop(): void
142
    {
143
        if (Util::isSystemctl()) {
144
            $systemctlPath = Util::which('systemctl');
145
            Processes::mwExec("{$systemctlPath} stop fail2ban");
146
        } else {
147
            $fail2banPath = Util::which('fail2ban-client');
148
            Processes::mwExec("{$fail2banPath} -x stop");
149
        }
150
    }
151
152
    /**
153
     * Create fail2ban dirs and DB if it does not exists
154
     *
155
     * @return string
156
     */
157
    public function fail2banMakeDirs(): string
158
    {
159
        $res_file = self::FAIL2BAN_DB_PATH;
160
        $filename = basename($res_file);
161
162
        $old_dir_db = '/cf/fail2ban';
163
        $dir_db     = $this->di->getShared('config')->path('core.fail2banDbDir');
164
        if (empty($dir_db)) {
165
            $dir_db = '/var/spool/fail2ban';
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 (file_exists($res_file)){
176
            if (filetype($res_file) !== 'link') {
177
                unlink($res_file);
178
                $create_link = true;
179
            } elseif (readlink($res_file) === "$old_dir_db/$filename") {
180
                unlink($res_file);
181
                $create_link = true;
182
                if (file_exists("$old_dir_db/$filename")) {
183
                    // Перемещаем файл в новое местоположение.
184
                    $mvPath = Util::which('mv');
185
                    Processes::mwExec("{$mvPath} '$old_dir_db/$filename' '$dir_db/$filename'");
186
                }
187
            }
188
        }
189
        if ($create_link === true) {
190
            Util::createUpdateSymlink("$dir_db/$filename", $res_file);
191
        }
192
193
        return $res_file;
194
    }
195
196
    /**
197
     * Записываем конфиг для fail2ban. Описываем правила блокировок.
198
     */
199
    public function writeConfig(): void
200
    {
201
        [$max_retry, $find_time, $ban_time, $user_whitelist] = $this->initProperty();
202
        $this->generateJails();
203
204
        $jails        = [
205
            'dropbear'    => 'iptables-allports[name=SSH, protocol=all]',
206
            'mikopbx-www' => 'iptables-allports[name=HTTP, protocol=all]',
207
        ];
208
209
        $this->generateModulesJailsLocal($max_retry, $find_time, $ban_time);
210
211
        $config       = "[DEFAULT]\n" .
212
            "ignoreip = 127.0.0.1 {$user_whitelist}\n\n";
213
214
        $syslog_file = SyslogConf::getSyslogFile();
215
216
        foreach ($jails as $jail => $action) {
217
            $config .= "[{$jail}]\n" .
218
                "enabled = true\n" .
219
                "backend = process\n" .
220
                "logpath = {$syslog_file}\n" .
221
                // "logprocess = logread -f\n".
222
                "maxretry = {$max_retry}\n" .
223
                "findtime = {$find_time}\n" .
224
                "bantime = {$ban_time}\n" .
225
                "logencoding = utf-8\n" .
226
                "action = {$action}\n\n";
227
        }
228
229
        $log_dir = System::getLogDir() . '/asterisk/';
230
        $config  .= "[asterisk_security_log]\n" .
231
            "enabled = true\n" .
232
            "filter = asterisk\n" .
233
            "action = iptables-allports[name=ASTERISK, protocol=all]\n" .
234
            "logencoding = utf-8\n" .
235
            "maxretry = {$max_retry}\n" .
236
            "findtime = {$find_time}\n" .
237
            "bantime = {$ban_time}\n" .
238
            "logpath = {$log_dir}security_log\n\n";
239
240
        $config .= "[asterisk_error]\n" .
241
            "enabled = true\n" .
242
            "filter = asterisk\n" .
243
            "action = iptables-allports[name=ASTERISK_ERROR, protocol=all]\n" .
244
            "maxretry = {$max_retry}\n" .
245
            "findtime = {$find_time}\n" .
246
            "bantime = {$ban_time}\n" .
247
            "logencoding = utf-8\n" .
248
            "logpath = {$log_dir}error\n\n";
249
250
        $config .= "[asterisk_public]\n" .
251
            "enabled = true\n" .
252
            "filter = asterisk\n" .
253
            "action = iptables-allports[name=ASTERISK_PUBLIC, protocol=all]\n" .
254
            "maxretry = {$max_retry}\n" .
255
            "findtime = {$find_time}\n" .
256
            "bantime = {$ban_time}\n" .
257
            "logencoding = utf-8\n" .
258
            "logpath = {$log_dir}messages\n\n";
259
260
        Util::fileWriteContent('/etc/fail2ban/jail.local', $config);
261
    }
262
263
    /**
264
     * Creates additional rules
265
     */
266
    private function generateJails(): void
267
    {
268
        $filterPath = self::FILTER_PATH;
269
270
        $conf = "[INCLUDES]\n" .
271
            "before = common.conf\n" .
272
            "[Definition]\n" .
273
            "_daemon = [\S\W\s]+web_auth\n" .
274
            'failregex = ^%(__prefix_line)sFrom:\s<HOST>\sUserAgent:(\S|\s)*Wrong password$' . "\n" .
275
            '            ^(\S|\s)*nginx:\s+\d+/\d+/\d+\s+(\S|\s)*status\s+403(\S|\s)*client:\s+<HOST>(\S|\s)*' . "\n" .
276
            "ignoreregex =\n";
277
        file_put_contents("{$filterPath}/mikopbx-www.conf", $conf);
278
279
        $conf = "[INCLUDES]\n" .
280
            "before = common.conf\n" .
281
            "[Definition]\n" .
282
            "_daemon = (authpriv.warn )?dropbear\n" .
283
            'prefregex = ^%(__prefix_line)s<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$' . "\n" .
284
            'failregex = ^[Ll]ogin attempt for nonexistent user (\'.*\' )?from <HOST>:\d+$' . "\n" .
285
            '            ^[Bb]ad (PAM )?password attempt for .+ from <HOST>(:\d+)?$' . "\n" .
286
            '            ^[Ee]xit before auth \(user \'.+\', \d+ fails\): Max auth tries reached - user \'.+\' from <HOST>:\d+\s*$' . "\n" .
287
            "ignoreregex =\n";
288
        file_put_contents("{$filterPath}/dropbear.conf", $conf);
289
290
        $this->generateModulesFilters();
291
    }
292
293
    /**
294
     * Generate additional modules filter files
295
     */
296
    public function generateModulesFilters(): void
297
    {
298
        $filterPath        = self::FILTER_PATH;
299
        $additionalModules = $this->di->getShared('pbxConfModules');
300
        $rmPath            = Util::which('rm');
301
        Processes::mwExec("{$rmPath} -rf {$filterPath}/module_*.conf");
302
        foreach ($additionalModules as $appClass) {
303
            if (!method_exists($appClass, 'generateFail2BanJails')) {
304
                continue;
305
            }
306
            $content = $appClass->generateFail2BanJails();
307
            if (empty($content)) {
308
                continue;
309
            }
310
            $moduleUniqueId = $appClass->moduleUniqueId;
311
            $fileName = Text::uncamelize($moduleUniqueId,'_').'.conf';
312
            file_put_contents("{$filterPath}/{$fileName}", $content);
313
        }
314
    }
315
316
    /**
317
     * @param $max_retry
318
     * @param $find_time
319
     * @param $ban_time
320
     */
321
    public function generateModulesJailsLocal($max_retry = 0, $find_time = 0, $ban_time = 0): void
322
    {
323
        if($max_retry === 0){
324
            [$max_retry, $find_time, $ban_time] = $this->initProperty();
325
        }
326
        if(!is_dir(self::JAILS_DIR)){
327
            Util::mwMkdir(self::JAILS_DIR);
328
        }
329
        $prefix = 'pbx_';
330
        $extension = 'conf';
331
        Processes::mwExec("rm -rf ".self::JAILS_DIR."/{$prefix}*.{$extension}");
332
        $syslog_file = SyslogConf::getSyslogFile();
333
334
        $additionalModules = $this->di->getShared('pbxConfModules');
335
        foreach ($additionalModules as $appClass) {
336
            if (!method_exists($appClass, 'generateFail2BanJails')) {
337
                continue;
338
            }
339
            if ( empty($appClass->generateFail2BanJails())) {
340
                continue;
341
            }
342
            $moduleUniqueId                    = $appClass->moduleUniqueId;
343
            $fileName = Text::uncamelize($moduleUniqueId,'_');
344
345
            $config = "[{$fileName}]\n" .
346
                "enabled = true\n" .
347
                "backend = process\n" .
348
                "logpath = {$syslog_file}\n" .
349
                "maxretry = {$max_retry}\n" .
350
                "findtime = {$find_time}\n" .
351
                "bantime = {$ban_time}\n" .
352
                "logencoding = utf-8\n" .
353
                "action = iptables-allports[name={$moduleUniqueId}, protocol=all]\n\n";
354
355
            file_put_contents(self::JAILS_DIR."/{$prefix}{$fileName}.{$extension}", $config);
356
        }
357
358
    }
359
360
    /**
361
     * @return array
362
     */
363
    private function initProperty(): array{
364
        $user_whitelist = '';
365
        /** @var Fail2BanRules $res */
366
        $res = Fail2BanRules::findFirst("id = '1'");
367
        if ($res !== null) {
368
            $max_retry = $res->maxretry;
369
            $find_time = $res->findtime;
370
            $ban_time = $res->bantime;
371
            $whitelist = $res->whitelist;
372
            $arr_whitelist = explode(' ', $whitelist);
373
            foreach ($arr_whitelist as $ip_string) {
374
                if (Verify::isIpAddress($ip_string)) {
375
                    $user_whitelist .= "$ip_string ";
376
                }
377
            }
378
            $net_filters = NetworkFilters::find("newer_block_ip = '1'");
379
            foreach ($net_filters as $filter) {
380
                $user_whitelist .= "{$filter->permit} ";
381
            }
382
383
            $user_whitelist = trim($user_whitelist);
384
        } else {
385
            $max_retry = '10';
386
            $find_time = '1800';
387
            $ban_time = '43200';
388
        }
389
        return array($max_retry, $find_time, $ban_time, $user_whitelist);
390
    }
391
392
}