Passed
Push — develop ( 49e3f5...56ecd0 )
by Nikolay
04:35
created

Network::cliAction()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 47
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 47
rs 8.8333
c 0
b 0
f 0
cc 7
nc 7
nop 1
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
            if ($port === '443'){
120
                $info .= "    - https://$address" . PHP_EOL;
121
            } else {
122
                $info .= "    - https://$address:$port" . PHP_EOL;
123
            }
124
125
        }
126
        $info .= PHP_EOL;
127
128
        // External web address info
129
        if (!empty($addresses['external'])) {
130
            $info .= "   The web interface is available at the external addresses:" . PHP_EOL . PHP_EOL;
131
            foreach ($addresses['external'] as $address) {
132
                if (empty($address)) {
133
                    continue;
134
                }
135
                if ($port === '443'){
136
                    $info .= "    - https://$address" . PHP_EOL;
137
                } else {
138
                    $info .= "    - https://$address:$port" . PHP_EOL;
139
                }
140
            }
141
            $info .= PHP_EOL;
142
        }
143
144
        // Default web user info
145
        $cloudInstanceId = PbxSettings::getValueByKey(PbxSettingsConstants::CLOUD_INSTANCE_ID);
146
        $webAdminPassword = PbxSettings::getValueByKey(PbxSettingsConstants::WEB_ADMIN_PASSWORD);
147
        if ($cloudInstanceId === $webAdminPassword) {
148
            $adminUser = PbxSettings::getValueByKey(PbxSettingsConstants::WEB_ADMIN_LOGIN);
149
            $info .= "  You should use the next credentials:" . PHP_EOL . PHP_EOL;
150
            $info .= "      Login: $adminUser" . PHP_EOL . PHP_EOL;
151
            $info .= "      Password: $cloudInstanceId" . PHP_EOL . PHP_EOL;
152
        }
153
154
        return $info;
155
    }
156
157
    /**
158
     * Configures the loopback interface (lo) with the IP address 127.0.0.1.
159
     *
160
     * @return void
161
     */
162
    public function loConfigure(): void
163
    {
164
        $busyboxPath = Util::which('busybox');
165
        $ifconfigPath = Util::which('ifconfig');
166
        Processes::mwExec("{$busyboxPath} {$ifconfigPath} lo 127.0.0.1");
167
    }
168
169
    /**
170
     * Generates the resolv.conf file based on system configuration.
171
     */
172
    public function resolvConfGenerate(): void
173
    {
174
        if (Util::isDocker()) {
175
            return;
176
        }
177
178
        // Initialize resolv.conf content
179
        $resolv_conf = '';
180
181
        // Get hostname information
182
        $data_hostname = self::getHostName();
183
184
        // Append domain to resolv.conf if it is not empty
185
        if (trim($data_hostname['domain']) !== '') {
186
            $resolv_conf .= "domain {$data_hostname['domain']}\n";
187
        }
188
189
        // Append local nameserver to resolv.conf
190
        $resolv_conf .= "nameserver 127.0.0.1\n";
191
192
        // Initialize an array to store named DNS servers
193
        $named_dns = [];
194
195
        // Retrieve host DNS settings
196
        $dns = $this->getHostDNS();
197
198
        // Iterate over each DNS server
199
        foreach ($dns as $ns) {
200
            // Skip empty DNS servers
201
            if (trim($ns) === '') {
202
                continue;
203
            }
204
            // Add the DNS server to the named_dns array
205
            $named_dns[] = $ns;
206
207
            // Append the DNS server to resolv.conf
208
            $resolv_conf .= "nameserver {$ns}\n";
209
        }
210
211
        // If no DNS servers were found, use default ones and add them to named_dns
212
        if (count($dns) === 0) {
213
            $resolv_conf .= "nameserver 4.4.4.4\n";
214
            $named_dns[] .= "8.8.8.8";
215
        }
216
217
        // Check if systemctl is available
218
        if (Util::isSystemctl()) {
219
220
            // Generate resolved.conf content for systemd-resolved
221
            $s_resolv_conf = "[Resolve]\n"
222
                . "DNS=127.0.0.1\n";
223
224
            // Append domain to resolved.conf if it is not empty
225
            if (trim($data_hostname['domain']) !== '') {
226
                $s_resolv_conf .= "Domains={$data_hostname['domain']}\n";
227
            }
228
229
            // Write resolved.conf content to the file
230
            file_put_contents('/etc/systemd/resolved.conf', $s_resolv_conf);
231
232
            // Restart systemd-resolved service
233
            $systemctlPath = Util::which('systemctl');
234
            Processes::mwExec("{$systemctlPath} restart systemd-resolved");
235
        } else {
236
            // Write resolv.conf content to the file
237
            file_put_contents('/etc/resolv.conf', $resolv_conf);
238
        }
239
240
        // Generate pdnsd configuration using named_dns
241
        $this->generatePdnsdConfig($named_dns);
242
    }
243
244
    /**
245
     * Retrieves the hostname and domain information.
246
     *
247
     * @return array An array containing the hostname and domain.
248
     */
249
    public static function getHostName(): array
250
    {
251
        // Initialize default hostname and domain
252
        $data = [
253
            'hostname' => 'mikopbx',
254
            'domain' => '',
255
        ];
256
257
        // Find the first LanInterfaces record with internet set to '1'
258
        /** @var LanInterfaces $res */
259
        $res = LanInterfaces::findFirst("internet = '1'");
260
261
        // If a matching record is found, update the hostname and domain
262
        if (null !== $res) {
263
            $data['hostname'] = $res->hostname;
264
            $data['domain'] = $res->domain;
265
        }
266
267
        // If the hostname is empty, set it to the default value 'mikopbx'
268
        $data['hostname'] = (empty($data['hostname'])) ? 'mikopbx' : $data['hostname'];
269
270
        return $data;
271
    }
272
273
    /**
274
     * Retrieves the DNS servers configured for the host.
275
     *
276
     * @return array An array containing the DNS servers.
277
     */
278
    public function getHostDNS(): array
279
    {
280
        $dns = [];
281
282
        // Find the first LanInterfaces record with internet set to '1'
283
        /** @var LanInterfaces $res */
284
        $res = LanInterfaces::findFirst("internet = '1'");
285
286
        // If a matching record is found, check and add primary and secondary DNS servers
287
        if (null !== $res) {
288
            // Check and add primary DNS server if not empty and not '127.0.0.1'
289
            if (!empty($res->primarydns) && '127.0.0.1' !== $res->primarydns) {
290
                $dns[] = $res->primarydns;
291
            }
292
            // Check and add secondary DNS server if not empty and not '127.0.0.1'
293
            if (!empty($res->secondarydns) && '127.0.0.1' !== $res->secondarydns) {
294
                $dns[] = $res->secondarydns;
295
            }
296
        }
297
298
        return $dns;
299
    }
300
301
    /**
302
     * Generates the pdnsd configuration file and restarts the pdnsd service if necessary.
303
     *
304
     * @param array $named_dns An array of named DNS servers.
305
     */
306
    public function generatePdnsdConfig(array $named_dns): void
307
    {
308
        $tempDir = $this->di->getShared('config')->path('core.tempDir');
309
        $cache_dir = $tempDir . '/pdnsd/cache';
310
        Util::mwMkdir($cache_dir);
311
312
        $conf = 'global {' . "\n" .
313
            '	perm_cache=10240;' . "\n" .
314
            '	cache_dir="' . $cache_dir . '";' . "\n" .
315
            '	pid_file = /var/run/pdnsd.pid;' . "\n" .
316
            '	run_as="nobody";' . "\n" .
317
            '	server_ip = 127.0.0.1;' . "\n" .
318
            '	status_ctl = on;' . "\n" .
319
            '	query_method=udp_tcp;' . "\n" .
320
            '	min_ttl=15m;' . "\n" .
321
            '	max_ttl=1w;' . "\n" .
322
            '	timeout=10;' . "\n" .
323
            '	neg_domain_pol=on;' . "\n" .
324
            '	run_as=root;' . "\n" .
325
            '	daemon=on;' . "\n" .
326
            '}' . "\n" .
327
            'server {' . "\n" .
328
            '	label = "main";' . "\n" .
329
            '	ip = ' . implode(', ', $named_dns) . ';' . "\n" .
330
            '	interface=lo;' . "\n" .
331
            '	uptest=if;' . "\n" .
332
            '	interval=10m;' . "\n" .
333
            '	purge_cache=off;' . "\n" .
334
            '}';
335
336
        $pdnsdConfFile = '/etc/pdnsd.conf';
337
338
        // Update the pdnsd.conf file if it has changed
339
        $savedConf = '';
340
        if (file_exists($pdnsdConfFile)) {
341
            $savedConf = file_get_contents($pdnsdConfFile);
342
        }
343
        if ($savedConf !== $conf) {
344
            file_put_contents($pdnsdConfFile, $conf);
345
        }
346
        $pdnsdPath = Util::which('pdnsd');
347
        $pid = Processes::getPidOfProcess($pdnsdPath);
348
349
        // Check if pdnsd process is running and the configuration has not changed
350
        if (!empty($pid) && $savedConf === $conf) {
351
352
            // Perform additional check if the DNS server is working
353
            $resultResolve = gethostbynamel('lic.miko.ru');
354
            if ($resultResolve !== false) {
355
                // Configuration has not changed and the DNS server is working,
356
                // no need to restart or reload the service
357
                return;
358
            }
359
            // Perform a reload of the DNS server
360
        }
361
362
        // If pdnsd process is running, terminate the process
363
        if (!empty($pid)) {
364
            $busyboxPath = Util::which('busybox');
365
            $killPath = Util::which('kill');
366
            Processes::mwExec("{$busyboxPath} {$killPath} '$pid'");
367
        }
368
369
        // Start the pdnsd service with the updated configuration
370
        Processes::mwExec("{$pdnsdPath} -c /etc/pdnsd.conf -4");
371
    }
372
373
    /**
374
     * Configures the LAN interfaces and performs related network operations.
375
     *
376
     * @return int The result of the configuration process.
377
     */
378
    public function lanConfigure(): int
379
    {
380
        if (Util::isDocker()) {
381
            return 0;
382
        }
383
384
        // Retrieve the network settings
385
        $networks = $this->getGeneralNetSettings();
386
387
        // Retrieve the paths of required commands
388
        $busyboxPath = Util::which('busybox');
389
        $vconfigPath = Util::which('vconfig');
390
        $killallPath = Util::which('killall');
391
392
        $arr_commands = [];
393
        $arr_commands[] = "{$killallPath} udhcpc";
394
        $eth_mtu = [];
395
        foreach ($networks as $if_data) {
396
            if ($if_data['disabled'] === '1') {
397
                continue;
398
            }
399
400
            $if_name = $if_data['interface'];
401
            $if_name = escapeshellcmd(trim($if_name));
402
            if (empty($if_name)) {
403
                continue;
404
            }
405
406
            $data_hostname = self::getHostName();
407
            $hostname = $data_hostname['hostname'];
408
409
            if ($if_data['vlanid'] > 0) {
410
                // Override the interface name for VLAN interfaces
411
                $arr_commands[] = "{$vconfigPath} set_name_type VLAN_PLUS_VID_NO_PAD";
412
                // Add the new VLAN interface
413
                $arr_commands[] = "{$vconfigPath} add {$if_data['interface_orign']} {$if_data['vlanid']}";
414
            }
415
            // Disable and reset the interface
416
            $arr_commands[] = "{$busyboxPath} ifconfig $if_name down";
417
            $arr_commands[] = "{$busyboxPath} ifconfig $if_name 0.0.0.0";
418
419
            $gw_param = '';
420
            if (trim($if_data['dhcp']) === '1') {
421
                // DHCP configuration
422
                /*
423
                 * -t - number of attempts.
424
                 * -T - timeout for each attempt.
425
                 * -v - enable debugging.
426
                 * -S - log messages to syslog.
427
                 * -q - exit after obtaining lease.
428
                 * -n - exit if lease is not obtained.
429
                 */
430
                $pid_file = "/var/run/udhcpc_{$if_name}";
431
                $pid_pcc = Processes::getPidOfProcess($pid_file);
432
                if (!empty($pid_pcc) && file_exists($pid_file)) {
433
                    // Terminate the old udhcpc process
434
                    $killPath = Util::which('kill');
435
                    $catPath = Util::which('cat');
436
                    system("{$killPath} `{$catPath} {$pid_file}` {$pid_pcc}");
437
                }
438
                $udhcpcPath = Util::which('udhcpc');
439
                $nohupPath = Util::which('nohup');
440
441
                // Obtain IP and wait for the process to finish
442
                $workerPath = '/etc/rc/udhcpc.configure';
443
                $options = '-t 6 -T 5 -q -n';
444
                $arr_commands[] = "{$udhcpcPath} {$options} -i {$if_name} -x hostname:{$hostname} -s {$workerPath}";
445
                // Start a new udhcpc process in the background
446
                $options = '-t 6 -T 5 -S -b -n';
447
                $arr_commands[] = "{$nohupPath} {$udhcpcPath} {$options} -p {$pid_file} -i {$if_name} -x hostname:{$hostname} -s {$workerPath} 2>&1 &";
448
                /*
449
                   udhcpc - utility for configuring the interface
450
                               - configures /etc/resolv.conf
451
                    Further route configuration will be performed in udhcpcConfigureRenewBound();
452
                    and udhcpcConfigureDeconfig(). These methods will be called by the script WorkerUdhcpcConfigure.php.
453
                    // man udhcp
454
                    // http://pwet.fr/man/linux/administration_systeme/udhcpc/
455
456
                */
457
            } else {
458
                // Static IP configuration
459
                $ipaddr = trim($if_data['ipaddr']);
460
                $subnet = trim($if_data['subnet']);
461
                $gateway = trim($if_data['gateway']);
462
                if (empty($ipaddr)) {
463
                    continue;
464
                }
465
                try {
466
                    // Calculate the short subnet mask
467
                    $calc_subnet = new SubnetCalculator($ipaddr, $subnet);
468
                    $subnet = $calc_subnet->getSubnetMask();
469
                } catch (Throwable $e) {
470
                    echo "Caught exception: $ipaddr $subnet", $e->getMessage(), "\n";
471
                    continue;
472
                }
473
474
                $ifconfigPath = Util::which('ifconfig');
475
                $arr_commands[] = "{$busyboxPath} {$ifconfigPath} $if_name $ipaddr netmask $subnet";
476
477
                if ("" !== trim($gateway)) {
478
                    $gw_param = "gw $gateway";
479
                }
480
481
                $routePath = Util::which('route');
482
                $arr_commands[] = "{$busyboxPath} {$routePath} del default $if_name";
483
484
                /** @var LanInterfaces $if_data */
485
                $if_data = LanInterfaces::findFirst("id = '{$if_data['id']}'");
486
                $is_inet = ($if_data !== null) ? (string)$if_data->internet : '0';
487
488
                if ($is_inet === '1') {
489
                    // Create default route only if the interface is for internet
490
                    $arr_commands[] = "{$busyboxPath} {$routePath} add default $gw_param dev $if_name";
491
                }
492
                // Bring up the interface
493
                $arr_commands[] = "{$busyboxPath} {$ifconfigPath} $if_name up";
494
495
                $eth_mtu[] = $if_name;
496
            }
497
        }
498
        $out = null;
499
        Processes::mwExecCommands($arr_commands, $out, 'net');
500
        $this->hostsGenerate();
501
502
        foreach ($eth_mtu as $eth) {
503
            Processes::mwExecBg("/etc/rc/networking.set.mtu '{$eth}'");
504
        }
505
506
        // Additional "manual" routes
507
        Util::fileWriteContent('/etc/static-routes', '');
508
        $arr_commands = [];
509
        $out = [];
510
        $grepPath = Util::which('grep');
511
        $awkPath = Util::which('awk');
512
        $catPath = Util::which('cat');
513
        Processes::mwExec(
514
            "{$catPath} /etc/static-routes | {$grepPath} '^rout' | {$busyboxPath} {$awkPath} -F ';' '{print $1}'",
515
            $arr_commands
516
        );
517
        Processes::mwExecCommands($arr_commands, $out, 'rout');
518
519
        $this->openVpnConfigure();
520
        return 0;
521
    }
522
523
    /**
524
     * Retrieves the general network settings and performs additional processing.
525
     *
526
     * @return array An array of network interfaces and their settings.
527
     */
528
    public function getGeneralNetSettings(): array
529
    {
530
        // Get the list of network interfaces visible to the operating system
531
        $src_array_eth = $this->getInterfacesNames();
532
533
        // Create a copy of the network interfaces array
534
        $array_eth = $src_array_eth;
535
536
        // Retrieve the LAN interface settings from the database
537
        $res = LanInterfaces::find(['order' => 'interface,vlanid']);
538
        $networks = $res->toArray();
539
540
        if (count($networks) > 0) {
541
            // Additional data processing
542
            foreach ($networks as &$if_data) {
543
                $if_data['interface_orign'] = $if_data['interface'];
544
                $if_data['interface'] = ($if_data['vlanid'] > 0) ? "vlan{$if_data['vlanid']}" : $if_data['interface'];
545
                $if_data['dhcp'] = ($if_data['vlanid'] > 0) ? 0 : $if_data['dhcp'];
546
547
                if (Verify::isIpAddress($if_data['subnet'])) {
548
                    $if_data['subnet'] = $this->netMaskToCidr($if_data['subnet']);
549
                }
550
551
                $key = array_search($if_data['interface_orign'], $src_array_eth, true);
552
                if ($key !== false) {
553
                    // Interface found
554
                    // Remove the array element if it's not a VLAN
555
                    if ($if_data['vlanid'] === '0') {
556
                        unset($array_eth[$key]);
557
                        $this->enableLanInterface($if_data['interface_orign']);
558
                    }
559
                } else {
560
                    // Interface does not exist
561
                    $this->disableLanInterface($if_data['interface_orign']);
562
                    // Disable the interface
563
                    $if_data['disabled'] = 1;
564
                }
565
            }
566
            unset($if_data);
567
        } elseif (count($array_eth) > 0) {
568
            $networks = [];
569
            // Configure the main interface
570
            $networks[] = $this->addLanInterface($array_eth[0], true);
571
            unset($array_eth[0]);
572
        }
573
        // $array_eth will contain only the elements without settings in the database
574
        // Add the "default" settings for these interfaces
575
        foreach ($array_eth as $eth) {
576
            // Add the interface and disable it
577
            $networks[] = $this->addLanInterface($eth, false);
578
        }
579
580
        // Check if there is an active internet interface, if not, set the first available interface as internet
581
        $res = LanInterfaces::findFirst("internet = '1' AND disabled='0'");
582
        if (null === $res) {
583
            /** @var LanInterfaces $eth_settings */
584
            $eth_settings = LanInterfaces::findFirst("disabled='0'");
585
            if ($eth_settings !== null) {
586
                $eth_settings->internet = 1;
587
                $eth_settings->save();
588
            }
589
        }
590
591
        return $networks;
592
    }
593
594
    /**
595
     * Converts a net mask to CIDR notation.
596
     *
597
     * @param string $net_mask The net mask to convert.
598
     * @return int The CIDR notation.
599
     */
600
    public function netMaskToCidr(string $net_mask): int
601
    {
602
        $bits = 0;
603
        $net_mask = explode(".", $net_mask);
604
605
        foreach ($net_mask as $oct_ect) {
606
            $bits += strlen(str_replace("0", "", decbin((int)$oct_ect)));
607
        }
608
609
        return $bits;
610
    }
611
612
    /**
613
     * Enables a LAN interface
614
     *
615
     * @param string $name The name of the interface to enable.
616
     * @return void
617
     */
618
    public function enableLanInterface(string $name): void
619
    {
620
        $parameters = [
621
            'conditions' => 'interface = :ifName: and disabled = :disabled:',
622
            'bind' => [
623
                'ifName' => $name,
624
                'disabled' => 1,
625
            ],
626
        ];
627
628
        $if_data = LanInterfaces::findFirst($parameters);
629
        if ($if_data !== null) {
630
            $if_data->disabled = 0;
631
            $if_data->update();
632
        }
633
    }
634
635
    /**
636
     * Disables a LAN interface by setting its internet flag to 0 and disabled flag to 1.
637
     *
638
     * @param string $name The name of the interface to disable.
639
     * @return void
640
     */
641
    public function disableLanInterface(string $name): void
642
    {
643
        $if_data = LanInterfaces::findFirst("interface = '{$name}'");
644
        if ($if_data !== null) {
645
            $if_data->internet = 0;
646
            $if_data->disabled = 1;
647
            $if_data->update();
648
        }
649
    }
650
651
    /**
652
     * Adds a LAN interface with the specified name and settings.
653
     *
654
     * @param string $name The name of the interface.
655
     * @param bool $general Flag indicating if the interface is a general interface.
656
     * @return array The array representation of the added interface.
657
     */
658
    private function addLanInterface(string $name, bool $general = false): array
659
    {
660
        $data = new LanInterfaces();
661
        $data->name = $name;
662
        $data->interface = $name;
663
        $data->dhcp = '1';
664
        $data->internet = ($general === true) ? '1' : '0';
665
        $data->disabled = '0';
666
        $data->vlanid = '0';
667
        $data->hostname = 'MikoPBX';
668
        $data->domain = '';
669
        $data->topology = LanInterfaces::TOPOLOGY_PRIVATE;
670
        $data->autoUpdateExtIp = '1';
671
        $data->primarydns = '';
672
        $data->save();
673
674
        return $data->toArray();
675
    }
676
677
    /**
678
     * Generates the hosts configuration.
679
     *
680
     * @return void
681
     */
682
    public function hostsGenerate(): void
683
    {
684
        $this->hostnameConfigure();
685
    }
686
687
    /**
688
     * Configures the hostname and hosts file.
689
     *
690
     * @return void
691
     */
692
    public function hostnameConfigure(): void
693
    {
694
        $data = self::getHostName();
695
        $hosts_conf = "127.0.0.1 localhost\n" .
696
            "127.0.0.1 {$data['hostname']}\n";
697
        if (!empty($data['domain'])) {
698
            $hosts_conf .= "127.0.0.1 {$data['hostname']}.{$data['domain']}\n";
699
        }
700
        $hostnamePath = Util::which('hostname');
701
        if (Util::isDocker()) {
702
            $realHostName = shell_exec($hostnamePath);
703
            $hosts_conf .= "127.0.0.1 $realHostName\n";
704
        }
705
        Util::fileWriteContent('/etc/hosts', $hosts_conf);
706
        Processes::mwExec($hostnamePath . ' ' . escapeshellarg($data['hostname']));
707
    }
708
709
    /**
710
     * Configuring OpenVPN. If a custom configuration file is specified in the system file customization, the network will be brought up.
711
     */
712
    public function openVpnConfigure(): void
713
    {
714
        $confFile = '/etc/openvpn.ovpn';
715
        Util::fileWriteContent($confFile, '');
716
        $data = file_get_contents($confFile);
717
718
        $pidFile = '/var/run/openvpn.pid';
719
        $pid = Processes::getPidOfProcess('openvpn');
720
        if (!empty($pid)) {
721
            // Terminate the process.
722
            $busyboxPath = Util::which('busybox');
723
            Processes::mwExec("{$busyboxPath} kill '$pid'");
724
        }
725
        if (!empty($data)) {
726
            $openvpnPath = Util::which('openvpn');
727
            Processes::mwExecBg("{$openvpnPath} --config /etc/openvpn.ovpn --writepid {$pidFile}", '/dev/null', 5);
728
        }
729
    }
730
731
    /**
732
     * Configures the LAN settings inside the Docker container.
733
     *
734
     * If the environment is not Docker, this method does nothing.
735
     *
736
     * @return void
737
     */
738
    public function configureLanInDocker(): void
739
    {
740
        // Check if the environment is Docker
741
        if (!Util::isDocker()) {
742
            return;
743
        }
744
745
        // Find the path to the busybox binary
746
        $busyboxPath = Util::which('busybox');
747
748
        // Retrieve the network settings
749
        $networks = $this->getGeneralNetSettings();
750
751
        foreach ($networks as $if_data) {
752
753
            $if_name = $if_data['interface'];
754
            $if_name = escapeshellcmd(trim($if_name));
755
756
            $commands = [
757
                'subnet' => $busyboxPath . ' ifconfig eth0 | awk \'/Mask:/ {sub("Mask:", "", $NF); print $NF}\'',
758
                'ipaddr' => $busyboxPath . ' ifconfig eth0 | awk \'/inet / {sub("addr:", "", $2); print $2}\'',
759
                'gateway' => $busyboxPath . ' route -n | awk \'/^0.0.0.0/ {print $2}\'',
760
                'hostname' => $busyboxPath . ' hostname',
761
            ];
762
            $data = [];
763
            foreach ($commands as $key => $command) {
764
                $output = [];
765
                if (Processes::MWExec($command, $output) === 0) {
766
                    $value = implode("", $output);
767
                    if ($key === 'subnet') {
768
                        $data[$key] = $this->netMaskToCidr($value);
769
                    } else {
770
                        $data[$key] = $value;
771
                    }
772
                }
773
            }
774
775
            // Save information to the database.
776
            $this->updateIfSettings($data, $if_name);
777
        }
778
779
    }
780
781
    /**
782
     * Updates the interface settings with the provided data.
783
     *
784
     * @param array $data The data to update the interface settings with.
785
     * @param string $name The name of the interface.
786
     *
787
     * @return void;
788
     */
789
    public function updateIfSettings(array $data, string $name): void
790
    {
791
        /** @var LanInterfaces $res */
792
        $res = LanInterfaces::findFirst("interface = '$name' AND vlanid=0");
793
        if ($res === null || !$this->settingsIsChange($data, $res->toArray())) {
794
            return;
795
        }
796
        foreach ($data as $key => $value) {
797
            $res->writeAttribute($key, $value);
798
        }
799
        $res->save();
800
    }
801
802
    /**
803
     * Updates the DNS settings with the provided data.
804
     *
805
     * @param array $data The data to update the DNS settings with.
806
     * @param string $name The name of the interface.
807
     *
808
     * @return void
809
     */
810
    public function updateDnsSettings(array $data, string $name): void
811
    {
812
        /** @var LanInterfaces $res */
813
        $res = LanInterfaces::findFirst("interface = '$name' AND vlanid=0");
814
        if ($res === null || !$this->settingsIsChange($data, $res->toArray())) {
815
            return;
816
        }
817
        foreach ($data as $key => $value) {
818
            $res->writeAttribute($key, $value);
819
        }
820
        if (empty($res->primarydns) && !empty($res->secondarydns)) {
821
            // Swap primary and secondary DNS if primary is empty
822
            $res->primarydns = $res->secondarydns;
823
            $res->secondarydns = '';
824
        }
825
        $res->save();
826
    }
827
828
    /**
829
     * Checks if the network settings have changed.
830
     *
831
     * @param array $data The new network settings.
832
     * @param array $dbData The existing network settings from the database.
833
     *
834
     * @return bool  Returns true if the settings have changed, false otherwise.
835
     */
836
    public function settingsIsChange(array $data, array $dbData): bool
837
    {
838
        $isChange = false;
839
        foreach ($dbData as $key => $value) {
840
            if (!isset($data[$key]) || (string)$value === (string)$data[$key]) {
841
                continue;
842
            }
843
            Util::sysLogMsg(__METHOD__, "Find new network settings: {$key} changed {$value}=>{$data[$key]}");
844
            $isChange = true;
845
        }
846
        return $isChange;
847
    }
848
849
    /**
850
     * Retrieves the interface name by its ID.
851
     *
852
     * @param string $id_net The ID of the network interface.
853
     *
854
     * @return string  The interface name.
855
     */
856
    public function getInterfaceNameById(string $id_net): string
857
    {
858
        $res = LanInterfaces::findFirstById($id_net);
859
        if ($res !== null && $res->interface !== null) {
860
            return $res->interface;
861
        }
862
863
        return '';
864
    }
865
866
    /**
867
     * Retrieves the enabled LAN interfaces.
868
     *
869
     * @return array  An array of enabled LAN interfaces.
870
     */
871
    public function getEnabledLanInterfaces(): array
872
    {
873
        /** @var LanInterfaces $res */
874
        $res = LanInterfaces::find('disabled=0');
875
876
        return $res->toArray();
877
    }
878
879
880
881
    /**
882
     * Updates the network settings with the provided data.
883
     * @param array $data The network settings data to update.
884
     */
885
    public function updateNetSettings(array $data): void
886
    {
887
        $res = LanInterfaces::findFirst("internet = '1'");
888
        $update_inet = false;
889
        if ($res === null) {
890
            // If no interface with internet connection is found, get the first interface.
891
            $res = LanInterfaces::findFirst();
892
            $update_inet = true;
893
        }
894
895
        if ($res !== null) {
896
            foreach ($data as $key => $value) {
897
                $res->$key = $value;
898
            }
899
            if ($update_inet === true) {
900
                $res->internet = 1;
901
            }
902
            $res->save();
903
        }
904
    }
905
906
    /**
907
     * Retrieves information about all network interfaces.
908
     * @return array An array of network interfaces with their respective information.
909
     */
910
    public function getInterfaces(): array
911
    {
912
        // Get all PCI interface names (network interfaces).
913
        $i_names = $this->getInterfacesNames();
914
        $if_list = [];
915
        foreach ($i_names as $i) {
916
            $if_list[$i] = $this->getInterface($i);
917
        }
918
919
        return $if_list;
920
    }
921
922
    /**
923
     * Retrieves information about a specific network interface.
924
     * @param string $name The name of the network interface.
925
     * @return array An array containing the interface information.
926
     */
927
    public function getInterface(string $name): array
928
    {
929
        $interface = [];
930
931
        // Get ifconfig's output for the specified interface.
932
        $busyboxPath = Util::which('busybox');
933
        Processes::mwExec("{$busyboxPath} ifconfig $name 2>/dev/null", $output);
934
        $output = implode(" ", $output);
935
936
        // Parse MAC address.
937
        preg_match("/HWaddr (\S+)/", $output, $matches);
938
        $interface['mac'] = (count($matches) > 0) ? $matches[1] : '';
939
940
        // Parse IP address.
941
        preg_match("/inet addr:(\S+)/", $output, $matches);
942
        $interface['ipaddr'] = (count($matches) > 0) ? $matches[1] : '';
943
944
        // Parse subnet mask.
945
        preg_match("/Mask:(\S+)/", $output, $matches);
946
        $subnet = (count($matches) > 0) ? $this->netMaskToCidr($matches[1]) : '';
947
        $interface['subnet'] = $subnet;
948
949
        // Check if the interface is up.
950
        preg_match("/\s+(UP)\s+/", $output, $matches);
951
        $status = (count($matches) > 0) ? $matches[1] : '';
952
        if ($status === "UP") {
953
            $interface['up'] = true;
954
        } else {
955
            $interface['up'] = false;
956
        }
957
        $busyboxPath = Util::which('busybox');
958
959
        // Get the default gateway.
960
        $grepPath = Util::which('grep');
961
        $cutPath = Util::which('cut');
962
        $routePath = Util::which('route');
963
964
        Processes::mwExec(
965
            "{$busyboxPath} {$routePath} -n | {$grepPath} {$name} | {$grepPath} \"^0.0.0.0\" | {$cutPath} -d ' ' -f 10",
966
            $matches
967
        );
968
        $gw = (count($matches) > 0) ? $matches[0] : '';
969
        if (Verify::isIpAddress($gw)) {
970
            $interface['gateway'] = $gw;
971
        }
972
973
        // Get DNS servers.
974
        $catPath = Util::which('cat');
975
        Processes::mwExec("{$catPath} /etc/resolv.conf | {$grepPath} nameserver | {$cutPath} -d ' ' -f 2", $dnsout);
976
977
        $dnsSrv = [];
978
        foreach ($dnsout as $line) {
979
            if (Verify::isIpAddress($line)) {
980
                $dnsSrv[] = $line;
981
            }
982
        }
983
        $interface['dns'] = $dnsSrv;
984
985
        return $interface;
986
    }
987
988
    /**
989
     * Update external IP address
990
     */
991
    public function updateExternalIp(): void
992
    {
993
        $ipInfoResult = SysinfoManagementProcessor::getExternalIpInfo();
994
        if ($ipInfoResult->success && isset($ipInfoResult->data['ip'])) {
995
            $currentIP = $ipInfoResult->data['ip'];
996
            $lanData = LanInterfaces::find('autoUpdateExtIp=1');
997
            foreach ($lanData as $lan) {
998
                $oldExtIp = $lan->extipaddr;
999
                $parts = explode(':', $oldExtIp);
1000
                $oldIP = $parts[0]; // Only IP part of the address
1001
                $port = isset($parts[1]) ? ':' . $parts[1] : '';
1002
                if ($oldIP !== $currentIP) {
1003
                    $newExtIp = $currentIP . $port;
1004
                    $lan->extipaddr = $newExtIp;
1005
                    if ($lan->save()) {
1006
                        Util::sysLogMsg(__METHOD__, "External IP address updated for interface {$lan->interface}");
1007
                    }
1008
                }
1009
            }
1010
        }
1011
    }
1012
1013
    /**
1014
     * Execute cli command to set up network
1015
     * @param string $action Action to perform (start or stop)
1016
     * @return void
1017
     */
1018
    public function cliAction(string $action): void
1019
    {
1020
        /**
1021
         * If running inside a Docker container, exit the script.
1022
         */
1023
        if (Util::isDocker()) {
1024
            return;
1025
        }
1026
1027
        if ('start' === $action) {
1028
1029
            /**
1030
             * Generate the resolv.conf file for DNS configuration.
1031
             */
1032
            $this->resolvConfGenerate();
1033
            if (Util::isT2SdeLinux()) {
1034
                /**
1035
                 * Configure the loopback interface for T2SDE Linux.
1036
                 */
1037
                $this->loConfigure();
1038
            }
1039
            /**
1040
             * Configure the LAN interfaces.
1041
             */
1042
            $this->lanConfigure();
1043
        } elseif ('stop' === $action) {
1044
            if (Util::isSystemctl()) {
1045
                /**
1046
                 * Stop networking using systemctl (systemd-based systems).
1047
                 */
1048
                $systemctlPath = Util::which('systemctl');
1049
                Processes::mwExec("{$systemctlPath} stop networking");
1050
            } else {
1051
                /**
1052
                 * Stop networking on T2SDE (non-systemd) systems.
1053
                 */
1054
                $if_list      = $this->getInterfaces();
1055
                $arr_commands = [];
1056
                $ifconfigPath = Util::which('ifconfig');
1057
                foreach ($if_list as $if_name => $data) {
1058
                    $arr_commands[] = "{$ifconfigPath} $if_name down";
1059
                }
1060
1061
                /**
1062
                 * Execute the stop commands for each interface.
1063
                 */
1064
                Processes::mwExecCommands($arr_commands, $out, 'net_stop');
1065
            }
1066
        }
1067
    }
1068
}