Passed
Push — develop ( a6a975...49e3f5 )
by Nikolay
04:45 queued 13s
created

Network::addCustomStaticRoutes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 10
rs 10
cc 2
nc 2
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
            $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
     * Processes DHCP renewal and binding for network interfaces.
816
     *
817
     * This function configures the network interface based on DHCP lease information.
818
     * It sets up the interface IP, subnet mask, default gateway, and static routes.
819
     * It also handles interface configuration deinitialization, DNS settings update, and MTU settings.
820
     *
821
     * @return void
822
     */
823
    public function udhcpcConfigureRenewBound(): void
824
    {
825
        // Initialize an array to store environment variables related to network configuration.
826
        $env_vars = [
827
            'broadcast' => '', // 10.0.0.255
828
            'interface' => '', // eth0
829
            'ip' => '', // 10.0.0.249
830
            'router' => '', // 10.0.0.1
831
            'timesvr' => '',
832
            'namesvr' => '',
833
            'dns' => '', // 10.0.0.254
834
            'hostname' => '', // bad
835
            'subnet' => '', // 255.255.255.0
836
            'serverid' => '', // 10.0.0.1
837
            'ipttl' => '',
838
            'lease' => '', // 86400
839
            'domain' => '', // bad
840
            'mtu'=>'' , // 1500
841
            'staticroutes'=> '', // 0.0.0.0/0 10.0.0.1 169.254.169.254/32 10.0.0.65 0.0.0.0/0 10.0.0.1
842
            'mask' => '', // 24
843
        ];
844
845
        // Check for debug mode to enable logging.
846
        $debugMode = $this->di->getShared('config')->path('core.debugMode');
847
848
        // Retrieve and trim the values of the required environment variables.
849
        foreach ($env_vars as $key => $value) {
850
            $env_vars[$key] = trim(getenv($key));
851
        }
852
        unset($value);
853
854
        // Configure broadcast address if provided, otherwise leave it blank.
855
        $BROADCAST = !empty($env_vars['broadcast']) ? "broadcast {$env_vars['broadcast']}" : "";
856
857
        // Handle subnet mask for /32 assignments and other cases.
858
        $NET_MASK = (!empty($env_vars['subnet']) && $env_vars['subnet'] !== '255.255.255.255') ? "netmask {$env_vars['subnet']}" : "";
859
860
        // Configure the network interface with the provided IP, broadcast, and subnet mask.
861
        $busyboxPath = Util::which('busybox');
862
        Processes::mwExec("{$busyboxPath} ifconfig {$env_vars['interface']} {$env_vars['ip']} $BROADCAST $NET_MASK");
863
864
865
        // Remove any existing default gateway routes associated with this interface.
866
        while (true) {
867
            $out = [];
868
            Processes::mwExec("route del default gw 0.0.0.0 dev {$env_vars['interface']}", $out);
869
            if (trim(implode('', $out)) !== '') {
870
                // An error occurred, indicating that all routes have been cleared.
871
                break;
872
            }
873
            if ($debugMode) {
874
                break;
875
            } // Otherwise, it will be an infinite loop.
876
        }
877
878
        // Add a default gateway route if a router address is provided and the interface is for the internet.
879
        $if_data = LanInterfaces::findFirst("interface = '{$env_vars['interface']}'");
880
        $is_inet = ($if_data !== null) ? (int)$if_data->internet : 0;
881
        if (!empty($env_vars['router']) && $is_inet === 1) {
882
            // Only add the default route if this interface is for the internet.
883
            $routers = explode(' ', $env_vars['router']);
884
            foreach ($routers as $router) {
885
                Processes::mwExec("route add default gw {$router} dev {$env_vars['interface']}");
886
            }
887
        }
888
889
        // Add custom static routes if any are provided.
890
        $this->addStaticRoutes($env_vars['staticroutes'], $env_vars['interface']);
891
892
        // Add custom routes.
893
        $this->addCustomStaticRoutes($env_vars['interface']);
894
895
896
        // Setup DNS.
897
        $named_dns = [];
898
        if ('' !== $env_vars['dns']) {
899
            $named_dns = explode(' ', $env_vars['dns']);
900
        }
901
        if ($is_inet === 1) {
902
            // Only generate pdnsd config if this interface is for internet.
903
            $this->generatePdnsdConfig($named_dns);
904
        }
905
906
        // Save information to the database.
907
        $data = [
908
            'subnet' => '',
909
            'ipaddr' => $env_vars['ip'],
910
            'gateway' => $env_vars['router'],
911
        ];
912
        if (Verify::isIpAddress($env_vars['ip'])) {
913
            $data['subnet'] = $this->netMaskToCidr($env_vars['subnet']);
914
        }
915
916
        $this->updateIfSettings($data, $env_vars['interface']);
917
918
        $data = [
919
            'primarydns' => $named_dns[0] ?? '',
920
            'secondarydns' => $named_dns[1] ?? '',
921
        ];
922
        $this->updateDnsSettings($data, $env_vars['interface']);
923
924
        Processes::mwExecBg("/etc/rc/networking.set.mtu '{$env_vars['interface']}'");
925
    }
926
927
    /**
928
     * Updates the DNS settings with the provided data.
929
     *
930
     * @param array $data The data to update the DNS settings with.
931
     * @param string $name The name of the interface.
932
     *
933
     * @return void
934
     */
935
    public function updateDnsSettings($data, $name): void
936
    {
937
        /** @var LanInterfaces $res */
938
        $res = LanInterfaces::findFirst("interface = '$name' AND vlanid=0");
939
        if ($res === null || !$this->settingsIsChange($data, $res->toArray())) {
940
            return;
941
        }
942
        foreach ($data as $key => $value) {
943
            $res->writeAttribute($key, $value);
944
        }
945
        if (empty($res->primarydns) && !empty($res->secondarydns)) {
946
            // Swap primary and secondary DNS if primary is empty
947
            $res->primarydns = $res->secondarydns;
948
            $res->secondarydns = '';
949
        }
950
        $res->save();
951
    }
952
953
    /**
954
     * Renews and configures the network settings after successful DHCP negotiation using systemd environment variables.
955
     * For OS systemctl (Debian).
956
     *  Configures LAN interface FROM dhcpc (renew_bound).
957
     * @return void
958
     */
959
    public function udhcpcConfigureRenewBoundSystemCtl(): void
960
    {
961
        $prefix = "new_";
962
963
        // Initialize the environment variables array.
964
        $env_vars = [
965
            'broadcast' => 'broadcast_address',
966
            'interface' => 'interface',
967
            'ip' => 'ip_address',
968
            'router' => 'routers',
969
            'timesvr' => '',
970
            'namesvr' => 'netbios_name_servers',
971
            'dns' => 'domain_name_servers',
972
            'hostname' => 'host_name',
973
            'subnet' => 'subnet_mask',
974
            'serverid' => '',
975
            'ipttl' => '',
976
            'lease' => 'new_dhcp_lease_time',
977
            'domain' => 'domain_name',
978
        ];
979
980
        // Get the values of environment variables.
981
        foreach ($env_vars as $key => $value) {
982
            $env_vars[$key] = trim(getenv("{$prefix}{$value}"));
983
        }
984
985
        /** @var LanInterfaces $if_data */
986
        $if_data = LanInterfaces::findFirst("interface = '{$env_vars['interface']}'");
987
        $is_inet = ($if_data !== null) ? (string)$if_data->internet : '0';
988
989
        $named_dns = [];
990
        if ('' !== $env_vars['dns']) {
991
            $named_dns = explode(' ', $env_vars['dns']);
992
        }
993
        if ($is_inet === '1') {
994
            // Only generate pdnsd config if this interface is for internet.
995
            $this->generatePdnsdConfig($named_dns);
996
        }
997
998
        // Save information to the database.
999
        $data = [
1000
            'subnet' => '',
1001
            'ipaddr' => $env_vars['ip'],
1002
            'gateway' => $env_vars['router'],
1003
        ];
1004
        if (Verify::isIpAddress($env_vars['ip'])) {
1005
            $data['subnet'] = $this->netMaskToCidr($env_vars['subnet']);
1006
        }
1007
        $this->updateIfSettings($data, $env_vars['interface']);
1008
        $data = [
1009
            'primarydns' => $named_dns[0] ?? '',
1010
            'secondarydns' => $named_dns[1] ?? '',
1011
        ];
1012
        $this->updateDnsSettings($data, $env_vars['interface']);
1013
    }
1014
1015
    /**
1016
     * Retrieves the interface name by its ID.
1017
     *
1018
     * @param string $id_net The ID of the network interface.
1019
     *
1020
     * @return string  The interface name.
1021
     */
1022
    public function getInterfaceNameById(string $id_net): string
1023
    {
1024
        $res = LanInterfaces::findFirstById($id_net);
1025
        if ($res !== null && $res->interface !== null) {
1026
            return $res->interface;
1027
        }
1028
1029
        return '';
1030
    }
1031
1032
    /**
1033
     * Retrieves the enabled LAN interfaces.
1034
     *
1035
     * @return array  An array of enabled LAN interfaces.
1036
     */
1037
    public function getEnabledLanInterfaces(): array
1038
    {
1039
        /** @var LanInterfaces $res */
1040
        $res = LanInterfaces::find('disabled=0');
1041
1042
        return $res->toArray();
1043
    }
1044
1045
    /**
1046
     * Performs deconfiguration of the udhcpc configuration.
1047
     */
1048
    public function udhcpcConfigureDeconfig(): void
1049
    {
1050
        $interface = trim(getenv('interface'));
1051
1052
        // For MIKO LFS Edition.
1053
        $busyboxPath = Util::which('busybox');
1054
1055
        // Bring the interface up.
1056
        Processes::mwExec("{$busyboxPath} ifconfig {$interface} up");
1057
1058
        // Set a default IP configuration for the interface.
1059
        Processes::mwExec("{$busyboxPath} ifconfig {$interface} 192.168.2.1 netmask 255.255.255.0");
1060
    }
1061
1062
    /**
1063
     * Updates the network settings with the provided data.
1064
     * @param array $data The network settings data to update.
1065
     */
1066
    public function updateNetSettings(array $data): void
1067
    {
1068
        $res = LanInterfaces::findFirst("internet = '1'");
1069
        $update_inet = false;
1070
        if ($res === null) {
1071
            // If no interface with internet connection is found, get the first interface.
1072
            $res = LanInterfaces::findFirst();
1073
            $update_inet = true;
1074
        }
1075
1076
        if ($res !== null) {
1077
            foreach ($data as $key => $value) {
1078
                $res->$key = $value;
1079
            }
1080
            if ($update_inet === true) {
1081
                $res->internet = 1;
1082
            }
1083
            $res->save();
1084
        }
1085
    }
1086
1087
    /**
1088
     * Retrieves information about all network interfaces.
1089
     * @return array An array of network interfaces with their respective information.
1090
     */
1091
    public function getInterfaces(): array
1092
    {
1093
        // Get all PCI interface names (network interfaces).
1094
        $i_names = $this->getInterfacesNames();
1095
        $if_list = [];
1096
        foreach ($i_names as $i) {
1097
            $if_list[$i] = $this->getInterface($i);
1098
        }
1099
1100
        return $if_list;
1101
    }
1102
1103
    /**
1104
     * Retrieves information about a specific network interface.
1105
     * @param string $name The name of the network interface.
1106
     * @return array An array containing the interface information.
1107
     */
1108
    public function getInterface(string $name): array
1109
    {
1110
        $interface = [];
1111
1112
        // Get ifconfig's output for the specified interface.
1113
        $busyboxPath = Util::which('busybox');
1114
        Processes::mwExec("{$busyboxPath} ifconfig $name 2>/dev/null", $output);
1115
        $output = implode(" ", $output);
1116
1117
        // Parse MAC address.
1118
        preg_match("/HWaddr (\S+)/", $output, $matches);
1119
        $interface['mac'] = (count($matches) > 0) ? $matches[1] : '';
1120
1121
        // Parse IP address.
1122
        preg_match("/inet addr:(\S+)/", $output, $matches);
1123
        $interface['ipaddr'] = (count($matches) > 0) ? $matches[1] : '';
1124
1125
        // Parse subnet mask.
1126
        preg_match("/Mask:(\S+)/", $output, $matches);
1127
        $subnet = (count($matches) > 0) ? $this->netMaskToCidr($matches[1]) : '';
1128
        $interface['subnet'] = $subnet;
1129
1130
        // Check if the interface is up.
1131
        preg_match("/\s+(UP)\s+/", $output, $matches);
1132
        $status = (count($matches) > 0) ? $matches[1] : '';
1133
        if ($status === "UP") {
1134
            $interface['up'] = true;
1135
        } else {
1136
            $interface['up'] = false;
1137
        }
1138
        $busyboxPath = Util::which('busybox');
1139
1140
        // Get the default gateway.
1141
        $grepPath = Util::which('grep');
1142
        $cutPath = Util::which('cut');
1143
        $routePath = Util::which('route');
1144
1145
        Processes::mwExec(
1146
            "{$busyboxPath} {$routePath} -n | {$grepPath} {$name} | {$grepPath} \"^0.0.0.0\" | {$cutPath} -d ' ' -f 10",
1147
            $matches
1148
        );
1149
        $gw = (count($matches) > 0) ? $matches[0] : '';
1150
        if (Verify::isIpAddress($gw)) {
1151
            $interface['gateway'] = $gw;
1152
        }
1153
1154
        // Get DNS servers.
1155
        $catPath = Util::which('cat');
1156
        Processes::mwExec("{$catPath} /etc/resolv.conf | {$grepPath} nameserver | {$cutPath} -d ' ' -f 2", $dnsout);
1157
1158
        $dnsSrv = [];
1159
        foreach ($dnsout as $line) {
1160
            if (Verify::isIpAddress($line)) {
1161
                $dnsSrv[] = $line;
1162
            }
1163
        }
1164
        $interface['dns'] = $dnsSrv;
1165
1166
        return $interface;
1167
    }
1168
1169
    /**
1170
     * Update external IP address
1171
     */
1172
    public function updateExternalIp(): void
1173
    {
1174
        $ipInfoResult = SysinfoManagementProcessor::getExternalIpInfo();
1175
        if ($ipInfoResult->success && isset($ipInfoResult->data['ip'])) {
1176
            $currentIP = $ipInfoResult->data['ip'];
1177
            $lanData = LanInterfaces::find('autoUpdateExtIp=1');
1178
            foreach ($lanData as $lan) {
1179
                $oldExtIp = $lan->extipaddr;
1180
                $parts = explode(':', $oldExtIp);
1181
                $oldIP = $parts[0]; // Only IP part of the address
1182
                $port = isset($parts[1]) ? ':' . $parts[1] : '';
1183
                if ($oldIP !== $currentIP) {
1184
                    $newExtIp = $currentIP . $port;
1185
                    $lan->extipaddr = $newExtIp;
1186
                    if ($lan->save()) {
1187
                        Util::sysLogMsg(__METHOD__, "External IP address updated for interface {$lan->interface}");
1188
                    }
1189
                }
1190
            }
1191
        }
1192
    }
1193
1194
    /**
1195
     * Add static routes based on DHCP provided static routes information.
1196
     * Parses the `staticroutes` environment variable and adds each route to the system.
1197
     *
1198
     * @param string $staticRoutes The static routes string from DHCP, format: "destination gateway"
1199
     * @param string $interface The network interface to add routes to, e.g., eth0
1200
     * @return void
1201
     */
1202
    private function addStaticRoutes(string $staticRoutes, string $interface): void
1203
    {
1204
        if (empty($staticRoutes)) {
1205
            return;
1206
        }
1207
1208
        // Split the static routes string into individual routes.
1209
        $routes = explode(' ', $staticRoutes);
1210
        $processedRoutes = []; // To keep track of processed routes and avoid duplicates.
1211
1212
        $busyboxPath = Util::which('busybox');
1213
1214
        // Iterate through the routes, adding each to the system.
1215
        for ($i = 0; $i < count($routes); $i += 2) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1216
            $destination = $routes[$i];
1217
            $gateway = $routes[$i + 1] ?? '';
1218
1219
            // Check if the route has already been processed to prevent duplicates.
1220
            if (!empty($destination) && !empty($gateway) && !in_array($destination, $processedRoutes)) {
1221
                Processes::mwExec("$busyboxPath ip route add $destination via $gateway dev $interface");
1222
                $processedRoutes[] = $destination; // Mark this route as processed.
1223
            }
1224
        }
1225
    }
1226
1227
    /**
1228
     * Add custom static routes based on the `/etc/static-routes` file.
1229
     *
1230
     * @param string $interface The network interface to add routes to, e.g., eth0
1231
     * @return void
1232
     */
1233
    private function addCustomStaticRoutes(string $interface): void
1234
    {
1235
        if (file_exists('/etc/static-routes')) {
1236
            $busyboxPath = Util::which('busybox');
1237
            $grepPath = Util::which('grep');
1238
            $awkPath = Util::which('awk');
1239
            $catPath = Util::which('cat');
1240
            $shPath = Util::which('sh');
1241
            Processes::mwExec(
1242
                "{$catPath} /etc/static-routes | {$grepPath} '^rout' | {$busyboxPath} {$awkPath} -F ';' '{print $1}' | {$grepPath} '{$interface}' | {$shPath}"
1243
            );
1244
        }
1245
    }
1246
1247
}