Passed
Push — develop ( 225864...73f8c7 )
by Nikolay
08:07 queued 03:40
created

Network::updateExternalIp()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 16
rs 8.8333
cc 7
nc 8
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System;
21
22
use MikoPBX\Common\Models\{LanInterfaces, PbxSettings, PbxSettingsConstants};
23
use MikoPBX\Core\Utilities\SubnetCalculator;
24
use MikoPBX\PBXCoreREST\Lib\SysinfoManagementProcessor;
25
use Phalcon\Di\Injectable;
26
use Throwable;
27
28
/**
29
 * Class Network
30
 *
31
 *
32
 *
33
 * @package MikoPBX\Core\System
34
 */
35
class Network extends Injectable
36
{
37
    public const INTERNET_FLAG_FILE = '/var/etc/internet_flag';
38
39
    /**
40
     * Starts the SIP dump process.
41
     */
42
    public static function startSipDump(): void
43
    {
44
        $config = new MikoPBXConfig();
45
        $use = $config->getGeneralSettings('USE_PCAP_SIP_DUMP');
46
        if ($use !== '1') {
47
            return;
48
        }
49
50
        Processes::killByName('pcapsipdump');
51
        $log_dir = System::getLogDir() . '/pcapsipdump';
52
        Util::mwMkdir($log_dir);
53
54
        $network = new Network();
55
        $arr_eth = $network->getInterfacesNames();
56
        $pcapsipdumpPath = Util::which('pcapsipdump');
57
        foreach ($arr_eth as $eth) {
58
            $pid_file = "/var/run/pcapsipdump_{$eth}.pid";
59
            Processes::mwExecBg(
60
                $pcapsipdumpPath . ' -T 120 -P ' . $pid_file . ' -i ' . $eth . ' -m \'^(INVITE|REGISTER)$\' -L ' . $log_dir . '/dump.db'
61
            );
62
        }
63
    }
64
65
    /**
66
     * Retrieves the names of all PCI network interfaces.
67
     *
68
     * @return array An array containing the names of the network interfaces.
69
     */
70
    public function getInterfacesNames(): array
71
    {
72
        $grepPath = Util::which('grep');
73
        $awkPath = Util::which('awk');
74
        if (Util::isDocker()) {
75
            $ifconfigPath = Util::which('ifconfig');
76
            $command = "{$ifconfigPath} | {$grepPath} -o -E '^[a-zA-Z0-9]+' | {$grepPath} -v 'lo'";
77
        } else {
78
            // Universal command to retrieve all PCI network interfaces.
79
            $lsPath = Util::which('ls');
80
            $command = "{$lsPath} -l /sys/class/net | {$grepPath} devices | {$grepPath} -v virtual | {$awkPath} '{ print $9 }'";
81
        }
82
        Processes::mwExec($command, $names);
83
        return $names;
84
    }
85
86
    /**
87
     * Retrieves the information message containing available web interface addresses.
88
     *
89
     * @return string The information message.
90
     */
91
    public static function getInfoMessage(): string
92
    {
93
        $addresses = [
94
            'local' => [],
95
            'external' => []
96
        ];
97
        /** @var LanInterfaces $interface */
98
        $interfaces = LanInterfaces::find("disabled='0'");
99
        foreach ($interfaces as $interface) {
100
            if (!empty($interface->ipaddr)) {
101
                $addresses['local'][] = $interface->ipaddr;
102
            }
103
            if (!empty($interface->exthostname) && !in_array($interface->exthostname, $addresses['local'], true)) {
104
                $addresses['external'][] = explode(':', $interface->exthostname)[0] ?? '';
105
            }
106
            if (!empty($interface->extipaddr) && !in_array($interface->extipaddr, $addresses['local'], true)) {
107
                $addresses['external'][] = explode(':', $interface->extipaddr)[0] ?? '';
108
            }
109
        }
110
        unset($interfaces);
111
112
        // Local network
113
        $port = PbxSettings::getValueByKey(PbxSettingsConstants::WEB_HTTPS_PORT);
114
        $info = PHP_EOL . "   The web interface is available at the local net addresses:" . PHP_EOL . PHP_EOL;
115
        foreach ($addresses['local'] as $address) {
116
            if (empty($address)) {
117
                continue;
118
            }
119
            $info .= "    - https://$address:$port" . PHP_EOL;
120
        }
121
        $info .= PHP_EOL;
122
123
        // External web address info
124
        if (!empty($addresses['external'])) {
125
            $info .= "   The web interface is available at the external addresses:" . PHP_EOL . PHP_EOL;
126
            foreach ($addresses['external'] as $address) {
127
                if (empty($address)) {
128
                    continue;
129
                }
130
                $info .= "    - https://$address:$port" . PHP_EOL;
131
            }
132
            $info .= PHP_EOL;
133
        }
134
135
        // Default web user info
136
        $cloudInstanceId = PbxSettings::getValueByKey(PbxSettingsConstants::CLOUD_INSTANCE_ID);
137
        $webAdminPassword = PbxSettings::getValueByKey(PbxSettingsConstants::WEB_ADMIN_PASSWORD);
138
        if ($cloudInstanceId === $webAdminPassword) {
139
            $adminUser = PbxSettings::getValueByKey(PbxSettingsConstants::WEB_ADMIN_LOGIN);
140
            $info .= "  You should use the next credentials:" . PHP_EOL . PHP_EOL;
141
            $info .= "      Login: $adminUser" . PHP_EOL . PHP_EOL;
142
            $info .= "      Password: $cloudInstanceId" . PHP_EOL . PHP_EOL;
143
        }
144
145
        return $info;
146
    }
147
148
    /**
149
     * Configures the loopback interface (lo) with the IP address 127.0.0.1.
150
     *
151
     * @return void
152
     */
153
    public function loConfigure(): void
154
    {
155
        $busyboxPath = Util::which('busybox');
156
        $ifconfigPath = Util::which('ifconfig');
157
        Processes::mwExec("{$busyboxPath} {$ifconfigPath} lo 127.0.0.1");
158
    }
159
160
    /**
161
     * Generates the resolv.conf file based on system configuration.
162
     */
163
    public function resolvConfGenerate(): void
164
    {
165
        if (Util::isDocker()) {
166
            return;
167
        }
168
169
        // Initialize resolv.conf content
170
        $resolv_conf = '';
171
172
        // Get hostname information
173
        $data_hostname = self::getHostName();
174
175
        // Append domain to resolv.conf if it is not empty
176
        if (trim($data_hostname['domain']) !== '') {
177
            $resolv_conf .= "domain {$data_hostname['domain']}\n";
178
        }
179
180
        // Append local nameserver to resolv.conf
181
        $resolv_conf .= "nameserver 127.0.0.1\n";
182
183
        // Initialize an array to store named DNS servers
184
        $named_dns = [];
185
186
        // Retrieve host DNS settings
187
        $dns = $this->getHostDNS();
188
189
        // Iterate over each DNS server
190
        foreach ($dns as $ns) {
191
            // Skip empty DNS servers
192
            if (trim($ns) === '') {
193
                continue;
194
            }
195
            // Add the DNS server to the named_dns array
196
            $named_dns[] = $ns;
197
198
            // Append the DNS server to resolv.conf
199
            $resolv_conf .= "nameserver {$ns}\n";
200
        }
201
202
        // If no DNS servers were found, use default ones and add them to named_dns
203
        if (count($dns) === 0) {
204
            $resolv_conf .= "nameserver 4.4.4.4\n";
205
            $named_dns[] .= "8.8.8.8";
206
        }
207
208
        // Check if systemctl is available
209
        if (Util::isSystemctl()) {
210
211
            // Generate resolved.conf content for systemd-resolved
212
            $s_resolv_conf = "[Resolve]\n"
213
                . "DNS=127.0.0.1\n";
214
215
            // Append domain to resolved.conf if it is not empty
216
            if (trim($data_hostname['domain']) !== '') {
217
                $s_resolv_conf .= "Domains={$data_hostname['domain']}\n";
218
            }
219
220
            // Write resolved.conf content to the file
221
            file_put_contents('/etc/systemd/resolved.conf', $s_resolv_conf);
222
223
            // Restart systemd-resolved service
224
            $systemctlPath = Util::which('systemctl');
225
            Processes::mwExec("{$systemctlPath} restart systemd-resolved");
226
        } else {
227
            // Write resolv.conf content to the file
228
            file_put_contents('/etc/resolv.conf', $resolv_conf);
229
        }
230
231
        // Generate pdnsd configuration using named_dns
232
        $this->generatePdnsdConfig($named_dns);
233
    }
234
235
    /**
236
     * Retrieves the hostname and domain information.
237
     *
238
     * @return array An array containing the hostname and domain.
239
     */
240
    public static function getHostName(): array
241
    {
242
        // Initialize default hostname and domain
243
        $data = [
244
            'hostname' => 'mikopbx',
245
            'domain' => '',
246
        ];
247
248
        // Find the first LanInterfaces record with internet set to '1'
249
        /** @var LanInterfaces $res */
250
        $res = LanInterfaces::findFirst("internet = '1'");
251
252
        // If a matching record is found, update the hostname and domain
253
        if (null !== $res) {
254
            $data['hostname'] = $res->hostname;
255
            $data['domain'] = $res->domain;
256
        }
257
258
        // If the hostname is empty, set it to the default value 'mikopbx'
259
        $data['hostname'] = (empty($data['hostname'])) ? 'mikopbx' : $data['hostname'];
260
261
        return $data;
262
    }
263
264
    /**
265
     * Retrieves the DNS servers configured for the host.
266
     *
267
     * @return array An array containing the DNS servers.
268
     */
269
    public function getHostDNS(): array
270
    {
271
        $dns = [];
272
273
        // Find the first LanInterfaces record with internet set to '1'
274
        /** @var LanInterfaces $res */
275
        $res = LanInterfaces::findFirst("internet = '1'");
276
277
        // If a matching record is found, check and add primary and secondary DNS servers
278
        if (null !== $res) {
279
            // Check and add primary DNS server if not empty and not '127.0.0.1'
280
            if (!empty($res->primarydns) && '127.0.0.1' !== $res->primarydns) {
281
                $dns[] = $res->primarydns;
282
            }
283
            // Check and add secondary DNS server if not empty and not '127.0.0.1'
284
            if (!empty($res->secondarydns) && '127.0.0.1' !== $res->secondarydns) {
285
                $dns[] = $res->secondarydns;
286
            }
287
        }
288
289
        return $dns;
290
    }
291
292
    /**
293
     * Generates the pdnsd configuration file and restarts the pdnsd service if necessary.
294
     *
295
     * @param array $named_dns An array of named DNS servers.
296
     */
297
    public function generatePdnsdConfig($named_dns): void
298
    {
299
        $tempDir = $this->di->getShared('config')->path('core.tempDir');
300
        $cache_dir = $tempDir . '/pdnsd/cache';
301
        Util::mwMkdir($cache_dir);
302
303
        $conf = 'global {' . "\n" .
304
            '	perm_cache=10240;' . "\n" .
305
            '	cache_dir="' . $cache_dir . '";' . "\n" .
306
            '	pid_file = /var/run/pdnsd.pid;' . "\n" .
307
            '	run_as="nobody";' . "\n" .
308
            '	server_ip = 127.0.0.1;' . "\n" .
309
            '	status_ctl = on;' . "\n" .
310
            '	query_method=udp_tcp;' . "\n" .
311
            '	min_ttl=15m;' . "\n" .
312
            '	max_ttl=1w;' . "\n" .
313
            '	timeout=10;' . "\n" .
314
            '	neg_domain_pol=on;' . "\n" .
315
            '	run_as=root;' . "\n" .
316
            '	daemon=on;' . "\n" .
317
            '}' . "\n" .
318
            'server {' . "\n" .
319
            '	label = "main";' . "\n" .
320
            '	ip = ' . implode(', ', $named_dns) . ';' . "\n" .
321
            '	interface=lo;' . "\n" .
322
            '	uptest=if;' . "\n" .
323
            '	interval=10m;' . "\n" .
324
            '	purge_cache=off;' . "\n" .
325
            '}';
326
327
        $pdnsdConfFile = '/etc/pdnsd.conf';
328
329
        // Update the pdnsd.conf file if it has changed
330
        $savedConf = '';
331
        if (file_exists($pdnsdConfFile)) {
332
            $savedConf = file_get_contents($pdnsdConfFile);
333
        }
334
        if ($savedConf !== $conf) {
335
            file_put_contents($pdnsdConfFile, $conf);
336
        }
337
        $pdnsdPath = Util::which('pdnsd');
338
        $pid = Processes::getPidOfProcess($pdnsdPath);
339
340
        // Check if pdnsd process is running and the configuration has not changed
341
        if (!empty($pid) && $savedConf === $conf) {
342
343
            // Perform additional check if the DNS server is working
344
            $resultResolve = gethostbynamel('lic.miko.ru');
345
            if ($resultResolve !== false) {
346
                // Configuration has not changed and the DNS server is working,
347
                // no need to restart or reload the service
348
                return;
349
            }
350
            // Perform a reload of the DNS server
351
        }
352
353
        // If pdnsd process is running, terminate the process
354
        if (!empty($pid)) {
355
            $busyboxPath = Util::which('busybox');
356
            $killPath = Util::which('kill');
357
            Processes::mwExec("{$busyboxPath} {$killPath} '$pid'");
358
        }
359
360
        // Start the pdnsd service with the updated configuration
361
        Processes::mwExec("{$pdnsdPath} -c /etc/pdnsd.conf -4");
362
    }
363
364
    /**
365
     * Configures the LAN interfaces and performs related network operations.
366
     *
367
     * @return int The result of the configuration process.
368
     */
369
    public function lanConfigure(): int
370
    {
371
        if (Util::isDocker()) {
372
            return 0;
373
        }
374
375
        // Retrieve the network settings
376
        $networks = $this->getGeneralNetSettings();
377
378
        // Retrieve the paths of required commands
379
        $busyboxPath = Util::which('busybox');
380
        $vconfigPath = Util::which('vconfig');
381
        $killallPath = Util::which('killall');
382
383
        $arr_commands = [];
384
        $arr_commands[] = "{$killallPath} udhcpc";
385
        $eth_mtu = [];
386
        foreach ($networks as $if_data) {
387
            if ($if_data['disabled'] === '1') {
388
                continue;
389
            }
390
391
            $if_name = $if_data['interface'];
392
            $if_name = escapeshellcmd(trim($if_name));
393
            if (empty($if_name)) {
394
                continue;
395
            }
396
397
            $data_hostname = self::getHostName();
398
            $hostname = $data_hostname['hostname'];
399
400
            if ($if_data['vlanid'] > 0) {
401
                // Override the interface name for VLAN interfaces
402
                $arr_commands[] = "{$vconfigPath} set_name_type VLAN_PLUS_VID_NO_PAD";
403
                // Add the new VLAN interface
404
                $arr_commands[] = "{$vconfigPath} add {$if_data['interface_orign']} {$if_data['vlanid']}";
405
            }
406
            // Disable and reset the interface
407
            $arr_commands[] = "{$busyboxPath} ifconfig $if_name down";
408
            $arr_commands[] = "{$busyboxPath} ifconfig $if_name 0.0.0.0";
409
410
            $gw_param = '';
411
            if (trim($if_data['dhcp']) === '1') {
412
                // DHCP configuration
413
                /*
414
                 * -t - number of attempts.
415
                 * -T - timeout for each attempt.
416
                 * -v - enable debugging.
417
                 * -S - log messages to syslog.
418
                 * -q - exit after obtaining lease.
419
                 * -n - exit if lease is not obtained.
420
                 */
421
                $pid_file = "/var/run/udhcpc_{$if_name}";
422
                $pid_pcc = Processes::getPidOfProcess($pid_file);
423
                if (!empty($pid_pcc) && file_exists($pid_file)) {
424
                    // Terminate the old udhcpc process
425
                    $killPath = Util::which('kill');
426
                    $catPath = Util::which('cat');
427
                    system("{$killPath} `{$catPath} {$pid_file}` {$pid_pcc}");
428
                }
429
                $udhcpcPath = Util::which('udhcpc');
430
                $nohupPath = Util::which('nohup');
431
432
                // Obtain IP and wait for the process to finish
433
                $workerPath = '/etc/rc/udhcpc.configure';
434
                $options = '-t 6 -T 5 -q -n';
435
                $arr_commands[] = "{$udhcpcPath} {$options} -i {$if_name} -x hostname:{$hostname} -s {$workerPath}";
436
                // Start a new udhcpc process in the background
437
                $options = '-t 6 -T 5 -S -b -n';
438
                $arr_commands[] = "{$nohupPath} {$udhcpcPath} {$options} -p {$pid_file} -i {$if_name} -x hostname:{$hostname} -s {$workerPath} 2>&1 &";
439
                /*
440
                   udhcpc - utility for configuring the interface
441
                               - configures /etc/resolv.conf
442
                    Further route configuration will be performed in udhcpcConfigureRenewBound();
443
                    and udhcpcConfigureDeconfig(). These methods will be called by the script WorkerUdhcpcConfigure.php.
444
                    // man udhcp
445
                    // http://pwet.fr/man/linux/administration_systeme/udhcpc/
446
447
                */
448
            } else {
449
                // Static IP configuration
450
                $ipaddr = trim($if_data['ipaddr']);
451
                $subnet = trim($if_data['subnet']);
452
                $gateway = trim($if_data['gateway']);
453
                if (empty($ipaddr)) {
454
                    continue;
455
                }
456
                try {
457
                    // Calculate the short subnet mask
458
                    $calc_subnet = new SubnetCalculator($ipaddr, $subnet);
459
                    $subnet = $calc_subnet->getSubnetMask();
460
                } catch (Throwable $e) {
461
                    echo "Caught exception: $ipaddr $subnet", $e->getMessage(), "\n";
462
                    continue;
463
                }
464
465
                $ifconfigPath = Util::which('ifconfig');
466
                $arr_commands[] = "{$busyboxPath} {$ifconfigPath} $if_name $ipaddr netmask $subnet";
467
468
                if ("" !== trim($gateway)) {
469
                    $gw_param = "gw $gateway";
470
                }
471
472
                $routePath = Util::which('route');
473
                $arr_commands[] = "{$busyboxPath} {$routePath} del default $if_name";
474
475
                /** @var LanInterfaces $if_data */
476
                $if_data = LanInterfaces::findFirst("id = '{$if_data['id']}'");
477
                $is_inet = ($if_data !== null) ? (string)$if_data->internet : '0';
478
479
                if ($is_inet === '1') {
480
                    // Create default route only if the interface is for internet
481
                    $arr_commands[] = "{$busyboxPath} {$routePath} add default $gw_param dev $if_name";
482
                }
483
                // Bring up the interface
484
                $arr_commands[] = "{$busyboxPath} {$ifconfigPath} $if_name up";
485
486
                $eth_mtu[] = $if_name;
487
            }
488
        }
489
        $out = null;
490
        Processes::mwExecCommands($arr_commands, $out, 'net');
491
        $this->hostsGenerate();
492
493
        foreach ($eth_mtu as $eth) {
494
            Processes::mwExecBg("/etc/rc/networking.set.mtu '{$eth}'");
495
        }
496
497
        // Additional "manual" routes
498
        Util::fileWriteContent('/etc/static-routes', '');
499
        $arr_commands = [];
500
        $out = [];
501
        $grepPath = Util::which('grep');
502
        $awkPath = Util::which('awk');
503
        $catPath = Util::which('cat');
504
        Processes::mwExec(
505
            "{$catPath} /etc/static-routes | {$grepPath} '^rout' | {$busyboxPath} {$awkPath} -F ';' '{print $1}'",
506
            $arr_commands
507
        );
508
        Processes::mwExecCommands($arr_commands, $out, 'rout');
509
510
        $this->openVpnConfigure();
511
        return 0;
512
    }
513
514
    /**
515
     * Retrieves the general network settings and performs additional processing.
516
     *
517
     * @return array An array of network interfaces and their settings.
518
     */
519
    public function getGeneralNetSettings(): array
520
    {
521
        // Get the list of network interfaces visible to the operating system
522
        $src_array_eth = $this->getInterfacesNames();
523
524
        // Create a copy of the network interfaces array
525
        $array_eth = $src_array_eth;
526
527
        // Retrieve the LAN interface settings from the database
528
        $res = LanInterfaces::find(['order' => 'interface,vlanid']);
529
        $networks = $res->toArray();
530
531
        if (count($networks) > 0) {
532
            // Additional data processing
533
            foreach ($networks as &$if_data) {
534
                $if_data['interface_orign'] = $if_data['interface'];
535
                $if_data['interface'] = ($if_data['vlanid'] > 0) ? "vlan{$if_data['vlanid']}" : $if_data['interface'];
536
                $if_data['dhcp'] = ($if_data['vlanid'] > 0) ? 0 : $if_data['dhcp'];
537
538
                if (Verify::isIpAddress($if_data['subnet'])) {
539
                    $if_data['subnet'] = $this->netMaskToCidr($if_data['subnet']);
540
                }
541
542
                $key = array_search($if_data['interface_orign'], $src_array_eth, true);
543
                if ($key !== false) {
544
                    // Interface found
545
                    // Remove the array element if it's not a VLAN
546
                    if ($if_data['vlanid'] === '0') {
547
                        unset($array_eth[$key]);
548
                        $this->enableLanInterface($if_data['interface_orign']);
549
                    }
550
                } else {
551
                    // Interface does not exist
552
                    $this->disableLanInterface($if_data['interface_orign']);
553
                    // Disable the interface
554
                    $if_data['disabled'] = 1;
555
                }
556
            }
557
            unset($if_data);
558
        } elseif (count($array_eth) > 0) {
559
            $networks = [];
560
            // Configure the main interface
561
            $networks[] = $this->addLanInterface($array_eth[0], true);
562
            unset($array_eth[0]);
563
        }
564
        // $array_eth will contain only the elements without settings in the database
565
        // Add the "default" settings for these interfaces
566
        foreach ($array_eth as $eth) {
567
            // Add the interface and disable it
568
            $networks[] = $this->addLanInterface($eth, false);
569
        }
570
571
        // Check if there is an active internet interface, if not, set the first available interface as internet
572
        $res = LanInterfaces::findFirst("internet = '1' AND disabled='0'");
573
        if (null === $res) {
574
            /** @var LanInterfaces $eth_settings */
575
            $eth_settings = LanInterfaces::findFirst("disabled='0'");
576
            if ($eth_settings !== null) {
577
                $eth_settings->internet = 1;
578
                $eth_settings->save();
579
            }
580
        }
581
582
        return $networks;
583
    }
584
585
    /**
586
     * Converts a net mask to CIDR notation.
587
     *
588
     * @param string $net_mask The net mask to convert.
589
     * @return int The CIDR notation.
590
     */
591
    public function netMaskToCidr(string $net_mask): int
592
    {
593
        $bits = 0;
594
        $net_mask = explode(".", $net_mask);
595
596
        foreach ($net_mask as $oct_ect) {
597
            $bits += strlen(str_replace("0", "", decbin((int)$oct_ect)));
598
        }
599
600
        return $bits;
601
    }
602
603
    /**
604
     * Enables a LAN interface
605
     *
606
     * @param string $name The name of the interface to enable.
607
     * @return void
608
     */
609
    public function enableLanInterface(string $name): void
610
    {
611
        $parameters = [
612
            'conditions' => 'interface = :ifName: and disabled = :disabled:',
613
            'bind' => [
614
                'ifName' => $name,
615
                'disabled' => 1,
616
            ],
617
        ];
618
619
        $if_data = LanInterfaces::findFirst($parameters);
620
        if ($if_data !== null) {
621
            $if_data->disabled = 0;
622
            $if_data->update();
623
        }
624
    }
625
626
    /**
627
     * Disables a LAN interface by setting its internet flag to 0 and disabled flag to 1.
628
     *
629
     * @param string $name The name of the interface to disable.
630
     * @return void
631
     */
632
    public function disableLanInterface(string $name): void
633
    {
634
        $if_data = LanInterfaces::findFirst("interface = '{$name}'");
635
        if ($if_data !== null) {
636
            $if_data->internet = 0;
637
            $if_data->disabled = 1;
638
            $if_data->update();
639
        }
640
    }
641
642
    /**
643
     * Adds a LAN interface with the specified name and settings.
644
     *
645
     * @param string $name The name of the interface.
646
     * @param bool $general Flag indicating if the interface is a general interface.
647
     * @return array The array representation of the added interface.
648
     */
649
    private function addLanInterface(string $name, bool $general = false): array
650
    {
651
        $data = new LanInterfaces();
652
        $data->name = $name;
653
        $data->interface = $name;
654
        $data->dhcp = '1';
655
        $data->internet = ($general === true) ? '1' : '0';
656
        $data->disabled = '0';
657
        $data->vlanid = '0';
658
        $data->hostname = 'MikoPBX';
659
        $data->domain = '';
660
        $data->topology = LanInterfaces::TOPOLOGY_PRIVATE;
661
        $data->autoUpdateExtIp = '1';
662
        $data->primarydns = '';
663
        $data->save();
664
665
        return $data->toArray();
666
    }
667
668
    /**
669
     * Generates the hosts configuration.
670
     *
671
     * @return void
672
     */
673
    public function hostsGenerate(): void
674
    {
675
        $this->hostnameConfigure();
676
    }
677
678
    /**
679
     * Configures the hostname and hosts file.
680
     *
681
     * @return void
682
     */
683
    public function hostnameConfigure(): void
684
    {
685
        $data = self::getHostName();
686
        $hosts_conf = "127.0.0.1 localhost\n" .
687
            "127.0.0.1 {$data['hostname']}\n";
688
        if (!empty($data['domain'])) {
689
            $hosts_conf .= "127.0.0.1 {$data['hostname']}.{$data['domain']}\n";
690
        }
691
        $hostnamePath = Util::which('hostname');
692
        if (Util::isDocker()) {
693
            $realHostName = shell_exec($hostnamePath);
694
            $hosts_conf .= "127.0.0.1 $realHostName\n";
695
        }
696
        Util::fileWriteContent('/etc/hosts', $hosts_conf);
697
        Processes::mwExec($hostnamePath . ' ' . escapeshellarg($data['hostname']));
698
    }
699
700
    /**
701
     * Configuring OpenVPN. If a custom configuration file is specified in the system file customization, the network will be brought up.
702
     */
703
    public function openVpnConfigure(): void
704
    {
705
        $confFile = '/etc/openvpn.ovpn';
706
        Util::fileWriteContent($confFile, '');
707
        $data = file_get_contents($confFile);
708
709
        $pidFile = '/var/run/openvpn.pid';
710
        $pid = Processes::getPidOfProcess('openvpn');
711
        if (!empty($pid)) {
712
            // Terminate the process.
713
            $busyboxPath = Util::which('busybox');
714
            Processes::mwExec("{$busyboxPath} kill '$pid'");
715
        }
716
        if (!empty($data)) {
717
            $openvpnPath = Util::which('openvpn');
718
            Processes::mwExecBg("{$openvpnPath} --config /etc/openvpn.ovpn --writepid {$pidFile}", '/dev/null', 5);
719
        }
720
    }
721
722
    /**
723
     * Configures the LAN settings inside the Docker container.
724
     *
725
     * If the environment is not Docker, this method does nothing.
726
     *
727
     * @return void
728
     */
729
    public function configureLanInDocker(): void
730
    {
731
        // Check if the environment is Docker
732
        if (!Util::isDocker()) {
733
            return;
734
        }
735
736
        // Find the path to the busybox binary
737
        $busyboxPath = Util::which('busybox');
738
739
        // Retrieve the network settings
740
        $networks = $this->getGeneralNetSettings();
741
742
        foreach ($networks as $if_data) {
743
744
            $if_name = $if_data['interface'];
745
            $if_name = escapeshellcmd(trim($if_name));
746
747
            $commands = [
748
                'subnet' => $busyboxPath . ' ifconfig eth0 | awk \'/Mask:/ {sub("Mask:", "", $NF); print $NF}\'',
749
                'ipaddr' => $busyboxPath . ' ifconfig eth0 | awk \'/inet / {sub("addr:", "", $2); print $2}\'',
750
                'gateway' => $busyboxPath . ' route -n | awk \'/^0.0.0.0/ {print $2}\'',
751
                'hostname' => $busyboxPath . ' hostname',
752
            ];
753
            $data = [];
754
            foreach ($commands as $key => $command) {
755
                $output = [];
756
                if (Processes::MWExec($command, $output) === 0) {
757
                    $value = implode("", $output);
758
                    if ($key === 'subnet') {
759
                        $data[$key] = $this->netMaskToCidr($value);
760
                    } else {
761
                        $data[$key] = $value;
762
                    }
763
                }
764
            }
765
766
            // Save information to the database.
767
            $this->updateIfSettings($data, $if_name);
768
        }
769
770
    }
771
772
    /**
773
     * Updates the interface settings with the provided data.
774
     *
775
     * @param array $data The data to update the interface settings with.
776
     * @param string $name The name of the interface.
777
     *
778
     * @return void;
779
     */
780
    public function updateIfSettings(array $data, string $name): void
781
    {
782
        /** @var LanInterfaces $res */
783
        $res = LanInterfaces::findFirst("interface = '$name' AND vlanid=0");
784
        if ($res === null || !$this->settingsIsChange($data, $res->toArray())) {
785
            return;
786
        }
787
        foreach ($data as $key => $value) {
788
            $res->writeAttribute($key, $value);
789
        }
790
        $res->save();
791
    }
792
793
    /**
794
     * Checks if the network settings have changed.
795
     *
796
     * @param array $data The new network settings.
797
     * @param array $dbData The existing network settings from the database.
798
     *
799
     * @return bool  Returns true if the settings have changed, false otherwise.
800
     */
801
    private function settingsIsChange(array $data, array $dbData): bool
802
    {
803
        $isChange = false;
804
        foreach ($dbData as $key => $value) {
805
            if (!isset($data[$key]) || (string)$value === (string)$data[$key]) {
806
                continue;
807
            }
808
            Util::sysLogMsg(__METHOD__, "Find new network settings: {$key} changed {$value}=>{$data[$key]}");
809
            $isChange = true;
810
        }
811
        return $isChange;
812
    }
813
814
    /**
815
     * Renews and configures the network settings after successful DHCP negotiation.
816
     *
817
     * @return void
818
     */
819
    public function udhcpcConfigureRenewBound(): void
820
    {
821
        // Initialize the environment variables array.
822
        $env_vars = [
823
            'broadcast' => '',
824
            'interface' => '',
825
            'ip' => '',
826
            'router' => '',
827
            'timesvr' => '',
828
            'namesvr' => '',
829
            'dns' => '',
830
            'hostname' => '',
831
            'subnet' => '',
832
            'serverid' => '',
833
            'ipttl' => '',
834
            'lease' => '',
835
            'domain' => '',
836
        ];
837
838
        $debugMode = $this->di->getShared('config')->path('core.debugMode');
839
        // Get the values of environment variables.
840
        foreach ($env_vars as $key => $value) {
841
            $env_vars[$key] = trim(getenv($key));
842
        }
843
        $BROADCAST = ($env_vars['broadcast'] === '') ? "" : "broadcast {$env_vars['broadcast']}";
844
        if ($env_vars['subnet'] === '255.255.255.255' || $env_vars['subnet'] === '') {
845
            // support /32 address assignment
846
            // https://forummikrotik.ru/viewtopic.php?f=3&t=6246&start=40
847
            $NET_MASK = '';
848
        } else {
849
            $NET_MASK = "netmask {$env_vars['subnet']}";
850
        }
851
852
        // Configure the interface.
853
        $busyboxPath = Util::which('busybox');
854
        Processes::mwExec("{$busyboxPath} ifconfig {$env_vars['interface']} {$env_vars['ip']} $BROADCAST $NET_MASK");
855
856
        // Remove old default routes.
857
        while (true) {
858
            $out = [];
859
            Processes::mwExec("route del default gw 0.0.0.0 dev {$env_vars['interface']}", $out);
860
            if (trim(implode('', $out)) !== '') {
861
                // An error occurred, indicating that all routes have been cleared.
862
                break;
863
            }
864
            if ($debugMode) {
865
                break;
866
            } // Otherwise, it will be an infinite loop.
867
        }
868
869
        // Add default routes.
870
        /** @var LanInterfaces $if_data */
871
        $if_data = LanInterfaces::findFirst("interface = '{$env_vars['interface']}'");
872
        $is_inet = ($if_data !== null) ? (int)$if_data->internet : 0;
873
        if ('' !== $env_vars['router'] && $is_inet === 1) {
874
            // Only add default route if this interface is for internet.
875
            $routers = explode(' ', $env_vars['router']);
876
            foreach ($routers as $router) {
877
                Processes::mwExec("route add default gw {$router} dev {$env_vars['interface']}");
878
            }
879
        }
880
        // Add custom routes.
881
        if (file_exists('/etc/static-routes')) {
882
            $busyboxPath = Util::which('busybox');
883
            $grepPath = Util::which('grep');
884
            $awkPath = Util::which('awk');
885
            $catPath = Util::which('cat');
886
            $shPath = Util::which('sh');
887
            Processes::mwExec(
888
                "{$catPath} /etc/static-routes | {$grepPath} '^rout' | {$busyboxPath} {$awkPath} -F ';' '{print $1}' | {$grepPath} '{$env_vars['interface']}' | {$shPath}"
889
            );
890
        }
891
        $named_dns = [];
892
        if ('' !== $env_vars['dns']) {
893
            $named_dns = explode(' ', $env_vars['dns']);
894
        }
895
        if ($is_inet === 1) {
896
            // Only generate pdnsd config if this interface is for internet.
897
            $this->generatePdnsdConfig($named_dns);
898
        }
899
900
        // Save information to the database.
901
        $data = [
902
            'subnet' => $env_vars['subnet'],
903
            'ipaddr' => $env_vars['ip'],
904
            'gateway' => $env_vars['router'],
905
        ];
906
        if (Verify::isIpAddress($env_vars['ip'])) {
907
            $data['subnet'] = $this->netMaskToCidr($env_vars['subnet']);
908
        } else {
909
            $data['subnet'] = '';
910
        }
911
        $this->updateIfSettings($data, $env_vars['interface']);
912
913
        $data = [
914
            'primarydns' => $named_dns[0] ?? '',
915
            'secondarydns' => $named_dns[1] ?? '',
916
        ];
917
        $this->updateDnsSettings($data, $env_vars['interface']);
918
919
        Processes::mwExecBg("/etc/rc/networking.set.mtu '{$env_vars['interface']}'");
920
    }
921
922
    /**
923
     * Updates the DNS settings with the provided data.
924
     *
925
     * @param array $data The data to update the DNS settings with.
926
     * @param string $name The name of the interface.
927
     *
928
     * @return void
929
     */
930
    public function updateDnsSettings($data, $name): void
931
    {
932
        /** @var LanInterfaces $res */
933
        $res = LanInterfaces::findFirst("interface = '$name' AND vlanid=0");
934
        if ($res === null || !$this->settingsIsChange($data, $res->toArray())) {
935
            return;
936
        }
937
        foreach ($data as $key => $value) {
938
            $res->writeAttribute($key, $value);
939
        }
940
        if (empty($res->primarydns) && !empty($res->secondarydns)) {
941
            // Swap primary and secondary DNS if primary is empty
942
            $res->primarydns = $res->secondarydns;
943
            $res->secondarydns = '';
944
        }
945
        $res->save();
946
    }
947
948
    /**
949
     * Renews and configures the network settings after successful DHCP negotiation using systemd environment variables.
950
     * For OS systemctl (Debian).
951
     *  Configures LAN interface FROM dhcpc (renew_bound).
952
     * @return void
953
     */
954
    public function udhcpcConfigureRenewBoundSystemCtl(): void
955
    {
956
        $prefix = "new_";
957
958
        // Initialize the environment variables array.
959
        $env_vars = [
960
            'broadcast' => 'broadcast_address',
961
            'interface' => 'interface',
962
            'ip' => 'ip_address',
963
            'router' => 'routers',
964
            'timesvr' => '',
965
            'namesvr' => 'netbios_name_servers',
966
            'dns' => 'domain_name_servers',
967
            'hostname' => 'host_name',
968
            'subnet' => 'subnet_mask',
969
            'serverid' => '',
970
            'ipttl' => '',
971
            'lease' => 'new_dhcp_lease_time',
972
            'domain' => 'domain_name',
973
        ];
974
975
        // Get the values of environment variables.
976
        foreach ($env_vars as $key => $value) {
977
            $var_name = "{$prefix}{$value}";
978
            if (empty($var_name)) {
979
                continue;
980
            }
981
            $env_vars[$key] = trim(getenv("{$prefix}{$value}"));
982
        }
983
984
        /** @var LanInterfaces $if_data */
985
        $if_data = LanInterfaces::findFirst("interface = '{$env_vars['interface']}'");
986
        $is_inet = ($if_data !== null) ? (string)$if_data->internet : '0';
987
988
        $named_dns = [];
989
        if ('' !== $env_vars['dns']) {
990
            $named_dns = explode(' ', $env_vars['dns']);
991
        }
992
        if ($is_inet === '1') {
993
            // Only generate pdnsd config if this interface is for internet.
994
            $this->generatePdnsdConfig($named_dns);
995
        }
996
997
        // Save information to the database.
998
        $data = [
999
            'subnet' => $env_vars['subnet'],
1000
            'ipaddr' => $env_vars['ip'],
1001
            'gateway' => $env_vars['router'],
1002
        ];
1003
        if (Verify::isIpAddress($env_vars['ip'])) {
1004
            $data['subnet'] = $this->netMaskToCidr($env_vars['subnet']);
1005
        } else {
1006
            $data['subnet'] = '';
1007
        }
1008
        $this->updateIfSettings($data, $env_vars['interface']);
1009
        $data = [
1010
            'primarydns' => $named_dns[0] ?? '',
1011
            'secondarydns' => $named_dns[1] ?? '',
1012
        ];
1013
        $this->updateDnsSettings($data, $env_vars['interface']);
1014
    }
1015
1016
    /**
1017
     * Retrieves the interface name by its ID.
1018
     *
1019
     * @param string $id_net The ID of the network interface.
1020
     *
1021
     * @return string  The interface name.
1022
     */
1023
    public function getInterfaceNameById(string $id_net): string
1024
    {
1025
        $res = LanInterfaces::findFirstById($id_net);
1026
        if ($res !== null && $res->interface !== null) {
1027
            return $res->interface;
1028
        }
1029
1030
        return '';
1031
    }
1032
1033
    /**
1034
     * Retrieves the enabled LAN interfaces.
1035
     *
1036
     * @return array  An array of enabled LAN interfaces.
1037
     */
1038
    public function getEnabledLanInterfaces(): array
1039
    {
1040
        /** @var LanInterfaces $res */
1041
        $res = LanInterfaces::find('disabled=0');
1042
1043
        return $res->toArray();
1044
    }
1045
1046
    /**
1047
     * Performs deconfiguration of the udhcpc configuration.
1048
     */
1049
    public function udhcpcConfigureDeconfig(): void
1050
    {
1051
        $interface = trim(getenv('interface'));
1052
1053
        // For MIKO LFS Edition.
1054
        $busyboxPath = Util::which('busybox');
1055
1056
        // Bring the interface up.
1057
        Processes::mwExec("{$busyboxPath} ifconfig {$interface} up");
1058
1059
        // Set a default IP configuration for the interface.
1060
        Processes::mwExec("{$busyboxPath} ifconfig {$interface} 192.168.2.1 netmask 255.255.255.0");
1061
    }
1062
1063
    /**
1064
     * Updates the network settings with the provided data.
1065
     * @param array $data The network settings data to update.
1066
     */
1067
    public function updateNetSettings(array $data): void
1068
    {
1069
        $res = LanInterfaces::findFirst("internet = '1'");
1070
        $update_inet = false;
1071
        if ($res === null) {
1072
            // If no interface with internet connection is found, get the first interface.
1073
            $res = LanInterfaces::findFirst();
1074
            $update_inet = true;
1075
        }
1076
1077
        if ($res !== null) {
1078
            foreach ($data as $key => $value) {
1079
                $res->$key = $value;
1080
            }
1081
            if ($update_inet === true) {
1082
                $res->internet = 1;
1083
            }
1084
            $res->save();
1085
        }
1086
    }
1087
1088
    /**
1089
     * Retrieves information about all network interfaces.
1090
     * @return array An array of network interfaces with their respective information.
1091
     */
1092
    public function getInterfaces(): array
1093
    {
1094
        // Get all PCI interface names (network interfaces).
1095
        $i_names = $this->getInterfacesNames();
1096
        $if_list = [];
1097
        foreach ($i_names as $i) {
1098
            $if_list[$i] = $this->getInterface($i);
1099
        }
1100
1101
        return $if_list;
1102
    }
1103
1104
    /**
1105
     * Retrieves information about a specific network interface.
1106
     * @param string $name The name of the network interface.
1107
     * @return array An array containing the interface information.
1108
     */
1109
    public function getInterface(string $name): array
1110
    {
1111
        $interface = [];
1112
1113
        // Get ifconfig's output for the specified interface.
1114
        $busyboxPath = Util::which('busybox');
1115
        Processes::mwExec("{$busyboxPath} ifconfig $name 2>/dev/null", $output);
1116
        $output = implode(" ", $output);
1117
1118
        // Parse MAC address.
1119
        preg_match("/HWaddr (\S+)/", $output, $matches);
1120
        $interface['mac'] = (count($matches) > 0) ? $matches[1] : '';
1121
1122
        // Parse IP address.
1123
        preg_match("/inet addr:(\S+)/", $output, $matches);
1124
        $interface['ipaddr'] = (count($matches) > 0) ? $matches[1] : '';
1125
1126
        // Parse subnet mask.
1127
        preg_match("/Mask:(\S+)/", $output, $matches);
1128
        $subnet = (count($matches) > 0) ? $this->netMaskToCidr($matches[1]) : '';
1129
        $interface['subnet'] = $subnet;
1130
1131
        // Check if the interface is up.
1132
        preg_match("/\s+(UP)\s+/", $output, $matches);
1133
        $status = (count($matches) > 0) ? $matches[1] : '';
1134
        if ($status === "UP") {
1135
            $interface['up'] = true;
1136
        } else {
1137
            $interface['up'] = false;
1138
        }
1139
        $busyboxPath = Util::which('busybox');
1140
1141
        // Get the default gateway.
1142
        $grepPath = Util::which('grep');
1143
        $cutPath = Util::which('cut');
1144
        $routePath = Util::which('route');
1145
1146
        Processes::mwExec(
1147
            "{$busyboxPath} {$routePath} -n | {$grepPath} {$name} | {$grepPath} \"^0.0.0.0\" | {$cutPath} -d ' ' -f 10",
1148
            $matches
1149
        );
1150
        $gw = (count($matches) > 0) ? $matches[0] : '';
1151
        if (Verify::isIpAddress($gw)) {
1152
            $interface['gateway'] = $gw;
1153
        }
1154
1155
        // Get DNS servers.
1156
        $catPath = Util::which('cat');
1157
        Processes::mwExec("{$catPath} /etc/resolv.conf | {$grepPath} nameserver | {$cutPath} -d ' ' -f 2", $dnsout);
1158
1159
        $dnsSrv = [];
1160
        foreach ($dnsout as $line) {
1161
            if (Verify::isIpAddress($line)) {
1162
                $dnsSrv[] = $line;
1163
            }
1164
        }
1165
        $interface['dns'] = $dnsSrv;
1166
1167
        return $interface;
1168
    }
1169
1170
    /**
1171
     * Update external IP address
1172
     */
1173
    public function updateExternalIp(): void
1174
    {
1175
        $ipInfoResult = SysinfoManagementProcessor::getExternalIpInfo();
1176
        if ($ipInfoResult->success && isset($ipInfoResult->data['ip'])) {
1177
            $currentIP = $ipInfoResult->data['ip'];
1178
            $lanData = LanInterfaces::find('autoUpdateExtIp=1');
1179
            foreach ($lanData as $lan) {
1180
                $oldExtIp = $lan->extipaddr;
1181
                $parts = explode(':', $oldExtIp);
1182
                $oldIP = $parts[0]; // Only IP part of the address
1183
                $port = isset($parts[1]) ? ':' . $parts[1] : '';
1184
                if ($oldIP !== $currentIP) {
1185
                    $newExtIp = $currentIP . $port;
1186
                    $lan->extipaddr = $newExtIp;
1187
                    if ($lan->save()) {
1188
                        Util::sysLogMsg(__METHOD__, "External IP address updated for interface {$lan->interface}");
1189
                    }
1190
                }
1191
            }
1192
        }
1193
    }
1194
1195
}