Passed
Push — develop ( 04a2ba...44a5a9 )
by Nikolay
04:26
created

Network::getInfoMessage()   B

Complexity

Conditions 11
Paths 81

Size

Total Lines 39
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 26
c 1
b 0
f 0
dl 0
loc 39
rs 7.3166
cc 11
nc 81
nop 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
21
22
use MikoPBX\Common\Models\{LanInterfaces, PbxSettings};
23
use MikoPBX\Core\Utilities\SubnetCalculator;
24
use Phalcon\Di\Injectable;
25
use Symfony\Component\Process\Process;
26
use Throwable;
27
28
/**
29
 * Class Network
30
 *
31
 *
32
 *
33
 * @package MikoPBX\Core\System
34
 */
35
class Network extends Injectable
36
{
37
38
    /**
39
     * Starts the SIP dump process.
40
     */
41
    public static function startSipDump(): void
42
    {
43
        $config = new MikoPBXConfig();
44
        $use = $config->getGeneralSettings('USE_PCAP_SIP_DUMP');
45
        if ($use !== '1') {
46
            return;
47
        }
48
49
        Processes::killByName('pcapsipdump');
50
        $log_dir = System::getLogDir() . '/pcapsipdump';
51
        Util::mwMkdir($log_dir);
52
53
        $network = new Network();
54
        $arr_eth = $network->getInterfacesNames();
55
        $pcapsipdumpPath = Util::which('pcapsipdump');
56
        foreach ($arr_eth as $eth) {
57
            $pid_file = "/var/run/pcapsipdump_{$eth}.pid";
58
            Processes::mwExecBg(
59
                $pcapsipdumpPath . ' -T 120 -P ' . $pid_file . ' -i ' . $eth . ' -m \'^(INVITE|REGISTER)$\' -L ' . $log_dir . '/dump.db'
60
            );
61
        }
62
    }
63
64
    /**
65
     * Retrieves the names of all PCI network interfaces.
66
     *
67
     * @return array An array containing the names of the network interfaces.
68
     */
69
    public function getInterfacesNames(): array
70
    {
71
        $grepPath = Util::which('grep');
72
        $awkPath = Util::which('awk');
73
        if (Util::isDocker()) {
74
            $ifconfigPath = Util::which('ifconfig');
75
            $command = "{$ifconfigPath} | {$grepPath} -o -E '^[a-zA-Z0-9]+' | {$grepPath} -v 'lo'";
76
        } else {
77
            // Universal command to retrieve all PCI network interfaces.
78
            $lsPath = Util::which('ls');
79
            $command = "{$lsPath} -l /sys/class/net | {$grepPath} devices | {$grepPath} -v virtual | {$awkPath} '{ print $9 }'";
80
        }
81
        Processes::mwExec($command, $names);
82
        return $names;
83
    }
84
85
    /**
86
     * Retrieves the information message containing available web interface addresses.
87
     *
88
     * @return string The information message.
89
     */
90
    public static function getInfoMessage(): string
91
    {
92
        $addresses = [
93
            'local' => [],
94
            'external' => []
95
        ];
96
        /** @var LanInterfaces $interface */
97
        $interfaces = LanInterfaces::find("disabled='0'");
98
        foreach ($interfaces as $interface) {
99
            if (!empty($interface->ipaddr)) {
100
                $addresses['local'][] = $interface->ipaddr;
101
            }
102
            if (!empty($interface->exthostname) && !in_array($interface->exthostname, $addresses['local'], true)) {
103
                $addresses['external'][] = explode(':', $interface->exthostname)[0] ?? '';
104
            }
105
            if (!empty($interface->extipaddr) && !in_array($interface->extipaddr, $addresses['local'], true)) {
106
                $addresses['external'][] = explode(':', $interface->extipaddr)[0] ?? '';
107
            }
108
        }
109
        unset($interfaces);
110
        $port = PbxSettings::getValueByKey('WEBHTTPSPort');
111
        $info = PHP_EOL . "   The web interface is available at the addresses:" . PHP_EOL . PHP_EOL;
112
        foreach ($addresses['local'] as $address) {
113
            if (empty($address)) {
114
                continue;
115
            }
116
            $info .= "    - https://$address:$port" . PHP_EOL;
117
        }
118
        $info .= PHP_EOL;
119
        $info .= "   The web interface is available at the external addresses:" . PHP_EOL . PHP_EOL;
120
        foreach ($addresses['external'] as $address) {
121
            if (empty($address)) {
122
                continue;
123
            }
124
            $info .= "    - https://$address:$port" . PHP_EOL;
125
        }
126
        $info .= PHP_EOL;
127
128
        return $info;
129
    }
130
131
    /**
132
     * Configures the loopback interface (lo) with the IP address 127.0.0.1.
133
     *
134
     * @return void
135
     */
136
    public function loConfigure(): void
137
    {
138
        $busyboxPath = Util::which('busybox');
139
        $ifconfigPath = Util::which('ifconfig');
140
        Processes::mwExec("{$busyboxPath} {$ifconfigPath} lo 127.0.0.1");
141
    }
142
143
    /**
144
     * Generates the resolv.conf file based on system configuration.
145
     */
146
    public function resolvConfGenerate(): void
147
    {
148
        if (Util::isDocker()) {
149
            return;
150
        }
151
152
        // Initialize resolv.conf content
153
        $resolv_conf = '';
154
155
        // Get hostname information
156
        $data_hostname = self::getHostName();
157
158
        // Append domain to resolv.conf if it is not empty
159
        if (trim($data_hostname['domain']) !== '') {
160
            $resolv_conf .= "domain {$data_hostname['domain']}\n";
161
        }
162
163
        // Append local nameserver to resolv.conf
164
        $resolv_conf .= "nameserver 127.0.0.1\n";
165
166
        // Initialize an array to store named DNS servers
167
        $named_dns = [];
168
169
        // Retrieve host DNS settings
170
        $dns = $this->getHostDNS();
171
172
        // Iterate over each DNS server
173
        foreach ($dns as $ns) {
174
            // Skip empty DNS servers
175
            if (trim($ns) === '') {
176
                continue;
177
            }
178
            // Add the DNS server to the named_dns array
179
            $named_dns[] = $ns;
180
181
            // Append the DNS server to resolv.conf
182
            $resolv_conf .= "nameserver {$ns}\n";
183
        }
184
185
        // If no DNS servers were found, use default ones and add them to named_dns
186
        if (count($dns) === 0) {
187
            $resolv_conf .= "nameserver 4.4.4.4\n";
188
            $named_dns[] .= "8.8.8.8";
189
        }
190
191
        // Check if systemctl is available
192
        if (Util::isSystemctl()) {
193
194
            // Generate resolved.conf content for systemd-resolved
195
            $s_resolv_conf = "[Resolve]\n"
196
                . "DNS=127.0.0.1\n";
197
198
            // Append domain to resolved.conf if it is not empty
199
            if (trim($data_hostname['domain']) !== '') {
200
                $s_resolv_conf .= "Domains={$data_hostname['domain']}\n";
201
            }
202
203
            // Write resolved.conf content to the file
204
            file_put_contents('/etc/systemd/resolved.conf', $s_resolv_conf);
205
206
            // Restart systemd-resolved service
207
            $systemctlPath = Util::which('systemctl');
208
            Processes::mwExec("{$systemctlPath} restart systemd-resolved");
209
        } else {
210
            // Write resolv.conf content to the file
211
            file_put_contents('/etc/resolv.conf', $resolv_conf);
212
        }
213
214
        // Generate pdnsd configuration using named_dns
215
        $this->generatePdnsdConfig($named_dns);
216
    }
217
218
    /**
219
     * Retrieves the hostname and domain information.
220
     *
221
     * @return array An array containing the hostname and domain.
222
     */
223
    public static function getHostName(): array
224
    {
225
        // Initialize default hostname and domain
226
        $data = [
227
            'hostname' => 'mikopbx',
228
            'domain' => '',
229
        ];
230
231
        // Find the first LanInterfaces record with internet set to '1'
232
        /** @var LanInterfaces $res */
233
        $res = LanInterfaces::findFirst("internet = '1'");
234
235
        // If a matching record is found, update the hostname and domain
236
        if (null !== $res) {
237
            $data['hostname'] = $res->hostname;
238
            $data['domain'] = $res->domain;
239
        }
240
241
        // If the hostname is empty, set it to the default value 'mikopbx'
242
        $data['hostname'] = (empty($data['hostname'])) ? 'mikopbx' : $data['hostname'];
243
244
        return $data;
245
    }
246
247
    /**
248
     * Retrieves the DNS servers configured for the host.
249
     *
250
     * @return array An array containing the DNS servers.
251
     */
252
    public function getHostDNS(): array
253
    {
254
        $dns = [];
255
256
        // Find the first LanInterfaces record with internet set to '1'
257
        /** @var LanInterfaces $res */
258
        $res = LanInterfaces::findFirst("internet = '1'");
259
260
        // If a matching record is found, check and add primary and secondary DNS servers
261
        if (null !== $res) {
262
            // Check and add primary DNS server if not empty and not '127.0.0.1'
263
            if (!empty($res->primarydns) && '127.0.0.1' !== $res->primarydns) {
264
                $dns[] = $res->primarydns;
265
            }
266
            // Check and add secondary DNS server if not empty and not '127.0.0.1'
267
            if (!empty($res->secondarydns) && '127.0.0.1' !== $res->secondarydns) {
268
                $dns[] = $res->secondarydns;
269
            }
270
        }
271
272
        return $dns;
273
    }
274
275
    /**
276
     * Generates the pdnsd configuration file and restarts the pdnsd service if necessary.
277
     *
278
     * @param array $named_dns An array of named DNS servers.
279
     */
280
    public function generatePdnsdConfig($named_dns): void
281
    {
282
        $tempDir = $this->di->getShared('config')->path('core.tempDir');
283
        $cache_dir = $tempDir . '/pdnsd/cache';
284
        Util::mwMkdir($cache_dir);
285
286
        $conf = 'global {' . "\n" .
287
            '	perm_cache=10240;' . "\n" .
288
            '	cache_dir="' . $cache_dir . '";' . "\n" .
289
            '	pid_file = /var/run/pdnsd.pid;' . "\n" .
290
            '	run_as="nobody";' . "\n" .
291
            '	server_ip = 127.0.0.1;' . "\n" .
292
            '	status_ctl = on;' . "\n" .
293
            '	query_method=udp_tcp;' . "\n" .
294
            '	min_ttl=15m;' . "\n" .
295
            '	max_ttl=1w;' . "\n" .
296
            '	timeout=10;' . "\n" .
297
            '	neg_domain_pol=on;' . "\n" .
298
            '	run_as=root;' . "\n" .
299
            '	daemon=on;' . "\n" .
300
            '}' . "\n" .
301
            'server {' . "\n" .
302
            '	label = "main";' . "\n" .
303
            '	ip = ' . implode(', ', $named_dns) . ';' . "\n" .
304
            '	interface=lo;' . "\n" .
305
            '	uptest=if;' . "\n" .
306
            '	interval=10m;' . "\n" .
307
            '	purge_cache=off;' . "\n" .
308
            '}';
309
310
        $pdnsdConfFile = '/etc/pdnsd.conf';
311
312
        // Update the pdnsd.conf file if it has changed
313
        $savedConf = '';
314
        if (file_exists($pdnsdConfFile)) {
315
            $savedConf = file_get_contents($pdnsdConfFile);
316
        }
317
        if ($savedConf !== $conf) {
318
            file_put_contents($pdnsdConfFile, $conf);
319
        }
320
        $pdnsdPath = Util::which('pdnsd');
321
        $pid = Processes::getPidOfProcess($pdnsdPath);
322
323
        // Check if pdnsd process is running and the configuration has not changed
324
        if (!empty($pid) && $savedConf === $conf) {
325
326
            // Perform additional check if the DNS server is working
327
            $resultResolve = gethostbynamel('lic.miko.ru');
328
            if ($resultResolve !== false) {
329
                // Configuration has not changed and the DNS server is working,
330
                // no need to restart or reload the service
331
                return;
332
            }
333
            // Perform a reload of the DNS server
334
        }
335
336
        // If pdnsd process is running, terminate the process
337
        if (!empty($pid)) {
338
            $busyboxPath = Util::which('busybox');
339
            $killPath = Util::which('kill');
340
            Processes::mwExec("{$busyboxPath} {$killPath} '$pid'");
341
        }
342
343
        // Start the pdnsd service with the updated configuration
344
        Processes::mwExec("{$pdnsdPath} -c /etc/pdnsd.conf -4");
345
    }
346
347
    /**
348
     * Configures the LAN interfaces and performs related network operations.
349
     *
350
     * @return int The result of the configuration process.
351
     */
352
    public function lanConfigure(): int
353
    {
354
        if (Util::isDocker()) {
355
            return 0;
356
        }
357
358
        // Retrieve the network settings
359
        $networks = $this->getGeneralNetSettings();
360
361
        // Retrieve the paths of required commands
362
        $busyboxPath = Util::which('busybox');
363
        $vconfigPath = Util::which('vconfig');
364
        $killallPath = Util::which('killall');
365
366
        $arr_commands = [];
367
        $arr_commands[] = "{$killallPath} udhcpc";
368
        $eth_mtu = [];
369
        foreach ($networks as $if_data) {
370
            if ($if_data['disabled'] === '1') {
371
                continue;
372
            }
373
374
            $if_name = $if_data['interface'];
375
            $if_name = escapeshellcmd(trim($if_name));
376
            if (empty($if_name)) {
377
                continue;
378
            }
379
380
            $data_hostname = self::getHostName();
381
            $hostname = $data_hostname['hostname'];
382
383
            if ($if_data['vlanid'] > 0) {
384
                // Override the interface name for VLAN interfaces
385
                $arr_commands[] = "{$vconfigPath} set_name_type VLAN_PLUS_VID_NO_PAD";
386
                // Add the new VLAN interface
387
                $arr_commands[] = "{$vconfigPath} add {$if_data['interface_orign']} {$if_data['vlanid']}";
388
            }
389
            // Disable and reset the interface
390
            $arr_commands[] = "{$busyboxPath} ifconfig $if_name down";
391
            $arr_commands[] = "{$busyboxPath} ifconfig $if_name 0.0.0.0";
392
393
            $gw_param = '';
394
            if (trim($if_data['dhcp']) === '1') {
395
                // DHCP configuration
396
                /*
397
                 * -t - number of attempts.
398
                 * -T - timeout for each attempt.
399
                 * -v - enable debugging.
400
                 * -S - log messages to syslog.
401
                 * -q - exit after obtaining lease.
402
                 * -n - exit if lease is not obtained.
403
                 */
404
                $pid_file = "/var/run/udhcpc_{$if_name}";
405
                $pid_pcc = Processes::getPidOfProcess($pid_file);
406
                if (!empty($pid_pcc) && file_exists($pid_file)) {
407
                    // Terminate the old udhcpc process
408
                    $killPath = Util::which('kill');
409
                    $catPath = Util::which('cat');
410
                    system("{$killPath} `{$catPath} {$pid_file}` {$pid_pcc}");
411
                }
412
                $udhcpcPath = Util::which('udhcpc');
413
                $nohupPath = Util::which('nohup');
414
415
                // Obtain IP and wait for the process to finish
416
                $workerPath = '/etc/rc/udhcpc.configure';
417
                $options = '-t 6 -T 5 -q -n';
418
                $arr_commands[] = "{$udhcpcPath} {$options} -i {$if_name} -x hostname:{$hostname} -s {$workerPath}";
419
                // Start a new udhcpc process in the background
420
                $options = '-t 6 -T 5 -S -b -n';
421
                $arr_commands[] = "{$nohupPath} {$udhcpcPath} {$options} -p {$pid_file} -i {$if_name} -x hostname:{$hostname} -s {$workerPath} 2>&1 &";
422
                /*
423
                   udhcpc - utility for configuring the interface
424
                               - configures /etc/resolv.conf
425
                    Further route configuration will be performed in udhcpcConfigureRenewBound();
426
                    and udhcpcConfigureDeconfig(). These methods will be called by the script WorkerUdhcpcConfigure.php.
427
                    // man udhcp
428
                    // http://pwet.fr/man/linux/administration_systeme/udhcpc/
429
430
                */
431
            } else {
432
                // Static IP configuration
433
                $ipaddr = trim($if_data['ipaddr']);
434
                $subnet = trim($if_data['subnet']);
435
                $gateway = trim($if_data['gateway']);
436
                if (empty($ipaddr)) {
437
                    continue;
438
                }
439
                try {
440
                    // Calculate the short subnet mask
441
                    $calc_subnet = new SubnetCalculator($ipaddr, $subnet);
0 ignored issues
show
Bug introduced by
$subnet of type string is incompatible with the type integer expected by parameter $network_size of MikoPBX\Core\Utilities\S...lculator::__construct(). ( Ignorable by Annotation )

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

441
                    $calc_subnet = new SubnetCalculator($ipaddr, /** @scrutinizer ignore-type */ $subnet);
Loading history...
442
                    $subnet = $calc_subnet->getSubnetMask();
443
                } catch (Throwable $e) {
444
                    echo "Caught exception: $ipaddr $subnet", $e->getMessage(), "\n";
445
                    continue;
446
                }
447
448
                $ifconfigPath = Util::which('ifconfig');
449
                $arr_commands[] = "{$busyboxPath} {$ifconfigPath} $if_name $ipaddr netmask $subnet";
450
451
                if ("" !== trim($gateway)) {
452
                    $gw_param = "gw $gateway";
453
                }
454
455
                $routePath = Util::which('route');
456
                $arr_commands[] = "{$busyboxPath} {$routePath} del default $if_name";
457
458
                /** @var LanInterfaces $if_data */
459
                $if_data = LanInterfaces::findFirst("id = '{$if_data['id']}'");
460
                $is_inet = ($if_data !== null) ? (string)$if_data->internet : '0';
461
462
                if ($is_inet === '1') {
463
                    // Create default route only if the interface is for internet
464
                    $arr_commands[] = "{$busyboxPath} {$routePath} add default $gw_param dev $if_name";
465
                }
466
                // Bring up the interface
467
                $arr_commands[] = "{$busyboxPath} {$ifconfigPath} $if_name up";
468
469
                $eth_mtu[] = $if_name;
470
            }
471
        }
472
        $out = null;
473
        Processes::mwExecCommands($arr_commands, $out, 'net');
474
        $this->hostsGenerate();
475
476
        foreach ($eth_mtu as $eth) {
477
            Processes::mwExecBg("/etc/rc/networking.set.mtu '{$eth}'");
478
        }
479
480
        // Additional "manual" routes
481
        Util::fileWriteContent('/etc/static-routes', '');
482
        $arr_commands = [];
483
        $out = [];
484
        $grepPath = Util::which('grep');
485
        $awkPath = Util::which('awk');
486
        $catPath = Util::which('cat');
487
        Processes::mwExec(
488
            "{$catPath} /etc/static-routes | {$grepPath} '^rout' | {$busyboxPath} {$awkPath} -F ';' '{print $1}'",
489
            $arr_commands
490
        );
491
        Processes::mwExecCommands($arr_commands, $out, 'rout');
492
493
        $this->openVpnConfigure();
494
        return 0;
495
    }
496
497
    /**
498
     * Retrieves the general network settings and performs additional processing.
499
     *
500
     * @return array An array of network interfaces and their settings.
501
     */
502
    public function getGeneralNetSettings(): array
503
    {
504
        // Get the list of network interfaces visible to the operating system
505
        $src_array_eth = $this->getInterfacesNames();
506
507
        // Create a copy of the network interfaces array
508
        $array_eth = $src_array_eth;
509
510
        // Retrieve the LAN interface settings from the database
511
        $res = LanInterfaces::find(['order' => 'interface,vlanid']);
512
        $networks = $res->toArray();
513
514
        if (count($networks) > 0) {
515
            // Additional data processing
516
            foreach ($networks as &$if_data) {
517
                $if_data['interface_orign'] = $if_data['interface'];
518
                $if_data['interface'] = ($if_data['vlanid'] > 0) ? "vlan{$if_data['vlanid']}" : $if_data['interface'];
519
                $if_data['dhcp'] = ($if_data['vlanid'] > 0) ? 0 : $if_data['dhcp'];
520
521
                if (Verify::isIpAddress($if_data['subnet'])) {
522
                    $if_data['subnet'] = $this->netMaskToCidr($if_data['subnet']);
523
                }
524
525
                $key = array_search($if_data['interface_orign'], $src_array_eth, true);
526
                if ($key !== false) {
527
                    // Interface found
528
                    // Remove the array element if it's not a VLAN
529
                    if ($if_data['vlanid'] === '0') {
530
                        unset($array_eth[$key]);
531
                        $this->enableLanInterface($if_data['interface_orign']);
532
                    }
533
                } else {
534
                    // Interface does not exist
535
                    $this->disableLanInterface($if_data['interface_orign']);
536
                    // Disable the interface
537
                    $if_data['disabled'] = 1;
538
                }
539
            }
540
            unset($if_data);
541
        } elseif (count($array_eth) > 0) {
542
            $networks = [];
543
            // Configure the main interface
544
            $networks[] = $this->addLanInterface($array_eth[0], true);
545
            unset($array_eth[0]);
546
        }
547
        // $array_eth will contain only the elements without settings in the database
548
        // Add the "default" settings for these interfaces
549
        foreach ($array_eth as $eth) {
550
            // Add the interface and disable it
551
            $networks[] = $this->addLanInterface($eth, false);
552
        }
553
554
        // Check if there is an active internet interface, if not, set the first available interface as internet
555
        $res = LanInterfaces::findFirst("internet = '1' AND disabled='0'");
556
        if (null === $res) {
557
            /** @var LanInterfaces $eth_settings */
558
            $eth_settings = LanInterfaces::findFirst("disabled='0'");
559
            if ($eth_settings !== null) {
560
                $eth_settings->internet = 1;
561
                $eth_settings->save();
562
            }
563
        }
564
565
        return $networks;
566
    }
567
568
    /**
569
     * Converts a net mask to CIDR notation.
570
     *
571
     * @param string $net_mask The net mask to convert.
572
     * @return int The CIDR notation.
573
     */
574
    public function netMaskToCidr(string $net_mask): int
575
    {
576
        $bits = 0;
577
        $net_mask = explode(".", $net_mask);
578
579
        foreach ($net_mask as $oct_ect) {
580
            $bits += strlen(str_replace("0", "", decbin((int)$oct_ect)));
581
        }
582
583
        return $bits;
584
    }
585
586
    /**
587
     * Enables a LAN interface
588
     *
589
     * @param string $name The name of the interface to enable.
590
     * @return void
591
     */
592
    public function enableLanInterface(string $name): void
593
    {
594
        $parameters = [
595
            'conditions' => 'interface = :ifName: and disabled = :disabled:',
596
            'bind' => [
597
                'ifName' => $name,
598
                'disabled' => 1,
599
            ],
600
        ];
601
602
        $if_data = LanInterfaces::findFirst($parameters);
603
        if ($if_data !== null) {
604
            $if_data->disabled = 0;
605
            $if_data->update();
606
        }
607
    }
608
609
    /**
610
     * Disables a LAN interface by setting its internet flag to 0 and disabled flag to 1.
611
     *
612
     * @param string $name The name of the interface to disable.
613
     * @return void
614
     */
615
    public function disableLanInterface(string $name): void
616
    {
617
        $if_data = LanInterfaces::findFirst("interface = '{$name}'");
618
        if ($if_data !== null) {
619
            $if_data->internet = 0;
620
            $if_data->disabled = 1;
621
            $if_data->update();
622
        }
623
    }
624
625
    /**
626
     * Adds a LAN interface with the specified name and settings.
627
     *
628
     * @param string $name The name of the interface.
629
     * @param bool $general Flag indicating if the interface is a general interface.
630
     * @return array The array representation of the added interface.
631
     */
632
    private function addLanInterface(string $name, bool $general = false): array
633
    {
634
        $data = new LanInterfaces();
635
        $data->name = $name;
636
        $data->interface = $name;
637
        $data->dhcp = '1';
638
        $data->internet = ($general === true) ? '1' : '0';
639
        $data->disabled = '0';
640
        $data->vlanid = '0';
641
        $data->hostname = 'mikopbx';
642
        $data->domain = '';
643
        $data->topology = 'private';
644
        $data->primarydns = '';
645
        $data->save();
646
647
        return $data->toArray();
648
    }
649
650
    /**
651
     * Generates the hosts configuration.
652
     *
653
     * @return void
654
     */
655
    public function hostsGenerate(): void
656
    {
657
        $this->hostnameConfigure();
658
    }
659
660
    /**
661
     * Configures the hostname and hosts file.
662
     *
663
     * @return void
664
     */
665
    public function hostnameConfigure(): void
666
    {
667
        $data = self::getHostName();
668
        $hosts_conf = "127.0.0.1 localhost\n" .
669
            "127.0.0.1 {$data['hostname']}\n";
670
        if (!empty($data['domain'])) {
671
            $hosts_conf .= "127.0.0.1 {$data['hostname']}.{$data['domain']}\n";
672
        }
673
        $hostnamePath = Util::which('hostname');
674
        if (Util::isDocker()) {
675
            $realHostName = shell_exec($hostnamePath);
676
            $hosts_conf .= "127.0.0.1 $realHostName\n";
677
        }
678
        Util::fileWriteContent('/etc/hosts', $hosts_conf);
679
        $hostnamePath = Util::which('hostname');
680
        Processes::mwExec($hostnamePath . ' ' . escapeshellarg($data['hostname']));
681
    }
682
683
    /**
684
     * Configuring OpenVPN. If a custom configuration file is specified in the system file customization, the network will be brought up.
685
     */
686
    public function openVpnConfigure(): void
687
    {
688
        $confFile = '/etc/openvpn.ovpn';
689
        Util::fileWriteContent($confFile, '');
690
        $data = file_get_contents($confFile);
691
692
        $pidFile = '/var/run/openvpn.pid';
693
        $pid = Processes::getPidOfProcess('openvpn');
694
        if (!empty($pid)) {
695
            // Terminate the process.
696
            $busyboxPath = Util::which('busybox');
697
            Processes::mwExec("{$busyboxPath} kill '$pid'");
698
        }
699
        if (!empty($data)) {
700
            $openvpnPath = Util::which('openvpn');
701
            Processes::mwExecBg("{$openvpnPath} --config /etc/openvpn.ovpn --writepid {$pidFile}", '/dev/null', 5);
702
        }
703
    }
704
705
    /**
706
     * Configures the LAN settings inside the Docker container.
707
     *
708
     * If the environment is not Docker, this method does nothing.
709
     *
710
     * @return void
711
     */
712
    public function configureLanInDocker(): void
713
    {
714
        // Check if the environment is Docker
715
        if (!Util::isDocker()) {
716
            return;
717
        }
718
719
        // Find the path to the busybox binary
720
        $busyboxPath = Util::which('busybox');
721
722
        // Retrieve the network settings
723
        $networks = $this->getGeneralNetSettings();
724
725
        foreach ($networks as $if_data) {
726
727
            $if_name = $if_data['interface'];
728
            $if_name = escapeshellcmd(trim($if_name));
729
730
            $commands = [
731
                'subnet' => $busyboxPath . ' ifconfig eth0 | awk \'/Mask:/ {sub("Mask:", "", $NF); print $NF}\'',
732
                'ipaddr' => $busyboxPath . ' ifconfig eth0 | awk \'/inet / {sub("addr:", "", $2); print $2}\'',
733
                'gateway' => $busyboxPath . ' route -n | awk \'/^0.0.0.0/ {print $2}\'',
734
            ];
735
            $data = [];
736
            foreach ($commands as $key => $command) {
737
                $output = [];
738
                if (Processes::MWExec($command, $output) === 0) {
739
                    $value = implode("", $output);
740
                    if ($key === 'subnet') {
741
                        $data[$key] = $this->netMaskToCidr($value);
742
                    } else {
743
                        $data[$key] = $value;
744
                    }
745
                }
746
            }
747
748
            // Save information to the database.
749
            $this->updateIfSettings($data, $if_name);
750
        }
751
752
    }
753
754
    /**
755
     * Updates the interface settings with the provided data.
756
     *
757
     * @param array $data The data to update the interface settings with.
758
     * @param string $name The name of the interface.
759
     *
760
     * @return void;
761
     */
762
    public function updateIfSettings(array $data, string $name): void
763
    {
764
        /** @var LanInterfaces $res */
765
        $res = LanInterfaces::findFirst("interface = '$name' AND vlanid=0");
766
        if ($res === null || !$this->settingsIsChange($data, $res->toArray())) {
767
            return;
768
        }
769
        foreach ($data as $key => $value) {
770
            $res->writeAttribute($key, $value);
771
        }
772
        $res->save();
773
    }
774
775
    /**
776
     * Checks if the network settings have changed.
777
     *
778
     * @param array $data The new network settings.
779
     * @param array $dbData The existing network settings from the database.
780
     *
781
     * @return bool  Returns true if the settings have changed, false otherwise.
782
     */
783
    private function settingsIsChange(array $data, array $dbData): bool
784
    {
785
        $isChange = false;
786
        foreach ($dbData as $key => $value) {
787
            if (!isset($data[$key]) || (string)$value === (string)$data[$key]) {
788
                continue;
789
            }
790
            Util::sysLogMsg(__METHOD__, "Find new network settings: {$key} changed {$value}=>{$data[$key]}");
791
            $isChange = true;
792
        }
793
        return $isChange;
794
    }
795
796
    /**
797
     * Renews and configures the network settings after successful DHCP negotiation.
798
     *
799
     * @return void
800
     */
801
    public function udhcpcConfigureRenewBound(): void
802
    {
803
        // Initialize the environment variables array.
804
        $env_vars = [
805
            'broadcast' => '',
806
            'interface' => '',
807
            'ip' => '',
808
            'router' => '',
809
            'timesvr' => '',
810
            'namesvr' => '',
811
            'dns' => '',
812
            'hostname' => '',
813
            'subnet' => '',
814
            'serverid' => '',
815
            'ipttl' => '',
816
            'lease' => '',
817
            'domain' => '',
818
        ];
819
820
        $debugMode = $this->di->getShared('config')->path('core.debugMode');
821
        // Get the values of environment variables.
822
        foreach ($env_vars as $key => $value) {
823
            $env_vars[$key] = trim(getenv($key));
824
        }
825
        $BROADCAST = ($env_vars['broadcast'] === '') ? "" : "broadcast {$env_vars['broadcast']}";
826
        if ($env_vars['subnet'] === '255.255.255.255' || $env_vars['subnet'] === '') {
827
            // support /32 address assignment
828
            // https://forummikrotik.ru/viewtopic.php?f=3&t=6246&start=40
829
            $NET_MASK = '';
830
        } else {
831
            $NET_MASK = "netmask {$env_vars['subnet']}";
832
        }
833
834
        // Configure the interface.
835
        $busyboxPath = Util::which('busybox');
836
        Processes::mwExec("{$busyboxPath} ifconfig {$env_vars['interface']} {$env_vars['ip']} $BROADCAST $NET_MASK");
837
838
        // Remove old default routes.
839
        while (true) {
840
            $out = [];
841
            Processes::mwExec("route del default gw 0.0.0.0 dev {$env_vars['interface']}", $out);
842
            if (trim(implode('', $out)) !== '') {
843
                // An error occurred, indicating that all routes have been cleared.
844
                break;
845
            }
846
            if ($debugMode) {
847
                break;
848
            } // Otherwise, it will be an infinite loop.
849
        }
850
851
        // Add default routes.
852
        /** @var LanInterfaces $if_data */
853
        $if_data = LanInterfaces::findFirst("interface = '{$env_vars['interface']}'");
854
        $is_inet = ($if_data !== null) ? (int)$if_data->internet : 0;
855
        if ('' !== $env_vars['router'] && $is_inet === 1) {
856
            // Only add default route if this interface is for internet.
857
            $routers = explode(' ', $env_vars['router']);
858
            foreach ($routers as $router) {
859
                Processes::mwExec("route add default gw {$router} dev {$env_vars['interface']}");
860
            }
861
        }
862
        // Add custom routes.
863
        if (file_exists('/etc/static-routes')) {
864
            $busyboxPath = Util::which('busybox');
865
            $grepPath = Util::which('grep');
866
            $awkPath = Util::which('awk');
867
            $catPath = Util::which('cat');
868
            $shPath = Util::which('sh');
869
            Processes::mwExec(
870
                "{$catPath} /etc/static-routes | {$grepPath} '^rout' | {$busyboxPath} {$awkPath} -F ';' '{print $1}' | {$grepPath} '{$env_vars['interface']}' | {$shPath}"
871
            );
872
        }
873
        $named_dns = [];
874
        if ('' !== $env_vars['dns']) {
875
            $named_dns = explode(' ', $env_vars['dns']);
876
        }
877
        if ($is_inet === 1) {
878
            // Only generate pdnsd config if this interface is for internet.
879
            $this->generatePdnsdConfig($named_dns);
880
        }
881
882
        // Save information to the database.
883
        $data = [
884
            'subnet' => $env_vars['subnet'],
885
            'ipaddr' => $env_vars['ip'],
886
            'gateway' => $env_vars['router'],
887
        ];
888
        if (Verify::isIpAddress($env_vars['ip'])) {
889
            $data['subnet'] = $this->netMaskToCidr($env_vars['subnet']);
890
        } else {
891
            $data['subnet'] = '';
892
        }
893
        $this->updateIfSettings($data, $env_vars['interface']);
894
895
        $data = [
896
            'primarydns' => $named_dns[0] ?? '',
897
            'secondarydns' => $named_dns[1] ?? '',
898
        ];
899
        $this->updateDnsSettings($data, $env_vars['interface']);
900
901
        Processes::mwExecBg("/etc/rc/networking.set.mtu '{$env_vars['interface']}'");
902
    }
903
904
    /**
905
     * Updates the DNS settings with the provided data.
906
     *
907
     * @param array $data The data to update the DNS settings with.
908
     * @param string $name The name of the interface.
909
     *
910
     * @return void
911
     */
912
    public function updateDnsSettings($data, $name): void
913
    {
914
        /** @var LanInterfaces $res */
915
        $res = LanInterfaces::findFirst("interface = '$name' AND vlanid=0");
916
        if ($res === null || !$this->settingsIsChange($data, $res->toArray())) {
917
            return;
918
        }
919
        foreach ($data as $key => $value) {
920
            $res->writeAttribute($key, $value);
921
        }
922
        if (empty($res->primarydns) && !empty($res->secondarydns)) {
923
            // Swap primary and secondary DNS if primary is empty
924
            $res->primarydns = $res->secondarydns;
925
            $res->secondarydns = '';
926
        }
927
        $res->save();
928
    }
929
930
    /**
931
     * Renews and configures the network settings after successful DHCP negotiation using systemd environment variables.
932
     * For OS systemctl (Debian).
933
     *  Configures LAN interface FROM dhcpc (renew_bound).
934
     * @return void
935
     */
936
    public function udhcpcConfigureRenewBoundSystemCtl(): void
937
    {
938
        $prefix = "new_";
939
940
        // Initialize the environment variables array.
941
        $env_vars = [
942
            'broadcast' => 'broadcast_address',
943
            'interface' => 'interface',
944
            'ip' => 'ip_address',
945
            'router' => 'routers',
946
            'timesvr' => '',
947
            'namesvr' => 'netbios_name_servers',
948
            'dns' => 'domain_name_servers',
949
            'hostname' => 'host_name',
950
            'subnet' => 'subnet_mask',
951
            'serverid' => '',
952
            'ipttl' => '',
953
            'lease' => 'new_dhcp_lease_time',
954
            'domain' => 'domain_name',
955
        ];
956
957
        // Get the values of environment variables.
958
        foreach ($env_vars as $key => $value) {
959
            $var_name = "{$prefix}{$value}";
960
            if (empty($var_name)) {
961
                continue;
962
            }
963
            $env_vars[$key] = trim(getenv("{$prefix}{$value}"));
964
        }
965
966
        /** @var LanInterfaces $if_data */
967
        $if_data = LanInterfaces::findFirst("interface = '{$env_vars['interface']}'");
968
        $is_inet = ($if_data !== null) ? (string)$if_data->internet : '0';
969
970
        $named_dns = [];
971
        if ('' !== $env_vars['dns']) {
972
            $named_dns = explode(' ', $env_vars['dns']);
973
        }
974
        if ($is_inet === '1') {
975
            // Only generate pdnsd config if this interface is for internet.
976
            $this->generatePdnsdConfig($named_dns);
977
        }
978
979
        // Save information to the database.
980
        $data = [
981
            'subnet' => $env_vars['subnet'],
982
            'ipaddr' => $env_vars['ip'],
983
            'gateway' => $env_vars['router'],
984
        ];
985
        if (Verify::isIpAddress($env_vars['ip'])) {
986
            $data['subnet'] = $this->netMaskToCidr($env_vars['subnet']);
987
        } else {
988
            $data['subnet'] = '';
989
        }
990
        $this->updateIfSettings($data, $env_vars['interface']);
991
        $data = [
992
            'primarydns' => $named_dns[0] ?? '',
993
            'secondarydns' => $named_dns[1] ?? '',
994
        ];
995
        $this->updateDnsSettings($data, $env_vars['interface']);
996
    }
997
998
    /**
999
     * Retrieves the interface name by its ID.
1000
     *
1001
     * @param string $id_net The ID of the network interface.
1002
     *
1003
     * @return string  The interface name.
1004
     */
1005
    public function getInterfaceNameById(string $id_net): string
1006
    {
1007
        $res = LanInterfaces::findFirstById($id_net);
1008
        if ($res !== null && $res->interface !== null) {
1009
            return $res->interface;
1010
        }
1011
1012
        return '';
1013
    }
1014
1015
    /**
1016
     * Retrieves the enabled LAN interfaces.
1017
     *
1018
     * @return array  An array of enabled LAN interfaces.
1019
     */
1020
    public function getEnabledLanInterfaces(): array
1021
    {
1022
        /** @var LanInterfaces $res */
1023
        $res = LanInterfaces::find('disabled=0');
1024
1025
        return $res->toArray();
1026
    }
1027
1028
    /**
1029
     * Performs deconfiguration of the udhcpc configuration.
1030
     */
1031
    public function udhcpcConfigureDeconfig(): void
1032
    {
1033
        $interface = trim(getenv('interface'));
1034
1035
        // For MIKO LFS Edition.
1036
        $busyboxPath = Util::which('busybox');
1037
1038
        // Bring the interface up.
1039
        Processes::mwExec("{$busyboxPath} ifconfig {$interface} up");
1040
1041
        // Set a default IP configuration for the interface.
1042
        Processes::mwExec("{$busyboxPath} ifconfig {$interface} 192.168.2.1 netmask 255.255.255.0");
1043
    }
1044
1045
    /**
1046
     * Updates the network settings with the provided data.
1047
     * @param array $data The network settings data to update.
1048
     */
1049
    public function updateNetSettings(array $data): void
1050
    {
1051
        $res = LanInterfaces::findFirst("internet = '1'");
1052
        $update_inet = false;
1053
        if ($res === null) {
1054
            // If no interface with internet connection is found, get the first interface.
1055
            $res = LanInterfaces::findFirst();
1056
            $update_inet = true;
1057
        }
1058
1059
        if ($res !== null) {
1060
            foreach ($data as $key => $value) {
1061
                $res->$key = $value;
1062
            }
1063
            if ($update_inet === true) {
1064
                $res->internet = 1;
1065
            }
1066
            $res->save();
1067
        }
1068
    }
1069
1070
    /**
1071
     * Retrieves information about all network interfaces.
1072
     * @return array An array of network interfaces with their respective information.
1073
     */
1074
    public function getInterfaces(): array
1075
    {
1076
        // Get all PCI interface names (network interfaces).
1077
        $i_names = $this->getInterfacesNames();
1078
        $if_list = [];
1079
        foreach ($i_names as $i) {
1080
            $if_list[$i] = $this->getInterface($i);
1081
        }
1082
1083
        return $if_list;
1084
    }
1085
1086
    /**
1087
     * Retrieves information about a specific network interface.
1088
     * @param string $name The name of the network interface.
1089
     * @return array An array containing the interface information.
1090
     */
1091
    public function getInterface(string $name): array
1092
    {
1093
        $interface = [];
1094
1095
        // Get ifconfig's output for the specified interface.
1096
        $busyboxPath = Util::which('busybox');
1097
        Processes::mwExec("{$busyboxPath} ifconfig $name 2>/dev/null", $output);
1098
        $output = implode(" ", $output);
1099
1100
        // Parse MAC address.
1101
        preg_match("/HWaddr (\S+)/", $output, $matches);
1102
        $interface['mac'] = (count($matches) > 0) ? $matches[1] : '';
1103
1104
        // Parse IP address.
1105
        preg_match("/inet addr:(\S+)/", $output, $matches);
1106
        $interface['ipaddr'] = (count($matches) > 0) ? $matches[1] : '';
1107
1108
        // Parse subnet mask.
1109
        preg_match("/Mask:(\S+)/", $output, $matches);
1110
        $subnet = (count($matches) > 0) ? $this->netMaskToCidr($matches[1]) : '';
1111
        $interface['subnet'] = $subnet;
1112
1113
        // Check if the interface is up.
1114
        preg_match("/\s+(UP)\s+/", $output, $matches);
1115
        $status = (count($matches) > 0) ? $matches[1] : '';
1116
        if ($status === "UP") {
1117
            $interface['up'] = true;
1118
        } else {
1119
            $interface['up'] = false;
1120
        }
1121
        $busyboxPath = Util::which('busybox');
1122
1123
        // Get the default gateway.
1124
        $grepPath = Util::which('grep');
1125
        $cutPath = Util::which('cut');
1126
        $routePath = Util::which('route');
1127
1128
        Processes::mwExec(
1129
            "{$busyboxPath} {$routePath} -n | {$grepPath} {$name} | {$grepPath} \"^0.0.0.0\" | {$cutPath} -d ' ' -f 10",
1130
            $matches
1131
        );
1132
        $gw = (count($matches) > 0) ? $matches[0] : '';
1133
        if (Verify::isIpAddress($gw)) {
1134
            $interface['gateway'] = $gw;
1135
        }
1136
1137
        // Get DNS servers.
1138
        $catPath = Util::which('cat');
1139
        Processes::mwExec("{$catPath} /etc/resolv.conf | {$grepPath} nameserver | {$cutPath} -d ' ' -f 2", $dnsout);
1140
1141
        $dnsSrv = [];
1142
        foreach ($dnsout as $line) {
1143
            if (Verify::isIpAddress($line)) {
1144
                $dnsSrv[] = $line;
1145
            }
1146
        }
1147
        $interface['dns'] = $dnsSrv;
1148
1149
        return $interface;
1150
    }
1151
}