Project

General

Profile

Download (142 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * util.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2004-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-2025 Rubicon Communications, LLC (Netgate)
9
 * All rights reserved.
10
 *
11
 * originally part of m0n0wall (http://m0n0.ch/wall)
12
 * Copyright (c) 2003-2004 Manuel Kasper <mk@neon1.net>.
13
 * All rights reserved.
14
 *
15
 * Licensed under the Apache License, Version 2.0 (the "License");
16
 * you may not use this file except in compliance with the License.
17
 * You may obtain a copy of the License at
18
 *
19
 * http://www.apache.org/licenses/LICENSE-2.0
20
 *
21
 * Unless required by applicable law or agreed to in writing, software
22
 * distributed under the License is distributed on an "AS IS" BASIS,
23
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
 * See the License for the specific language governing permissions and
25
 * limitations under the License.
26
 */
27

    
28
//require_once('interfaces.inc');
29
require_once('Net/IPv6.php');
30
define('VIP_ALL', 1);
31
define('VIP_CARP', 2);
32
define('VIP_IPALIAS', 3);
33
// Special networks flags
34
define('SPECIALNET_NONE', 0);
35
define('SPECIALNET_ANY', 1);
36
define('SPECIALNET_COMPAT_ADDR', 22);
37
define('SPECIALNET_ADDR', 2);
38
define('SPECIALNET_COMPAT_ADDRAL', 33);
39
define('SPECIALNET_ADDRAL', 3);
40
define('SPECIALNET_NET', 4);
41
define('SPECIALNET_NETAL', 5);
42
define('SPECIALNET_SELF', 6);
43
define('SPECIALNET_CLIENTS', 7);
44
define('SPECIALNET_IFADDR', 8);
45
define('SPECIALNET_IFSUB', 9);
46
define('SPECIALNET_IFNET', 10);
47
define('SPECIALNET_GROUP', 11);
48
define('SPECIALNET_VIPS', 12);
49
define('SPECIALNET_IFADDR4', 13);
50
define('SPECIALNET_IFADDR6', 14);
51
define('SPECIALNET_VIPALIAS', 15);
52
define('SPECIALNET_CHECKPERM', 98);
53
define('SPECIALNET_EXCLUDE', 99);
54

    
55
function v6_range_to_prefixlen(string $start, string $end): string|false {
56
	$start_packed = inet_pton(Net_IPv6::uncompress($start, true));
57
	$end_packed = inet_pton(Net_IPv6::uncompress($end, true));
58
	if ($start_packed === false || $end_packed === false) {
59
		return (false);
60
	}
61

    
62
	$len = 0;
63
	foreach (str_split($start_packed ^ $end_packed) as $char) {
64
		$byte = ord($char);
65
		for ($i = 7; $i >= 0; $i--) {
66
			if (($byte >> $i) & 1) {
67
				break 2;
68
			}
69
			$len++;
70
		}
71
	}
72

    
73
	return ($len);
74
}
75

    
76
/**
77
 * Test the machine type
78
 *
79
 * @param string $arch		The architecture to test for
80
 * @param bool $strict		Use strict architecture matching
81
 *
82
 * @return bool
83
 */
84
function is_arch(string $arch, bool $strict = false): bool
85
{
86
	$sys_arch = php_uname('m');
87
	return (($arch == $sys_arch) || (!$strict && str_contains($sys_arch, $arch)));
88
}
89

    
90
/**
91
 * Test if the machine type is any arm
92
 *
93
 * @return bool
94
 */
95
function is_arm(): bool
96
{
97
	return (is_arch('arm', false));
98
}
99

    
100
/**
101
 * Test if the machine type is amd64
102
 *
103
 * @return bool
104
 */
105
function is_amd64(): bool
106
{
107
	return (is_arch('amd64', true));
108
}
109

    
110
/**
111
 * Test if the machine type is arm64
112
 */
113
function is_arm64(): bool
114
{
115
	return (is_arch('arm64', true));
116
}
117

    
118
/**
119
 * Test if the machine type is any 64bit
120
 */
121
function is_64bit(): bool
122
{
123
	return (is_amd64() || is_arm64());
124
}
125

    
126
/* kill a process by pid file */
127
function killbypid($pidfile, $waitfor = 0) {
128
	return sigkillbypid($pidfile, "TERM", $waitfor);
129
}
130

    
131
function isvalidpid(string $pidfile): bool {
132
	exec(implode(' ', [
133
		'/bin/pkill',
134
		'-0',
135
		'-F', $pidfile,
136
		'2>/dev/null'
137
	]), $_out, $ret);
138

    
139
	return ($ret === 0);
140
}
141

    
142
function is_process_running(string $name): bool {
143
	if (empty($name)) {
144
		return false;
145
	}
146

    
147
	exec(implode(' ', [
148
		'/bin/pkill',
149
		'-0',
150
		'-x', escapeshellarg($name),
151
		'2>/dev/null'
152
	]), $_out, $ret);
153

    
154
	return ($ret === 0);
155
}
156

    
157
function isvalidproc($proc) {
158
	return is_process_running($proc);
159
}
160

    
161
/* sigkill a process by pid file, and wait for it to terminate or remove the .pid file for $waitfor seconds */
162
/* return 1 for success and 0 for a failure */
163
function sigkillbypid(string $pidfile, string|int $sig, int $waitfor = 0): int {
164
	if (!isvalidpid($pidfile)) {
165
		return 0;
166
	}
167

    
168
	exec(implode(' ', [
169
		'/bin/pkill',
170
		"-${sig}",
171
		'-F', $pidfile,
172
		'2>/dev/null'
173
	]), $_out, $ret);
174

    
175
	$waitcounter = $waitfor * 10;
176
	while(isvalidpid($pidfile) && $waitcounter > 0) {
177
		$waitcounter = $waitcounter - 1;
178
		usleep(100000); /* 100ms */
179
	}
180

    
181
	return $ret;
182
}
183

    
184
/* kill a process by name */
185
function sigkillbyname($procname, $sig) {
186
	if (isvalidproc($procname)) {
187
		return mwexec("/usr/bin/killall " . escapeshellarg("-{$sig}") . " " . escapeshellarg($procname), true);
188
	}
189
}
190

    
191
/* kill a process by name */
192
function killbyname($procname) {
193
	if (isvalidproc($procname)) {
194
		mwexec("/usr/bin/killall " . escapeshellarg($procname));
195
	}
196
}
197

    
198
function is_subsystem_dirty($subsystem = "") {
199
	global $g;
200

    
201
	if ($subsystem == "") {
202
		return false;
203
	}
204

    
205
	if (file_exists("{$g['varrun_path']}/{$subsystem}.dirty")) {
206
		return true;
207
	}
208

    
209
	return false;
210
}
211

    
212
function mark_subsystem_dirty($subsystem = "") {
213
	global $g;
214

    
215
	if (!file_put_contents("{$g['varrun_path']}/{$subsystem}.dirty", "DIRTY")) {
216
		log_error(sprintf(gettext("WARNING: Could not mark subsystem: %s dirty"), $subsystem));
217
	}
218
}
219

    
220
function clear_subsystem_dirty($subsystem = "") {
221
	global $g;
222

    
223
	@unlink("{$g['varrun_path']}/{$subsystem}.dirty");
224
}
225

    
226
function clear_filter_subsystems_dirty() {
227
	clear_subsystem_dirty('aliases');
228
	clear_subsystem_dirty('filter');
229
	clear_subsystem_dirty('natconf');
230
	clear_subsystem_dirty('shaper');
231
}
232

    
233
/* lock configuration file */
234
function lock($lock, $op = LOCK_SH) {
235
	global $g;
236
	if (!$lock) {
237
		die(gettext("WARNING: A name must be given as parameter to lock() function."));
238
	}
239
	if (!file_exists("{$g['tmp_path']}/{$lock}.lock")) {
240
		@touch("{$g['tmp_path']}/{$lock}.lock");
241
		@chmod("{$g['tmp_path']}/{$lock}.lock", 0666);
242
	}
243
	if ($fp = fopen("{$g['tmp_path']}/{$lock}.lock", "w")) {
244
		if (flock($fp, $op)) {
245
			return $fp;
246
		} else {
247
			fclose($fp);
248
		}
249
	}
250
}
251

    
252
function try_lock($lock, $timeout = 5) {
253
	global $g;
254
	if (!$lock) {
255
		die(gettext("WARNING: A name must be given as parameter to try_lock() function."));
256
	}
257
	if (!file_exists("{$g['tmp_path']}/{$lock}.lock")) {
258
		@touch("{$g['tmp_path']}/{$lock}.lock");
259
		@chmod("{$g['tmp_path']}/{$lock}.lock", 0666);
260
	}
261
	if ($fp = fopen("{$g['tmp_path']}/{$lock}.lock", "w")) {
262
		$trycounter = 0;
263
		while (!flock($fp, LOCK_EX | LOCK_NB)) {
264
			if ($trycounter >= $timeout) {
265
				fclose($fp);
266
				return NULL;
267
			}
268
			sleep(1);
269
			$trycounter++;
270
		}
271

    
272
		return $fp;
273
	}
274

    
275
	return NULL;
276
}
277

    
278
/* unlock configuration file */
279
function unlock($cfglckkey = 0)
280
{
281
	if (!is_resource($cfglckkey))
282
		return;
283

    
284
	flock($cfglckkey, LOCK_UN);
285
	fclose($cfglckkey);
286
}
287

    
288
/* unlock forcefully configuration file */
289
function unlock_force($lock) {
290
	global $g;
291

    
292
	@unlink("{$g['tmp_path']}/{$lock}.lock");
293
}
294

    
295
function send_event($cmd) {
296
	global $g;
297

    
298
	if (!isset($g['event_address'])) {
299
		$g['event_address'] = "unix:///var/run/check_reload_status";
300
	}
301

    
302
	$try = 0;
303
	while ($try < 3) {
304
		$fd = @fsockopen(g_get('event_address'));
305
		if ($fd) {
306
			fwrite($fd, $cmd);
307
			$resp = fread($fd, 4096);
308
			if ($resp != "OK\n") {
309
				log_error("send_event: sent {$cmd} got {$resp}");
310
			}
311
			fclose($fd);
312
			$try = 3;
313
		} else if (!is_process_running("check_reload_status")) {
314
			mwexec_bg("/usr/bin/nice -n20 /usr/local/sbin/check_reload_status");
315
		}
316
		$try++;
317
	}
318
}
319

    
320
function send_multiple_events($cmds) {
321
	global $g;
322

    
323
	if (!isset($g['event_address'])) {
324
		$g['event_address'] = "unix:///var/run/check_reload_status";
325
	}
326

    
327
	if (!is_array($cmds)) {
328
		return;
329
	}
330

    
331
	$try = 0;
332
	while ($try < 3) {
333
		$fd = @fsockopen(g_get('event_address'));
334
		if ($fd) {
335
			foreach ($cmds as $cmd) {
336
				fwrite($fd, $cmd);
337
				$resp = fread($fd, 4096);
338
				if ($resp != "OK\n") {
339
					log_error("send_event: sent {$cmd} got {$resp}");
340
				}
341
			}
342
			fclose($fd);
343
			$try = 3;
344
		} else if (!is_process_running("check_reload_status")) {
345
			mwexec_bg("/usr/bin/nice -n20 /usr/local/sbin/check_reload_status");
346
		}
347
		$try++;
348
	}
349
}
350

    
351
/**
352
 * Test if a kernel module exists on disk in well-known locations
353
 *
354
 * Locations:
355
 *   > /boot/kernel
356
 *   > /bood/modules
357
 *
358
 * @param string $module_name		The name of the kernel module
359
 * @param string ...$add_dirs		Additional directories to search
360
 *
361
 * @return bool
362
 */
363
function is_module_available(string $module_name, string ...$add_dirs): bool
364
{
365
	/* bail if module is already loaded */
366
	if (is_module_loaded($module_name)) {
367
		return (true);
368
	}
369

    
370
	/* ensure the $module_name ends with .ko */
371
	if (!str_ends_with($module_name, '.ko')) {
372
		$module_name .= '.ko';
373
	}
374

    
375
	/* build list of well-known locations */
376
	$mod_dirs = [
377
		'/boot/kernel',
378
		'/boot/modules',
379
		...$add_dirs
380
	];
381

    
382
	/* filter list of well-known locations and cast to bool */
383
	return ((bool) array_filter($mod_dirs, function($mod_dir) use ($module_name) {
384
		return (file_exists($mod_dir . '/' . $module_name));
385
	}));
386
}
387

    
388
/**
389
 * Test if a kernel module is loaded
390
 *
391
 * @param string $module_name	The name of the kernel module
392
 *
393
 * @param bool
394
 */
395
function is_module_loaded(string $module_name): bool
396
{
397
	/* sanitize input */
398
	$module_name = escapeshellarg(rtrim($module_name, '.ko'));
399

    
400
	$base_cmd = ['/sbin/kldstat', '-q'];
401

    
402
	/* first pass test by module name, tests for in-kernel modules */
403
	$cmd = implode(' ', [...$base_cmd, '-m', $module_name]);
404
	exec($cmd, $out, $rc);
405

    
406
	/* bailout early if we found it */
407
	if ($rc === 0) {
408
		return (true);
409
	}
410

    
411
	/* last pass test by file name */
412
	$cmd = implode(' ', [...$base_cmd, '-n', $module_name]);
413
	exec($cmd, $out, $rc);
414

    
415
	return ($rc === 0);
416
}
417

    
418
/* validate non-negative numeric string, or equivalent numeric variable */
419
function is_numericint($arg) {
420
	return (((is_int($arg) && $arg >= 0) || (is_string($arg) && strlen($arg) > 0 && ctype_digit(strval($arg)))) ? true : false);
421
}
422

    
423
/* Generate the (human readable) ipv4 or ipv6 subnet address (i.e., netmask, or subnet start IP)
424
   given an (human readable) ipv4 or ipv6 host address and subnet bit count */
425
function gen_subnet($ipaddr, $bits) {
426
	if (($sn = gen_subnetv6($ipaddr, $bits)) == '') {
427
		$sn = gen_subnetv4($ipaddr, $bits);  // try to avoid rechecking IPv4/v6
428
	}
429
	return $sn;
430
}
431

    
432
/* same as gen_subnet() but accepts IPv4 only */
433
function gen_subnetv4($ipaddr, $bits) {
434
	if (is_ipaddrv4($ipaddr) && is_numericint($bits) && $bits <= 32) {
435
		if ($bits == 0) {
436
			return '0.0.0.0';  // avoids <<32
437
		}
438
		return long2ip32(ip2long($ipaddr) & ((0xFFFFFFFF << (32 - $bits)) & 0xFFFFFFFF));
439
	}
440
	return "";
441
}
442

    
443
/* same as gen_subnet() but accepts IPv6 only */
444
function gen_subnetv6($ipaddr, $bits) {
445
	if (is_ipaddrv6($ipaddr) && is_numericint($bits) && $bits <= 128) {
446
		return text_to_compressed_ip6(Net_IPv6::getNetmask($ipaddr, $bits));
447
	}
448
	return "";
449
}
450

    
451
/* Generate the (human readable) ipv4 or ipv6 subnet end address (i.e., highest address, end IP, or IPv4 broadcast address)
452
   given an (human readable) ipv4 or ipv6 host address and subnet bit count. */
453
function gen_subnet_max($ipaddr, $bits) {
454
	if (($sn = gen_subnetv6_max($ipaddr, $bits)) == '') {
455
		$sn = gen_subnetv4_max($ipaddr, $bits);  // try to avoid rechecking IPv4/v6
456
	}
457
	return $sn;
458
}
459

    
460
/* same as gen_subnet_max() but validates IPv4 only */
461
function gen_subnetv4_max($ipaddr, $bits) {
462
	if (is_ipaddrv4($ipaddr) && is_numericint($bits) && $bits <= 32) {
463
		if ($bits == 32) {
464
			return $ipaddr;
465
		}
466
		return long2ip32(ip2long($ipaddr) | (~gen_subnet_mask_long($bits) & 0xFFFFFFFF));
467
	}
468
	return "";
469
}
470

    
471
/* same as gen_subnet_max() but validates IPv6 only */
472
function gen_subnetv6_max($ipaddr, $bits) {
473
	if (is_ipaddrv6($ipaddr) && is_numericint($bits) && $bits <= 128) {
474
		$endip_bin = substr(ip6_to_bin($ipaddr), 0, $bits) . str_repeat('1', 128 - $bits);
475
		return bin_to_compressed_ip6($endip_bin);
476
	}
477
	return "";
478
}
479

    
480
/* returns a subnet mask (long given a bit count) */
481
function gen_subnet_mask_long($bits) {
482
	$sm = 0;
483
	for ($i = 0; $i < $bits; $i++) {
484
		$sm >>= 1;
485
		$sm |= 0x80000000;
486
	}
487
	return $sm;
488
}
489

    
490
/* same as above but returns a string */
491
function gen_subnet_mask($bits) {
492
	return long2ip32(gen_subnet_mask_long($bits));
493
}
494

    
495
/* Convert a prefix length to an IPv6 address-like mask notation. Very rare but at least ntp needs it. See #4463 */
496
function gen_subnet_mask_v6($bits) {
497
	/* Binary representation of the prefix length */
498
	$bin = str_repeat('1', $bits);
499
	/* Pad right with zeroes to reach the full address length */
500
	$bin = str_pad($bin, 128, '0', STR_PAD_RIGHT);
501
	/* Convert back to an IPv6 address style notation */
502
	return bin_to_ip6($bin);
503
}
504

    
505
/* Convert long int to IPv4 address
506
   Returns '' if not valid IPv4 (including if any bits >32 are non-zero) */
507
function long2ip32($ip) {
508
	if (PHP_INT_SIZE == 4) {
509
		/* Convert long int on 32bit ARM, see NG #5445 */
510
		return long2ip((int) ($ip + 0));
511
	}
512
	return long2ip($ip & 0xFFFFFFFF);
513
}
514

    
515
/* Convert IPv4 address to long int, truncated to 32-bits to avoid sign extension on 64-bit platforms.
516
   Returns '' if not valid IPv4. */
517
function ip2long32($ip) {
518
	return (ip2long($ip) & 0xFFFFFFFF);
519
}
520

    
521
/* Convert IPv4 address to unsigned long int.
522
   Returns '' if not valid IPv4. */
523
function ip2ulong($ip) {
524
	return sprintf("%u", ip2long32($ip));
525
}
526

    
527
/*
528
 * Convert IPv6 address to binary
529
 *
530
 * Obtained from: pear-Net_IPv6
531
 */
532
function ip6_to_bin($ip) {
533
	$binstr = '';
534

    
535
	$ip = Net_IPv6::removeNetmaskSpec($ip);
536
	$ip = Net_IPv6::Uncompress($ip);
537

    
538
	$parts = explode(':', $ip);
539

    
540
	foreach ( $parts as $v ) {
541

    
542
		$str     = base_convert($v, 16, 2);
543
		$binstr .= str_pad($str, 16, '0', STR_PAD_LEFT);
544

    
545
	}
546

    
547
	return $binstr;
548
}
549

    
550
/*
551
 * Convert IPv6 binary to uncompressed address
552
 *
553
 * Obtained from: pear-Net_IPv6
554
 */
555
function bin_to_ip6($bin) {
556
	$ip = "";
557

    
558
	if (strlen($bin) < 128) {
559
		$bin = str_pad($bin, 128, '0', STR_PAD_LEFT);
560
	}
561

    
562
	$parts = str_split($bin, "16");
563

    
564
	foreach ( $parts as $v ) {
565
		$str = base_convert($v, 2, 16);
566
		$ip .= $str.":";
567
	}
568

    
569
	$ip = substr($ip, 0, -1);
570

    
571
	return $ip;
572
}
573

    
574
/*
575
 * Convert IPv6 binary to compressed address
576
 */
577
function bin_to_compressed_ip6($bin) {
578
	return text_to_compressed_ip6(bin_to_ip6($bin));
579
}
580

    
581
/*
582
 * Convert textual IPv6 address string to compressed address
583
 */
584
function text_to_compressed_ip6($text) {
585
	// Force re-compression by passing parameter 2 (force) true.
586
	// This ensures that supposedly-compressed formats are uncompressed
587
	// first then re-compressed into strictly correct form.
588
	// e.g. 2001:0:0:4:0:0:0:1
589
	// 2001::4:0:0:0:1 is a strictly-incorrect compression,
590
	// but maybe the user entered it like that.
591
	// The "force" parameter will ensure it is returned as:
592
	// 2001:0:0:4::1
593
	return Net_IPv6::compress($text, true);
594
}
595

    
596
/* Find out how many IPs are contained within a given IP range
597
 *  e.g. 192.168.0.0 to 192.168.0.255 returns 256
598
 */
599
function ip_range_size_v4($startip, $endip) {
600
	if (is_ipaddrv4($startip) && is_ipaddrv4($endip)) {
601
		// Operate as unsigned long because otherwise it wouldn't work
602
		//   when crossing over from 127.255.255.255 / 128.0.0.0 barrier
603
		return abs(ip2ulong($startip) - ip2ulong($endip)) + 1;
604
	}
605
	return -1;
606
}
607

    
608
/* Find the smallest possible subnet mask which can contain a given number of IPs
609
 *  e.g. 512 IPs can fit in a /23, but 513 IPs need a /22
610
 */
611
function find_smallest_cidr_v4($number) {
612
	$smallest = 1;
613
	for ($b=32; $b > 0; $b--) {
614
		$smallest = ($number <= pow(2, $b)) ? $b : $smallest;
615
	}
616
	return (32-$smallest);
617
}
618

    
619
/* Return the previous IP address before the given address */
620
function ip_before($ip, $offset = 1) {
621
	return long2ip32(ip2long($ip) - $offset);
622
}
623

    
624
/* Return the next IP address after the given address */
625
function ip_after($ip, $offset = 1) {
626
	return long2ip32(ip2long($ip) + $offset);
627
}
628

    
629
/**
630
 * Return the next IPv6 address after the given address
631
 * 
632
 * @param string $ip IPv6 address
633
 * 
634
 * @return string|false IPv6 address; false if it cannot be incremented
635
 */
636
function ip6_after(string $ip): string|false {
637
	/* Convert the IPv6 string to its in_addr representation,
638
	 * increment it by one, then convert it back to a string. */
639
	return inet_ntop(str_pad(gmp_export(
640
		str_increment((string) gmp_import(inet_pton($ip)))
641
	), 16, "\0", STR_PAD_LEFT));
642
}
643

    
644
/* Return true if the first IP is 'before' the second */
645
function ip_less_than($ip1, $ip2) {
646
	// Compare as unsigned long because otherwise it wouldn't work when
647
	//   crossing over from 127.255.255.255 / 128.0.0.0 barrier
648
	return ip2ulong($ip1) < ip2ulong($ip2);
649
}
650

    
651
/* Return true if the first IP is 'after' the second */
652
function ip_greater_than($ip1, $ip2) {
653
	// Compare as unsigned long because otherwise it wouldn't work
654
	//   when crossing over from 127.255.255.255 / 128.0.0.0 barrier
655
	return ip2ulong($ip1) > ip2ulong($ip2);
656
}
657

    
658
/* compare two IP addresses */
659
function ipcmp($a, $b) {
660
	if (is_subnet($a)) {
661
		$a = explode('/', $a)[0];
662
	}
663
	if (is_subnet($b)) {
664
		$b = explode('/', $b)[0];
665
	}
666
	if (ip_less_than($a, $b)) {
667
		return -1;
668
	} else if (ip_greater_than($a, $b)) {
669
		return 1;
670
	} else {
671
		return 0;
672
	}
673
}
674

    
675
/* Convert a range of IPv4 addresses to an array of individual addresses. */
676
/* Note: IPv6 ranges are not yet supported here. */
677
function ip_range_to_address_array($startip, $endip, $max_size = 5000) {
678
	if (!is_ipaddrv4($startip) || !is_ipaddrv4($endip)) {
679
		return false;
680
	}
681

    
682
	if (ip_greater_than($startip, $endip)) {
683
		// Swap start and end so we can process sensibly.
684
		$temp = $startip;
685
		$startip = $endip;
686
		$endip = $temp;
687
	}
688

    
689
	if (ip_range_size_v4($startip, $endip) > $max_size) {
690
		return false;
691
	}
692

    
693
	// Container for IP addresses within this range.
694
	$rangeaddresses = array();
695
	$end_int = ip2ulong($endip);
696
	for ($ip_int = ip2ulong($startip); $ip_int <= $end_int; $ip_int++) {
697
		$rangeaddresses[] = long2ip32($ip_int);
698
	}
699

    
700
	return $rangeaddresses;
701
}
702

    
703
/*
704
 * Convert an IPv4 or IPv6 IP range to an array of subnets which can contain the range.
705
 * Algorithm and embodying code PD'ed by Stilez - enjoy as you like :-)
706
 *
707
 * Documented on pfsense dev list 19-20 May 2013. Summary:
708
 *
709
 * The algorithm looks at patterns of 0's and 1's in the least significant bit(s), whether IPv4 or IPv6.
710
 * These are all that needs checking to identify a _guaranteed_ correct, minimal and optimal subnet array.
711
 *
712
 * As a result, string/binary pattern matching of the binary IP is very efficient. It uses just 2 pattern-matching rules
713
 * to chop off increasingly larger subnets at both ends that can't be part of larger subnets, until nothing's left.
714
 *
715
 * (a) If any range has EITHER low bit 1 (in startip) or 0 (in endip), that end-point is _always guaranteed_ to be optimally
716
 * represented by its own 'single IP' CIDR; the remaining range then shrinks by one IP up or down, causing the new end-point's
717
 * low bit to change from 1->0 (startip) or 0->1 (endip). Only one edge case needs checking: if a range contains exactly 2
718
 * adjacent IPs of this format, then the two IPs themselves are required to span it, and we're done.
719
 * Once this rule is applied, the remaining range is _guaranteed_ to end in 0's and 1's so rule (b) can now be used, and its
720
 * low bits can now be ignored.
721
 *
722
 * (b) If any range has BOTH startip and endip ending in some number of 0's and 1's respectively, these low bits can
723
 * *always* be ignored and "bit-shifted" for subnet spanning. So provided we remember the bits we've place-shifted, we can
724
 * _always_ right-shift and chop off those bits, leaving a smaller range that has EITHER startip ending in 1 or endip ending
725
 * in 0 (ie can now apply (a) again) or the entire range has vanished and we're done.
726
 * We then loop to redo (a) again on the remaining (place shifted) range until after a few loops, the remaining (place shifted)
727
 * range 'vanishes' by meeting the exit criteria of (a) or (b), and we're done.
728
 */
729
function ip_range_to_subnet_array($ip1, $ip2) {
730

    
731
	if (is_ipaddrv4($ip1) && is_ipaddrv4($ip2)) {
732
		$proto = 'ipv4';  // for clarity
733
		$bits = 32;
734
		$ip1bin = decbin(ip2long32($ip1));
735
		$ip2bin = decbin(ip2long32($ip2));
736
	} elseif (is_ipaddrv6($ip1) && is_ipaddrv6($ip2)) {
737
		$proto = 'ipv6';
738
		$bits = 128;
739
		$ip1bin = ip6_to_bin($ip1);
740
		$ip2bin = ip6_to_bin($ip2);
741
	} else {
742
		return array();
743
	}
744

    
745
	// it's *crucial* that binary strings are guaranteed the expected length;  do this for certainty even though for IPv6 it's redundant
746
	$ip1bin = str_pad($ip1bin, $bits, '0', STR_PAD_LEFT);
747
	$ip2bin = str_pad($ip2bin, $bits, '0', STR_PAD_LEFT);
748

    
749
	if ($ip1bin == $ip2bin) {
750
		return array($ip1 . '/' . $bits); // exit if ip1=ip2 (trivial case)
751
	}
752

    
753
	if ($ip1bin > $ip2bin) {
754
		list ($ip1bin, $ip2bin) = array($ip2bin, $ip1bin);  // swap if needed (ensures ip1 < ip2)
755
	}
756

    
757
	$rangesubnets = array();
758
	$netsize = 0;
759

    
760
	do {
761
		// at loop start, $ip1 is guaranteed strictly less than $ip2 (important for edge case trapping and preventing accidental binary wrapround)
762
		// which means the assignments $ip1 += 1 and $ip2 -= 1 will always be "binary-wrapround-safe"
763

    
764
		// step #1 if start ip (as shifted) ends in any '1's, then it must have a single cidr to itself (any cidr would include the '0' below it)
765

    
766
		if (substr($ip1bin, -1, 1) == '1') {
767
			// the start ip must be in a separate one-IP cidr range
768
			$new_subnet_ip = substr($ip1bin, $netsize, $bits - $netsize) . str_repeat('0', $netsize);
769
			$rangesubnets[$new_subnet_ip] = $bits - $netsize;
770
			$n = strrpos($ip1bin, '0');  //can't be all 1's
771
			$ip1bin = ($n == 0 ? '' : substr($ip1bin, 0, $n)) . '1' . str_repeat('0', $bits - $n - 1);  // BINARY VERSION OF $ip1 += 1
772
		}
773

    
774
		// step #2, if end ip (as shifted) ends in any zeros then that must have a cidr to itself (as cidr cant span the 1->0 gap)
775

    
776
		if (substr($ip2bin, -1, 1) == '0') {
777
			// the end ip must be in a separate one-IP cidr range
778
			$new_subnet_ip = substr($ip2bin, $netsize, $bits - $netsize) . str_repeat('0', $netsize);
779
			$rangesubnets[$new_subnet_ip] = $bits - $netsize;
780
			$n = strrpos($ip2bin, '1');  //can't be all 0's
781
			$ip2bin = ($n == 0 ? '' : substr($ip2bin, 0, $n)) . '0' . str_repeat('1', $bits - $n - 1);  // BINARY VERSION OF $ip2 -= 1
782
			// already checked for the edge case where end = start+1 and start ends in 0x1, above, so it's safe
783
		}
784

    
785
		// this is the only edge case arising from increment/decrement.
786
		// it happens if the range at start of loop is exactly 2 adjacent ips, that spanned the 1->0 gap. (we will have enumerated both by now)
787

    
788
		if ($ip2bin < $ip1bin) {
789
			continue;
790
		}
791

    
792
		// step #3 the start and end ip MUST now end in '0's and '1's respectively
793
		// so we have a non-trivial range AND the last N bits are no longer important for CIDR purposes.
794

    
795
		$shift = $bits - max(strrpos($ip1bin, '0'), strrpos($ip2bin, '1'));  // num of low bits which are '0' in ip1 and '1' in ip2
796
		$ip1bin = str_repeat('0', $shift) . substr($ip1bin, 0, $bits - $shift);
797
		$ip2bin = str_repeat('0', $shift) . substr($ip2bin, 0, $bits - $shift);
798
		$netsize += $shift;
799
		if ($ip1bin == $ip2bin) {
800
			// we're done.
801
			$new_subnet_ip = substr($ip1bin, $netsize, $bits - $netsize) . str_repeat('0', $netsize);
802
			$rangesubnets[$new_subnet_ip] = $bits - $netsize;
803
			continue;
804
		}
805

    
806
		// at this point there's still a remaining range, and either startip ends with '1', or endip ends with '0'. So repeat cycle.
807
	} while ($ip1bin < $ip2bin);
808

    
809
	// subnets are ordered by bit size. Re sort by IP ("naturally") and convert back to IPv4/IPv6
810

    
811
	ksort($rangesubnets, SORT_STRING);
812
	$out = array();
813

    
814
	foreach ($rangesubnets as $ip => $netmask) {
815
		if ($proto == 'ipv4') {
816
			$i = str_split($ip, 8);
817
			$out[] = implode('.', array(bindec($i[0]), bindec($i[1]), bindec($i[2]), bindec($i[3]))) . '/' . $netmask;
818
		} else {
819
			$out[] = bin_to_compressed_ip6($ip) . '/' . $netmask;
820
		}
821
	}
822

    
823
	return $out;
824
}
825

    
826
/* returns true if $range is a valid pair of IPv4 or IPv6 addresses separated by a "-"
827
	false - if not a valid pair
828
	true (numeric 4 or 6) - if valid, gives type of addresses */
829
function is_iprange($range) {
830
	if (substr_count($range, '-') != 1) {
831
		return false;
832
	}
833
	list($ip1, $ip2) = explode ('-', $range);
834
	if (is_ipaddrv4($ip1) && is_ipaddrv4($ip2)) {
835
		return 4;
836
	}
837
	if (is_ipaddrv6($ip1) && is_ipaddrv6($ip2)) {
838
		return 6;
839
	}
840
	return false;
841
}
842

    
843
/* returns true if $ipaddr is a valid dotted IPv4 address or a IPv6
844
	false - not valid
845
	true (numeric 4 or 6) - if valid, gives type of address */
846
function is_ipaddr($ipaddr) {
847
	if (is_ipaddrv4($ipaddr)) {
848
		return 4;
849
	}
850
	if (is_ipaddrv6($ipaddr)) {
851
		return 6;
852
	}
853
	return false;
854
}
855

    
856
/* returns true if $ipaddr is a valid IPv6 address */
857
function is_ipaddrv6($ipaddr) {
858
	if (!is_string($ipaddr) || empty($ipaddr)) {
859
		return false;
860
	}
861
	/*
862
	 * While Net_IPv6::checkIPv6() considers IPv6/mask a valid IPv6,
863
	 * is_ipaddrv6() needs to be more strict to keep the compatibility
864
	 * with is_ipaddrv4().
865
	 */
866
	if (strstr($ipaddr, "/")) {
867
		return false;
868
	}
869
	if (strstr($ipaddr, "%") && is_linklocal($ipaddr)) {
870
		$tmpip = explode("%", $ipaddr);
871
		$ipaddr = $tmpip[0];
872
	}
873
	/*
874
	 * Net_IPv6::checkIPv6 does not reject multiple attempts at compression
875
	 * so we must check it beforehand.
876
	 * https://redmine.pfsense.org/issues/13069
877
	 */
878
	if (substr_count($ipaddr, '::') > 1) {
879
		return false;
880
	}
881
	/*
882
	 * Net_IPv6::checkIPv6 does not properly handle addresses with extra segments
883
	 * when compression is also used.
884
	 * https://redmine.pfsense.org/issues/16005
885
	 */
886
	try {
887
		return Net_IPv6::checkIPv6($ipaddr);
888
	} catch (\Throwable $th) {
889
		return false;
890
	}
891
}
892

    
893
function is_ipaddrv6_v4map($ipaddr) {
894
	/* check RFC4291 par 2.2.2 format, ex: fd00::1.2.3.4
895
	 * see https://redmine.pfsense.org/issues/11446 */
896
	if (is_ipaddrv6($ipaddr) && preg_match('/^[0-9a-f:]{2,30}[0-9.]{7,15}$/i', $ipaddr)) {
897
		return true;
898
	}
899
	return false;
900
}
901

    
902
/* returns true if $ipaddr is a valid dotted IPv4 address */
903
function is_ipaddrv4($ipaddr) {
904
	if (!is_string($ipaddr) || empty($ipaddr) || ip2long($ipaddr) === FALSE) {
905
		return false;
906
	}
907
	return true;
908
}
909

    
910
function is_mcast($ipaddr) {
911
	if (is_mcastv4($ipaddr)) {
912
		return 4;
913
	}
914
	if (is_mcastv6($ipaddr)) {
915
		return 6;
916
	}
917
	return false;
918
}
919

    
920
function is_mcastv4($ipaddr) {
921
	if (!is_ipaddrv4($ipaddr) ||
922
	    !preg_match('/^2(?:2[4-9]|3\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d?|0)){3}$/', $ipaddr)) {
923
		return false;
924
	}
925
	return true;
926
}
927

    
928
function is_mcastv6($ipaddr) {
929
	if (!is_ipaddrv6($ipaddr) || !preg_match('/^ff.+$/', $ipaddr)) {
930
		return false;
931
	}
932
	return true;
933
}
934

    
935
/* returns 4 or 6 respectively (== TRUE) if $ipaddr is a valid IPv4 or IPv6 linklocal address
936
   returns false if not a valid linklocal address */
937
function is_linklocal($ipaddr) {
938
	if (is_ipaddrv4($ipaddr)) {
939
		// input is IPv4
940
		// test if it's 169.254.x.x per rfc3927 2.1
941
		$ip4 = explode(".", $ipaddr);
942
		if ($ip4[0] == '169' && $ip4[1] == '254') {
943
			return 4;
944
		}
945
	} elseif (Net_IPv6::getAddressType($ipaddr) == NET_IPV6_LOCAL_LINK) {
946
		return 6;
947
	}
948
	return false;
949
}
950

    
951
/* returns scope of a linklocal address */
952
function get_ll_scope($addr) {
953
	if (!is_linklocal($addr) || !strstr($addr, "%")) {
954
		return "";
955
	}
956
	return explode("%", $addr)[1];
957
}
958

    
959
/* returns true if $ipaddr is a valid literal IPv6 address */
960
function is_literalipaddrv6($ipaddr) {
961
	if (substr($ipaddr,0,1) == '[' && substr($ipaddr,-1,1) == ']') {
962
		// if it's data wrapped in "[ ... ]" then test if middle part is valid IPv6
963
		return is_ipaddrv6(substr($ipaddr,1,-1));
964
	}
965
	return false;
966
}
967

    
968
/* returns true if $iport is a valid IPv4:port or [Literal IPv6]:port
969
	false - not valid
970
	true (numeric 4 or 6) - if valid, gives type of address */
971
function is_ipaddrwithport($ipport) {
972
	$c = strrpos($ipport, ":");
973
	if ($c === false) {
974
		return false;  // can't split at final colon if no colon exists
975
	}
976

    
977
	if (!is_port(substr($ipport, $c + 1))) {
978
		return false;  // no valid port after last colon
979
	}
980

    
981
	$ip = substr($ipport, 0, $c);  // else is text before last colon a valid IP
982
	if (is_literalipaddrv6($ip)) {
983
		return 6;
984
	} elseif (is_ipaddrv4($ip)) {
985
		return 4;
986
	} else {
987
		return false;
988
	}
989
}
990

    
991
function is_hostnamewithport($hostport) {
992
	$parts = explode(":", $hostport);
993
	// no need to validate with is_string(); if it's not a string then explode won't return 2 parts anyway
994
	if (count($parts) == 2) {
995
		return is_hostname($parts[0]) && is_port($parts[1]);
996
	}
997
	return false;
998
}
999

    
1000
function get_reserved_table_names($name = '', $type = '') {
1001
	global $reserved_table_names;
1002

    
1003
	if (!isset($name) || !isset($type) || !is_string($name) || !is_string($type)) {
1004
		return [];
1005
	}
1006

    
1007
	$return = [];
1008
	$name = strtolower($name);
1009
	$requested_types = explode(',', $type);
1010

    
1011
	// Check for given alias name.
1012
	if (!empty($name)) {
1013
		if (isset($reserved_table_names[$name]) && (empty($type) || in_array($reserved_table_names[$name]['type'], $requested_types))) {
1014
			$return = [$name => $reserved_table_names[$name]];
1015
		}
1016
		return $return;
1017
	}
1018

    
1019
	// Check for give alias type(s).
1020
	foreach ($reserved_table_names as $alias_name => $alias_info) {
1021
		if (empty($type) || in_array($alias_info['type'], $requested_types)) {
1022
			$return[$alias_name] = $alias_info;
1023
		}
1024
	}
1025
	return $return;
1026
}
1027

    
1028
function set_reserved_table_name($name, $value) {
1029
	global $reserved_table_names;
1030

    
1031
	if (empty($name) || !is_string($name) || !is_string($value)) {
1032
		return false;
1033
	}
1034

    
1035
	// Normalize the name.
1036
	$name = strtolower($name);
1037
	if (!isset($reserved_table_names[$name])) {
1038
		return false;
1039
	}
1040

    
1041
	if (is_numericint($value)) {
1042
		$value = strval($value);
1043
	} elseif (!isset($value) || !is_string($value)) {
1044
		return false;
1045
	}
1046

    
1047
	if ((($reserved_table_names[$name]['type'] == 'address') && is_ipaddr($value)) ||
1048
	    (($reserved_table_names[$name]['type'] == 'port') && is_port($value))) {
1049
		$reserved_table_names[$name]['address'] = $value;
1050
		return true;
1051
	}
1052

    
1053
	return false;
1054
}
1055

    
1056
/**
1057
 * Check if $ipaddr is a valid dotted IPv4 address or an alias thereof.
1058
 * Used in source/destination form input validation.
1059
 * @param mixed $ipaddr
1060
 * @return bool|int If an IP address, either 4 or 6 to denote the IP family.
1061
 */
1062
function is_ipaddroralias($ipaddr) {
1063
	if (empty($ipaddr)) {
1064
		return false;
1065
	}
1066

    
1067
	if (!empty(get_reserved_table_names($ipaddr, 'host,network,url,urltable'))) {
1068
		return true;
1069
	}
1070

    
1071
	if (is_alias($ipaddr)) {
1072
		foreach (config_get_path('aliases/alias', []) as $alias) {
1073
			if ($alias['name'] == $ipaddr && !preg_match("/port/i", $alias['type'])) {
1074
				return true;
1075
			}
1076
		}
1077
		return false;
1078
	} else {
1079
		return is_ipaddr($ipaddr);
1080
	}
1081

    
1082
}
1083

    
1084
/* returns true if $subnet is a valid IPv4 or IPv6 subnet in CIDR format
1085
	false - if not a valid subnet
1086
	true (numeric 4 or 6) - if valid, gives type of subnet */
1087
function is_subnet($subnet) {
1088
	if (is_string($subnet) && preg_match('/^(?:([0-9.]{7,15})|([0-9a-f:]{2,39}|[0-9a-f:]{2,30}[0-9.]{7,15}))\/(\d{1,3})$/i', $subnet, $parts)) {
1089
		if (is_ipaddrv4($parts[1]) && $parts[3] <= 32) {
1090
			return 4;
1091
		}
1092
		if (is_ipaddrv6($parts[2]) && $parts[3] <= 128) {
1093
			return 6;
1094
		}
1095
	}
1096
	return false;
1097
}
1098

    
1099
function is_v4($ip_or_subnet) {
1100
	return is_ipaddrv4($ip_or_subnet) || is_subnetv4($ip_or_subnet);
1101
}
1102

    
1103
function is_v6($ip_or_subnet) {
1104
	return is_ipaddrv6($ip_or_subnet) || is_subnetv6($ip_or_subnet);
1105
}
1106

    
1107
/**
1108
 * Checks if an IPv6 address is a Global Unicast Address (GUA).
1109
 * 
1110
 * @param string $ip_or_subnet IPv6 address or subnet
1111
 * 
1112
 * @return bool
1113
 */
1114
function is_v6gua(string $ip_or_subnet): bool {
1115
	if (is_subnet($ip_or_subnet) == 6) {
1116
		list($ip_or_subnet) = explode('/', $ip_or_subnet);
1117
	}
1118
	if (!is_ipaddrv6($ip_or_subnet)) {
1119
		return false;
1120
	}
1121
	if (ip_in_subnet($ip_or_subnet, '2000::/3')) {
1122
		return true;
1123
	} else {
1124
		return false;
1125
	}
1126
}
1127

    
1128
/* same as is_subnet() but accepts IPv4 only */
1129
function is_subnetv4($subnet) {
1130
	return (is_subnet($subnet) == 4);
1131
}
1132

    
1133
/* same as is_subnet() but accepts IPv6 only */
1134
function is_subnetv6($subnet) {
1135
	return (is_subnet($subnet) == 6);
1136
}
1137

    
1138
/* returns true if $subnet is a valid subnet in CIDR format or an alias thereof */
1139
function is_subnetoralias($subnet) {
1140
	if (empty($subnet)) {
1141
		return false;
1142
	}
1143

    
1144
	global $aliastable;
1145

    
1146
	$reserved_alias = get_reserved_table_names($subnet, 'host,network,url,urltable');
1147
	if (!empty($reserved_alias)) {
1148
		return boolval(is_subnet($reserved_alias[array_key_first($reserved_alias)]['address']));
1149
	}
1150

    
1151
	if (isset($aliastable[$subnet]) && is_subnet($aliastable[$subnet])) {
1152
		return true;
1153
	} else {
1154
		return is_subnet($subnet);
1155
	}
1156
}
1157

    
1158
/* Get number of addresses in an IPv4/IPv6 subnet (represented as a string)
1159
   optional $exact=true forces error (0) to be returned if it can't be represented exactly
1160
   Exact result not possible above PHP_MAX_INT which is about 2^31 addresses on x32 or 2^63 on x64
1161
   Returns 0 for bad data or if cannot represent size as an INT when $exact is set. */
1162
function subnet_size($subnet, $exact=false) {
1163
	$parts = explode("/", $subnet);
1164
	$iptype = is_ipaddr($parts[0]);
1165
	if (count($parts) == 2 && $iptype) {
1166
		return subnet_size_by_netmask($iptype, $parts[1], $exact);
1167
	}
1168
	return 0;
1169
}
1170

    
1171
/* Get number of addresses in an IPv4/IPv6 subnet (represented numerically as IP type + bits)
1172
   optional $exact=true forces error (0) to be returned if it can't be represented exactly
1173
   Hard to think where we might need to count exactly a huge subnet but an overflow detection option is probably sensible
1174
   Returns 0 for bad data or if cannot represent size as an INT when $exact is set. */
1175
function subnet_size_by_netmask($iptype, $bits, $exact=false) {
1176
	if (!is_numericint($bits)) {
1177
		return 0;
1178
	} elseif ($iptype == 4 && $bits <= 32) {
1179
		$snsize = 32 - $bits;
1180
	} elseif ($iptype == 6 && $bits <= 128) {
1181
		$snsize = 128 - $bits;
1182
	} else {
1183
		return 0;
1184
	}
1185

    
1186
	// 2**N returns an exact result as an INT if possible, and a float/double if not.
1187
	// Detect this switch, rather than comparing $result<=PHP_MAX_INT or $bits >=8*PHP_INT_SIZE as it's (probably) easier to get completely reliable
1188
	$result = 2 ** $snsize;
1189

    
1190
	if ($exact && !is_int($result)) {
1191
		//exact required but can't represent result exactly as an INT
1192
		return 0;
1193
	} else {
1194
		// result ok, will be an INT where possible (guaranteed up to 2^31 addresses on x32/x64) and a float for 'huge' subnets
1195
		return $result;
1196
	}
1197
}
1198

    
1199
/* function used by pfblockerng */
1200
function subnetv4_expand($subnet) {
1201
	$result = array();
1202
	list ($ip, $bits) = explode("/", $subnet);
1203
	$net = ip2long($ip);
1204
	$mask = (0xffffffff << (32 - $bits));
1205
	$net &= $mask;
1206
	$size = round(exp(log(2) * (32 - $bits)));
1207
	for ($i = 0; $i < $size; $i += 1) {
1208
		$result[] = long2ip32($net | $i);
1209
	}
1210
	return $result;
1211
}
1212

    
1213
/* find out whether two IPv4/IPv6 CIDR subnets overlap.
1214
   Note: CIDR overlap implies one is identical or included so largest sn will be the same */
1215
function check_subnets_overlap($subnet1, $bits1, $subnet2, $bits2) {
1216
	if (is_ipaddrv4($subnet1)) {
1217
		return check_subnetsv4_overlap($subnet1, $bits1, $subnet2, $bits2);
1218
	} else {
1219
		return check_subnetsv6_overlap($subnet1, $bits1, $subnet2, $bits2);
1220
	}
1221
}
1222

    
1223
/* find out whether two IPv4 CIDR subnets overlap.
1224
   Note: CIDR overlap means sn1/sn2 are identical or one is included in other. So sn using largest $bits will be the same  */
1225
function check_subnetsv4_overlap($subnet1, $bits1, $subnet2, $bits2) {
1226
	$largest_sn = min($bits1, $bits2);
1227
	$subnetv4_start1 = gen_subnetv4($subnet1, $largest_sn);
1228
	$subnetv4_start2 = gen_subnetv4($subnet2, $largest_sn);
1229

    
1230
	if ($subnetv4_start1 == '' || $subnetv4_start2 == '') {
1231
		// One or both args is not a valid IPv4 subnet
1232
		//FIXME: needs to return "bad data" not true/false if bad. For now return false, best we can do until fixed
1233
		return false;
1234
	}
1235
	return ($subnetv4_start1 == $subnetv4_start2);
1236
}
1237

    
1238
/* find out whether two IPv6 CIDR subnets overlap.
1239
   Note: CIDR overlap means sn1/sn2 are identical or one is included in other. So sn using largest $bits will be the same  */
1240
function check_subnetsv6_overlap($subnet1, $bits1, $subnet2, $bits2) {
1241
	$largest_sn = min($bits1, $bits2);
1242
	$subnetv6_start1 = gen_subnetv6($subnet1, $largest_sn);
1243
	$subnetv6_start2 = gen_subnetv6($subnet2, $largest_sn);
1244

    
1245
	if ($subnetv6_start1 == '' || $subnetv6_start2 == '') {
1246
		// One or both args is not a valid IPv6 subnet
1247
		//FIXME: needs to return "bad data" not true/false if bad. For now return false, best we can do until fixed
1248
		return false;
1249
	}
1250
	return ($subnetv6_start1 == $subnetv6_start2);
1251
}
1252

    
1253
/* return all PTR zones for a IPv6 network */
1254
function get_v6_ptr_zones($subnet, $bits) {
1255
	$result = array();
1256

    
1257
	if (!is_ipaddrv6($subnet)) {
1258
		return $result;
1259
	}
1260

    
1261
	if (!is_numericint($bits) || $bits > 128) {
1262
		return $result;
1263
	}
1264

    
1265
	/*
1266
	 * Find a small nibble boundary subnet mask
1267
	 * e.g. a /29 will create 8 /32 PTR zones
1268
	 */
1269
	$small_sn = $bits;
1270
	while ($small_sn % 4 != 0) {
1271
		$small_sn++;
1272
	}
1273

    
1274
	/* Get network prefix */
1275
	$small_subnet = Net_IPv6::getNetmask($subnet, $bits);
1276

    
1277
	/*
1278
	 * While small network is part of bigger one, increase 4-bit in last
1279
	 * digit to get next small network
1280
	 */
1281
	while (Net_IPv6::isInNetmask($small_subnet, $subnet, $bits)) {
1282
		/* Get a pure hex value */
1283
		$unpacked = unpack('H*hex', inet_pton($small_subnet));
1284
		/* Create PTR record using $small_sn / 4 chars */
1285
		$result[] = implode('.', array_reverse(str_split(substr(
1286
		    $unpacked['hex'], 0, $small_sn / 4)))).'.ip6.arpa';
1287

    
1288
		/* Detect what part of IP should be increased */
1289
		$change_part = (int) ($small_sn / 16);
1290
		if ($small_sn % 16 == 0) {
1291
			$change_part--;
1292
		}
1293

    
1294
		/* Increase 1 to desired part */
1295
		$parts = explode(":", Net_IPv6::uncompress($small_subnet));
1296
		$parts[$change_part]++;
1297
		$small_subnet = implode(":", $parts);
1298
	}
1299

    
1300
	return $result;
1301
}
1302

    
1303
/* return true if $addr is in $subnet, false if not */
1304
function ip_in_subnet($addr, $subnet) {
1305
	if (is_ipaddrv6($addr) && is_subnetv6($subnet)) {
1306
		/* Normalize IPv6 prefix to its start address to avoid PHP errors
1307
		 * https://redmine.pfsense.org/issues/14256
1308
		 */
1309
		list($prefix, $length) = explode("/", $subnet);
1310
		$prefix = gen_subnetv6($prefix, $length);
1311
		$subnet = "{$prefix}/{$length}";
1312
		return (Net_IPv6::isInNetmask($addr, $subnet));
1313
	} else if (is_ipaddrv4($addr) && is_subnetv4($subnet)) {
1314
		list($ip, $mask) = explode('/', $subnet);
1315
		$mask = (0xffffffff << (32 - $mask)) & 0xffffffff;
1316
		return ((ip2long($addr) & $mask) == (ip2long($ip) & $mask));
1317
	}
1318
	return false;
1319
}
1320

    
1321
/* returns true if $hostname is just a valid hostname (top part without any of the domain part) */
1322
function is_unqualified_hostname($hostname) {
1323
	if (!is_string($hostname)) {
1324
		return false;
1325
	}
1326

    
1327
	if (preg_match('/^(?:[a-z0-9_]|[a-z0-9_][a-z0-9_\-]*[a-z0-9_])$/i', $hostname)) {
1328
		return true;
1329
	} else {
1330
		return false;
1331
	}
1332
}
1333

    
1334
/* returns true if $hostname is a valid hostname, with or without being a fully-qualified domain name. */
1335
function is_hostname($hostname, $allow_wildcard=false) {
1336
	if (!is_string($hostname)) {
1337
		return false;
1338
	}
1339

    
1340
	if (is_domain($hostname, $allow_wildcard)) {
1341
		if ((substr_count($hostname, ".") == 1) && ($hostname[strlen($hostname)-1] == ".")) {
1342
			/* Only a single dot at the end like "test." - hosts cannot be directly in the root domain. */
1343
			return false;
1344
		} else {
1345
			return true;
1346
		}
1347
	} else {
1348
		return false;
1349
	}
1350
}
1351

    
1352
/* returns true if $domain is a valid domain name */
1353
function is_domain($domain, $allow_wildcard=false, $trailing_dot=true) {
1354
	if (!is_string($domain)) {
1355
		return false;
1356
	}
1357
	if (!$trailing_dot && ($domain[strlen($domain)-1] == ".")) {
1358
		return false;
1359
	}
1360
	if ($allow_wildcard) {
1361
		$domain_regex = '/^(?:(?:[a-z_0-9\*]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9])\.)*(?:[a-z_0-9]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9\.])$/i';
1362
	} else {
1363
		$domain_regex = '/^(?:(?:[a-z_0-9]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9])\.)*(?:[a-z_0-9]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9\.])$/i';
1364
	}
1365

    
1366
	if (preg_match($domain_regex, $domain)) {
1367
		return true;
1368
	} else {
1369
		return false;
1370
	}
1371
}
1372

    
1373
/* returns true if $macaddr is a valid MAC address */
1374
function is_macaddr($macaddr, $partial=false) {
1375
	$values = explode(":", $macaddr);
1376

    
1377
	/* Verify if the MAC address has a proper amount of parts for either a partial or full match. */
1378
	if ($partial) {
1379
		if ((count($values) < 1) || (count($values) > 6)) {
1380
			return false;
1381
		}
1382
	} elseif (count($values) != 6) {
1383
		return false;
1384
	}
1385
	for ($i = 0; $i < count($values); $i++) {
1386
		if (ctype_xdigit($values[$i]) == false)
1387
			return false;
1388
		if (hexdec($values[$i]) < 0 || hexdec($values[$i]) > 255)
1389
			return false;
1390
	}
1391

    
1392
	return true;
1393
}
1394

    
1395
/*
1396
	If $return_message is true then
1397
		returns a text message about the reason that the name is invalid.
1398
		the text includes the type of "thing" that is being checked, passed in $object. (e.g. "alias", "gateway group", "schedule")
1399
	else
1400
		returns true if $name is a valid name for an alias
1401
		returns false if $name is not a valid name for an alias
1402

    
1403
	Aliases cannot be:
1404
		bad chars: anything except a-z 0-9 and underscore
1405
		bad names: empty string, pure numeric, pure underscore
1406
		reserved words: pre-defined service/protocol/port names which should not be ambiguous, and the words "port" and  "pass" */
1407

    
1408
function is_validaliasname($name, $return_message = false, $object = "alias") {
1409
	/* Array of reserved words */
1410
	$reserved = array("port", "pass");
1411

    
1412
	if (!is_string($name) || strlen($name) >= 32 || preg_match('/(^_*$|^\d*$|[^a-z0-9_])/i', $name)) {
1413
		if ($return_message) {
1414
			return sprintf(gettext('The %1$s name must be less than 32 characters long, may not consist of only numbers, may not consist of only underscores, and may only contain the following characters: %2$s'), $object, 'a-z, A-Z, 0-9, _');
1415
		} else {
1416
			return false;
1417
		}
1418
	}
1419
	if (in_array($name, $reserved, true)) {
1420
		if ($return_message) {
1421
			return sprintf(gettext('The %1$s name must not be either of the reserved words %2$s or %3$s.'), $object, "'port'", "'pass'");
1422
		} else {
1423
			return false;
1424
		}
1425
	}
1426
	if (getprotobyname($name)) {
1427
		if ($return_message) {
1428
			return sprintf(gettext('The %1$s name must not be an IP protocol name such as TCP, UDP, ICMP etc.'), $object);
1429
		} else {
1430
			return false;
1431
		}
1432
	}
1433
	if (getservbyname($name, "tcp") || getservbyname($name, "udp")) {
1434
		if ($return_message) {
1435
			return sprintf(gettext('The %1$s name must not be a well-known or registered TCP or UDP port name such as ssh, smtp, pop3, tftp, http, openvpn etc.'), $object);
1436
		} else {
1437
			return false;
1438
		}
1439
	}
1440
	if ($return_message) {
1441
		return sprintf(gettext('The %1$s name is valid.'), $object);
1442
	} else {
1443
		return true;
1444
	}
1445
}
1446

    
1447
/* returns a text message indicating if the alias name is valid, or the reason it is not valid. */
1448
function invalidaliasnamemsg($name, $object = "alias") {
1449
	return is_validaliasname($name, true, $object);
1450
}
1451

    
1452
/*
1453
 * returns true if $range is a valid integer range between $min and $max
1454
 * range delimiter can be ':' or '-'
1455
 */
1456
function is_intrange($range, $min, $max) {
1457
	$values = preg_split("/[:-]/", $range);
1458

    
1459
	if (!is_array($values) || count($values) != 2) {
1460
		return false;
1461
	}
1462

    
1463
	if (!ctype_digit(strval($values[0])) || !ctype_digit(strval($values[1]))) {
1464
		return false;
1465
	}
1466

    
1467
	$values[0] = intval($values[0]);
1468
	$values[1] = intval($values[1]);
1469

    
1470
	if ($values[0] >= $values[1]) {
1471
		return false;
1472
	}
1473

    
1474
	if ($values[0] < $min || $values[1] > $max) {
1475
		return false;
1476
	}
1477

    
1478
	return true;
1479
}
1480

    
1481
/* returns true if $port is a valid TCP/UDP/SCTP port */
1482
function is_port($port, $validate_name = true) {
1483
	if (ctype_digit(strval($port)) && ((intval($port) >= 1) && (intval($port) <= 65535))) {
1484
		return true;
1485
	}
1486
	if ($validate_name && (getservbyname($port, "tcp") || getservbyname($port, "udp") || getservbyname($port, "sctp"))) {
1487
		return true;
1488
	}
1489
	return false;
1490
}
1491

    
1492
/* returns true if $port is in use */
1493
function is_port_in_use($port, $proto = "tcp", $ip_version = 4) {
1494
	$port_info = array();
1495
	exec("/usr/bin/netstat --libxo json -an " . escapeshellarg('-' . $ip_version) . " -p " . escapeshellarg($proto), $rawdata, $rc);
1496
	if ($rc == 0) {
1497
		$netstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
1498
		$netstatarr = $netstatarr['statistics']['socket'];
1499

    
1500
		foreach($netstatarr as $portstats){
1501
			array_push($port_info, $portstats['local']['port']);
1502
		}
1503
	}
1504

    
1505
	return in_array($port, $port_info);
1506
}
1507

    
1508
/* returns true if $portrange is a valid portrange ("<port>:<port>") */
1509
function is_portrange($portrange) {
1510
	$ports = explode(":", $portrange);
1511

    
1512
	return (count($ports) == 2 && is_port($ports[0]) && is_port($ports[1]));
1513
}
1514

    
1515
/* returns true if $port is a valid port number or range ("<port>:<port>") */
1516
function is_port_or_range($port) {
1517
	return (is_port($port) || is_portrange($port));
1518
}
1519

    
1520
/**
1521
 * Check if $port is a user or system alias that is a 'port' type.
1522
 * @param mixed $port The alias check.
1523
 * @return bool True if the alias is of type 'port'.
1524
 */
1525
function is_portalias($port) {
1526
	if (empty($port)) {
1527
		return false;
1528
	}
1529

    
1530
	// pass input validation
1531
	if (!empty(get_reserved_table_names($port, 'port,url_ports,urltable_ports'))) {
1532
		return true;
1533
	}
1534

    
1535
	if (is_alias($port)) {
1536
		foreach (config_get_path('aliases/alias', []) as $alias) {
1537
			if ($alias['name'] == $port && preg_match("/port/i", $alias['type'])) {
1538
				return true;
1539
			}
1540
		}
1541
	}
1542
	return false;
1543
}
1544

    
1545
/* returns true if $port is a valid port number or an alias thereof */
1546
function is_port_or_alias($port) {
1547
	return (is_port($port) || is_portalias($port));
1548
}
1549

    
1550
/* returns true if $port is a valid port number or range ("<port>:<port>") or an alias thereof */
1551
function is_port_or_range_or_alias($port) {
1552
	return (is_port($port) || is_portrange($port) || is_portalias($port));
1553
}
1554

    
1555
/* create ranges of sequential port numbers (200:215) and remove duplicates */
1556
function group_ports($ports, $kflc = false) {
1557
	if (!is_array($ports) || empty($ports)) {
1558
		return;
1559
	}
1560

    
1561
	$uniq = array();
1562
	$comments = array();
1563
	foreach ($ports as $port) {
1564
		if (($kflc) && (strpos($port, '#') === 0)) {	// Keep Full Line Comments (lines beginning with #).
1565
			$comments[] = $port;
1566
		} else if (is_portrange($port)) {
1567
			list($begin, $end) = explode(":", $port);
1568
			if ($begin > $end) {
1569
				$aux = $begin;
1570
				$begin = $end;
1571
				$end = $aux;
1572
			}
1573
			for ($i = $begin; $i <= $end; $i++) {
1574
				if (!in_array($i, $uniq)) {
1575
					$uniq[] = $i;
1576
				}
1577
			}
1578
		} else if (is_port($port)) {
1579
			if (!in_array($port, $uniq)) {
1580
				$uniq[] = $port;
1581
			}
1582
		}
1583
	}
1584
	sort($uniq, SORT_NUMERIC);
1585

    
1586
	$result = array();
1587
	foreach ($uniq as $idx => $port) {
1588
		if ($idx == 0) {
1589
			$result[] = $port;
1590
			continue;
1591
		}
1592

    
1593
		$last = end($result);
1594
		if (is_portrange($last)) {
1595
			list($begin, $end) = explode(":", $last);
1596
		} else {
1597
			$begin = $end = $last;
1598
		}
1599

    
1600
		if ($port == ($end+1)) {
1601
			$end++;
1602
			$result[count($result)-1] = "{$begin}:{$end}";
1603
		} else {
1604
			$result[] = $port;
1605
		}
1606
	}
1607

    
1608
	return array_merge($comments, $result);
1609
}
1610

    
1611
/* returns true if $val is a valid shaper bandwidth value */
1612
function is_valid_shaperbw($val) {
1613
	return (preg_match("/^(\d+(?:\.\d+)?)([MKG]?b|%)$/", $val));
1614
}
1615

    
1616
/* returns true if $test is in the range between $start and $end */
1617
function is_inrange_v4($test, $start, $end) {
1618
	if (!is_ipaddrv4($test) || !is_ipaddrv4($start) || !is_ipaddrv4($end)) {
1619
		return false;
1620
	}
1621

    
1622
	if (ip2ulong($test) <= ip2ulong($end) &&
1623
	    ip2ulong($test) >= ip2ulong($start)) {
1624
		return true;
1625
	}
1626

    
1627
	return false;
1628
}
1629

    
1630
/* returns true if $test is in the range between $start and $end */
1631
function is_inrange_v6($test, $start, $end) {
1632
	if (!is_ipaddrv6($test) || !is_ipaddrv6($start) || !is_ipaddrv6($end)) {
1633
		return false;
1634
	}
1635

    
1636
	if (inet_pton($test) <= inet_pton($end) &&
1637
	    inet_pton($test) >= inet_pton($start)) {
1638
		return true;
1639
	}
1640

    
1641
	return false;
1642
}
1643

    
1644
/* returns true if $test is in the range between $start and $end */
1645
function is_inrange($test, $start, $end) {
1646
	return is_ipaddrv6($test) ? is_inrange_v6($test, $start, $end) : is_inrange_v4($test, $start, $end);
1647
}
1648

    
1649
/**
1650
 * Check if an ethertype is valid
1651
 *
1652
 * @param string $ethertype	The ethertype as hex string
1653
 *
1654
 * @return bool
1655
 */
1656
function is_ethertype(string $ethertype): bool {
1657
	$ethertype = strtolower($ethertype);
1658

    
1659
	if (!str_starts_with($ethertype, '0x')) {
1660
		return (false);
1661
	}
1662

    
1663
	$ethertype = substr($ethertype, 2);
1664

    
1665
	if (!ctype_xdigit($ethertype)) {
1666
		return (false);
1667
	}
1668

    
1669
	return (filter_var(hexdec($ethertype), FILTER_VALIDATE_INT, ['options' => ['min_range' => 0x1, 'max_range' => 0xffff]]));
1670
}
1671

    
1672
function build_vip_list($fif, $family = "all") {
1673
	$list = array('address' => gettext('Interface Address'));
1674

    
1675
	$viplist = get_configured_vip_list($family);
1676
	foreach ($viplist as $vip => $address) {
1677
		if ($fif == get_configured_vip_interface($vip)) {
1678
			$list[$vip] = "$address";
1679
			if (get_vip_descr($address)) {
1680
				$list[$vip] .= " (". get_vip_descr($address) .")";
1681
			}
1682
		} else {
1683
			/**
1684
			 * additional check necessary: Alias-on-CARP VIPs return a "_vip<id>" string
1685
			 * that needs to be resolved via an additional check to see if that VIP should
1686
			 * be displayed here or skipped, as the parent isn't configured on this interface
1687
			 */
1688

    
1689
			$parentif = get_configured_vip_interface($vip);
1690
			if (str_starts_with($parentif, "_vip")) {
1691
				if ($fif == get_configured_vip_interface($parentif)) {
1692
					$list[$vip] = "$address";
1693
					if (get_vip_descr($address)) {
1694
						$list[$vip] .= " (". get_vip_descr($address) .")";
1695
					}
1696
				}
1697
			}
1698
		}
1699
	}
1700

    
1701
	return($list);
1702
}
1703

    
1704
function get_configured_vip_list($family = 'all', $type = VIP_ALL) {
1705
	$list = array();
1706
	$viparr = config_get_path('virtualip/vip');
1707
	if (!is_array($viparr) || empty($viparr)) {
1708
		return ($list);
1709
	}
1710

    
1711
	foreach ($viparr as $vip) {
1712

    
1713
		if ($type == VIP_CARP) {
1714
			if ($vip['mode'] != "carp")
1715
				continue;
1716
		} elseif ($type == VIP_IPALIAS) {
1717
			if ($vip['mode'] != "ipalias")
1718
				continue;
1719
		} else {
1720
			if ($vip['mode'] != "carp" && $vip['mode'] != "ipalias")
1721
				continue;
1722
		}
1723

    
1724
		if ($family == 'all' ||
1725
		    ($family == 'inet' && is_ipaddrv4($vip['subnet'])) ||
1726
		    ($family == 'inet6' && is_ipaddrv6($vip['subnet']))) {
1727
			$list["_vip{$vip['uniqid']}"] = $vip['subnet'];
1728
		}
1729
	}
1730
	return ($list);
1731
}
1732

    
1733
function get_configured_vip($vipinterface = '') {
1734

    
1735
	return (get_configured_vip_detail($vipinterface, 'all', 'vip'));
1736
}
1737

    
1738
function get_configured_vip_interface($vipinterface = '') {
1739

    
1740
	return (get_configured_vip_detail($vipinterface, 'all', 'iface'));
1741
}
1742

    
1743
function get_configured_vip_ipv4($vipinterface = '') {
1744

    
1745
	return (get_configured_vip_detail($vipinterface, 'inet', 'ip'));
1746
}
1747

    
1748
function get_configured_vip_ipv6($vipinterface = '') {
1749

    
1750
	return (get_configured_vip_detail($vipinterface, 'inet6', 'ip'));
1751
}
1752

    
1753
function get_configured_vip_subnetv4($vipinterface = '') {
1754

    
1755
	return (get_configured_vip_detail($vipinterface, 'inet', 'subnet'));
1756
}
1757

    
1758
function get_configured_vip_subnetv6($vipinterface = '') {
1759

    
1760
	return (get_configured_vip_detail($vipinterface, 'inet6', 'subnet'));
1761
}
1762

    
1763
function get_configured_vip_detail($vipinterface = '', $family = 'inet', $what = 'ip') {
1764
	$viparr = config_get_path('virtualip/vip');
1765
	if (empty($vipinterface) || !is_array($viparr) || empty($viparr)) {
1766
		return (NULL);
1767
	}
1768

    
1769
	foreach ($viparr as $vip) {
1770
		if ($vip['mode'] != "carp" && $vip['mode'] != "ipalias") {
1771
			continue;
1772
		}
1773

    
1774
		if ($vipinterface != "_vip{$vip['uniqid']}") {
1775
			continue;
1776
		}
1777

    
1778
		switch ($what) {
1779
			case 'subnet':
1780
				if ($family == 'inet' && is_ipaddrv4($vip['subnet']))
1781
					return ($vip['subnet_bits']);
1782
				else if ($family == 'inet6' && is_ipaddrv6($vip['subnet']))
1783
					return ($vip['subnet_bits']);
1784
				break;
1785
			case 'iface':
1786
				return ($vip['interface']);
1787
				break;
1788
			case 'vip':
1789
				return ($vip);
1790
				break;
1791
			case 'ip':
1792
			default:
1793
				if ($family == 'inet' && is_ipaddrv4($vip['subnet'])) {
1794
					return ($vip['subnet']);
1795
				} else if ($family == 'inet6' && is_ipaddrv6($vip['subnet'])) {
1796
					return ($vip['subnet']);
1797
				}
1798
				break;
1799
		}
1800
		break;
1801
	}
1802

    
1803
	return (NULL);
1804
}
1805

    
1806
/* comparison function for sorting by the order in which interfaces are normally created */
1807
function compare_interface_friendly_names($a, $b) {
1808
	if ($a == $b) {
1809
		return 0;
1810
	} else if ($a == 'wan') {
1811
		return -1;
1812
	} else if ($b == 'wan') {
1813
		return 1;
1814
	} else if ($a == 'lan') {
1815
		return -1;
1816
	} else if ($b == 'lan') {
1817
		return 1;
1818
	}
1819

    
1820
	return strnatcmp($a, $b);
1821
}
1822

    
1823
/**
1824
 * Get the configured interfaces list
1825
 *
1826
 * @param bool $with_disabled Include disabled interfaces
1827
 *
1828
 * @return array
1829
 */
1830
function get_configured_interface_list(bool $with_disabled = false) : array
1831
{
1832
	$iflist = [];
1833
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1834
		if ($with_disabled || isset($if_detail['enable'])) {
1835
			$iflist[$if] = $if;
1836
		}
1837
	}
1838

    
1839
	return ($iflist);
1840
}
1841

    
1842
/**
1843
 * Return the configured (and real) interfaces list.
1844
 *
1845
 * @param bool $with_disabled Include disabled interfaces
1846
 *
1847
 * @return array
1848
 */
1849
function get_configured_interface_list_by_realif(bool $with_disabled = false) : array
1850
{
1851
	$iflist = [];
1852
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1853
		if ($with_disabled || isset($if_detail['enable'])) {
1854
			$tmpif = get_real_interface($if);
1855
			if (empty($tmpif)) {
1856
				continue;
1857
			}
1858
			$iflist[$tmpif] = $if;
1859
		}
1860
	}
1861

    
1862
	return ($iflist);
1863
}
1864

    
1865
/**
1866
 * Return the configured interfaces list with their description.
1867
 *
1868
 * @param bool $with_disabled Include disabled interfaces
1869
 *
1870
 * @return array
1871
 */
1872
function get_configured_interface_with_descr(bool $with_disabled = false) : array
1873
{
1874
	global $user_settings;
1875

    
1876
	$iflist = [];
1877
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1878
		if ($with_disabled || isset($if_detail['enable'])) {
1879
			$iflist[$if] = strtoupper(array_get_path($if_detail, 'descr', $if));
1880
		}
1881
	}
1882

    
1883
	if (is_array($user_settings)
1884
	    && array_get_path($user_settings, 'webgui/interfacessort')) {
1885
		asort($iflist, SORT_NATURAL);
1886
	}
1887

    
1888
	return ($iflist);
1889
}
1890

    
1891
/*
1892
 *   get_configured_ip_addresses() - Return a list of all configured
1893
 *   IPv4 addresses.
1894
 *
1895
 */
1896
function get_configured_ip_addresses() {
1897
	if (!function_exists('get_interface_ip')) {
1898
		require_once("interfaces.inc");
1899
	}
1900
	$ip_array = array();
1901
	$interfaces = get_configured_interface_list();
1902
	if (is_array($interfaces)) {
1903
		foreach ($interfaces as $int) {
1904
			$ipaddr = get_interface_ip($int);
1905
			$ip_array[$int] = $ipaddr;
1906
		}
1907
	}
1908
	$interfaces = get_configured_vip_list('inet');
1909
	if (is_array($interfaces)) {
1910
		foreach ($interfaces as $int => $ipaddr) {
1911
			$ip_array[$int] = $ipaddr;
1912
		}
1913
	}
1914

    
1915
	/* pppoe server */
1916
	if (is_array(config_get_path('pppoes/pppoe'))) {
1917
		foreach (config_get_path('pppoes/pppoe', []) as $pppoe) {
1918
			if ($pppoe['mode'] == "server") {
1919
				if (is_ipaddr($pppoe['localip'])) {
1920
					$int = "poes". $pppoe['pppoeid'];
1921
					$ip_array[$int] = $pppoe['localip'];
1922
				}
1923
			}
1924
		}
1925
	}
1926

    
1927
	return $ip_array;
1928
}
1929

    
1930
/*
1931
 *   get_configured_ipv6_addresses() - Return a list of all configured
1932
 *   IPv6 addresses.
1933
 *
1934
 */
1935
function get_configured_ipv6_addresses($linklocal_fallback = false) {
1936
	require_once("interfaces.inc");
1937
	$ipv6_array = array();
1938
	$interfaces = get_configured_interface_list();
1939
	if (is_array($interfaces)) {
1940
		foreach ($interfaces as $int) {
1941
			$ipaddrv6 = text_to_compressed_ip6(get_interface_ipv6($int, false, $linklocal_fallback));
1942
			$ipv6_array[$int] = $ipaddrv6;
1943
		}
1944
	}
1945
	$interfaces = get_configured_vip_list('inet6');
1946
	if (is_array($interfaces)) {
1947
		foreach ($interfaces as $int => $ipaddrv6) {
1948
			$ipv6_array[$int] = text_to_compressed_ip6($ipaddrv6);
1949
		}
1950
	}
1951
	return $ipv6_array;
1952
}
1953

    
1954
/*
1955
 *   get_interface_list() - Return a list of all physical interfaces
1956
 *   along with MAC and status.
1957
 *
1958
 *   $mode = "active" - use ifconfig -lu
1959
 *           "media"  - use ifconfig to check physical connection
1960
 *			status (much slower)
1961
 */
1962
function get_interface_list($mode = "active", $keyby = "physical", $vfaces = false) {
1963
	$upints = array();
1964
	/* get a list of virtual interface types */
1965
	if (!$vfaces) {
1966
		$vfaces = array(
1967
				'bridge',
1968
				'ppp',
1969
				'pppoe',
1970
				'poes',
1971
				'pptp',
1972
				'l2tp',
1973
				'sl',
1974
				'gif',
1975
				'gre',
1976
				'faith',
1977
				'lo',
1978
				'ng',
1979
				'_vlan',
1980
				'_wlan',
1981
				'pflog',
1982
				'plip',
1983
				'pfsync',
1984
				'enc',
1985
				'tun',
1986
				'lagg',
1987
				'vip'
1988
		);
1989
	} else {
1990
		$vfaces = array(
1991
				'bridge',
1992
				'poes',
1993
				'sl',
1994
				'faith',
1995
				'lo',
1996
				'ng',
1997
				'_vlan',
1998
				'_wlan',
1999
				'pflog',
2000
				'plip',
2001
				'pfsync',
2002
				'enc',
2003
				'tun',
2004
				'lagg',
2005
				'vip',
2006
				'l2tps'
2007
		);
2008
	}
2009
	switch ($mode) {
2010
		case "active":
2011
			$upints = pfSense_interface_listget(IFF_UP);
2012
			break;
2013
		case "media":
2014
			$intlist = pfSense_interface_listget();
2015
			$ifconfig = [];
2016
			exec("/sbin/ifconfig -a", $ifconfig);
2017
			$ifstatus = preg_grep('/status:/', $ifconfig);
2018
			foreach ($ifstatus as $status) {
2019
				$int = array_shift($intlist);
2020
				if (stristr($status, "active")) {
2021
					$upints[] = $int;
2022
				}
2023
			}
2024
			break;
2025
		default:
2026
			$upints = pfSense_interface_listget();
2027
			break;
2028
	}
2029
	/* build interface list with netstat */
2030
	$linkinfo = [];
2031
	exec("/usr/bin/netstat -inW -f link | awk '{ print $1, $4 }'", $linkinfo);
2032
	array_shift($linkinfo);
2033
	/* build ip address list with netstat */
2034
	$ipinfo = [];
2035
	exec("/usr/bin/netstat -inW -f inet | awk '{ print $1, $4 }'", $ipinfo);
2036
	array_shift($ipinfo);
2037
	foreach ($linkinfo as $link) {
2038
		$friendly = "";
2039
		$alink = explode(" ", $link);
2040
		$ifname = rtrim(trim($alink[0]), '*');
2041
		/* trim out all numbers before checking for vfaces */
2042
		$tmp_ifname = preg_split('/(\d-)*\d$/', $ifname);
2043
		if (!in_array(array_shift($tmp_ifname), $vfaces) &&
2044
		    interface_is_vlan($ifname) == NULL &&
2045
		    interface_is_qinq($ifname) == NULL &&
2046
		    !stristr($ifname, "_wlan") &&
2047
		    !stristr($ifname, "_stf")) {
2048
			$toput = array(
2049
					"mac" => trim($alink[1]),
2050
					"up" => in_array($ifname, $upints)
2051
				);
2052
			foreach ($ipinfo as $ip) {
2053
				$aip = explode(" ", $ip);
2054
				if ($aip[0] == $ifname) {
2055
					$toput['ipaddr'] = $aip[1];
2056
				}
2057
			}
2058
			if (is_array(config_get_path('interfaces'))) {
2059
				foreach (config_get_path('interfaces', []) as $name => $int) {
2060
					if ($int['if'] == $ifname) {
2061
						$friendly = $name;
2062
					}
2063
				}
2064
			}
2065
			switch ($keyby) {
2066
			case "physical":
2067
				if ($friendly != "") {
2068
					$toput['friendly'] = $friendly;
2069
				}
2070
				$dmesg_arr = array();
2071
				exec("/sbin/dmesg |grep $ifname | head -n1", $dmesg_arr);
2072
				preg_match_all("/<(.*?)>/i", $dmesg_arr[0], $dmesg);
2073
				$toput['dmesg'] = $dmesg[1][0];
2074
				$iflist[$ifname] = $toput;
2075
				break;
2076
			case "ppp":
2077

    
2078
			case "friendly":
2079
				if ($friendly != "") {
2080
					$toput['if'] = $ifname;
2081
					$iflist[$friendly] = $toput;
2082
				}
2083
				break;
2084
			}
2085
		}
2086
	}
2087
	return $iflist;
2088
}
2089

    
2090
function get_lagg_interface_list() {
2091
	$plist = array();
2092
	if (is_array(config_get_path('laggs/lagg'))) {
2093
		foreach (config_get_path('laggs/lagg', []) as $lagg) {
2094
			$lagg['mac'] = get_interface_mac($lagg['laggif']);
2095
			$lagg['islagg'] = true;
2096
			$plist[$lagg['laggif']] = $lagg;
2097
		}
2098
	}
2099

    
2100
	return ($plist);
2101
}
2102

    
2103
/****f* util/log_error
2104
* NAME
2105
*   log_error  - Sends a string to syslog.
2106
* INPUTS
2107
*   $error     - string containing the syslog message.
2108
* RESULT
2109
*   null
2110
******/
2111
function log_error($error) {
2112
	global $g;
2113
	$page = $_SERVER['SCRIPT_NAME'];
2114
	if (empty($page)) {
2115
		$files = get_included_files();
2116
		$page = basename($files[0]);
2117
	}
2118
	syslog(LOG_ERR, "$page: $error");
2119
	if (g_get('debug')) {
2120
		syslog(LOG_WARNING, var_export(debug_backtrace()));
2121
	}
2122
	return;
2123
}
2124

    
2125
/****f* util/log_auth
2126
* NAME
2127
*   log_auth   - Sends a string to syslog as LOG_AUTH facility
2128
* INPUTS
2129
*   $error     - string containing the syslog message.
2130
* RESULT
2131
*   null
2132
******/
2133
function log_auth($error, $force_quiet = false) {
2134
	global $g;
2135
	$page = $_SERVER['SCRIPT_NAME'];
2136
	$level = ($force_quiet || config_path_enabled('system/webgui', 'quietlogin')) ? LOG_NOTICE|LOG_AUTH : LOG_AUTH;
2137
	syslog($level, "{$page}: {$error}");
2138
	if (g_get('debug')) {
2139
		syslog(LOG_WARNING, var_export(debug_backtrace()));
2140
	}
2141
	return;
2142
}
2143

    
2144
/****f* util/exec_command
2145
 * NAME
2146
 *   exec_command - Execute a command and return a string of the result.
2147
 * INPUTS
2148
 *   $command   - String of the command to be executed.
2149
 * RESULT
2150
 *   String containing the command's result.
2151
 * NOTES
2152
 *   This function returns the command's stdout and stderr.
2153
 ******/
2154
function exec_command($command) {
2155
	$output = array();
2156
	exec($command . ' 2>&1', $output);
2157
	return(implode("\n", $output));
2158
}
2159

    
2160
/* wrapper for exec()
2161
   Executes in background or foreground.
2162
   For background execution, returns PID of background process to allow calling code control */
2163
function mwexec($command, $nologentry = false, $clearsigmask = false, $background = false) {
2164
	global $g;
2165
	$retval = 0;
2166

    
2167
	if (g_get('debug')) {
2168
		if (!$_SERVER['REMOTE_ADDR']) {
2169
			echo "mwexec(): $command" . ($background ? " [BG]":"") . "\n";
2170
		}
2171
	}
2172
	if ($clearsigmask) {
2173
		$oldset = array();
2174
		pcntl_sigprocmask(SIG_SETMASK, array(), $oldset);
2175
	}
2176

    
2177
	if ($background) {
2178
		// start background process and return PID
2179
		$retval = exec("/usr/bin/nohup $command > /dev/null 2>&1 & echo $!");
2180
	} else {
2181
		// run in foreground, and (optionally) log if nonzero return
2182
		$outputarray = array();
2183
		exec("$command 2>&1", $outputarray, $retval);
2184
		if (($retval <> 0) && (!$nologentry || config_path_enabled('system', 'developerspew'))) {
2185
			log_error(sprintf(gettext("The command '%1\$s' returned exit code '%2\$d', the output was '%3\$s' "), $command, $retval, implode(" ", $outputarray)));
2186
		}
2187
	}
2188

    
2189
	if ($clearsigmask) {
2190
		pcntl_sigprocmask(SIG_SETMASK, $oldset);
2191
	}
2192

    
2193
	return $retval;
2194
}
2195

    
2196
/* wrapper for exec() in background */
2197
function mwexec_bg($command, $clearsigmask = false) {
2198
	return mwexec($command, false, $clearsigmask, true);
2199
}
2200

    
2201
/*
2202
 * Unlink a file, or pattern-match of a file, if it exists
2203
 *
2204
 * If the file/path contains glob() compatible wildcards, all matching files
2205
 * will be unlinked.
2206
 * Any warning/errors are suppressed (e.g. no matching files to delete)
2207
 * If there are matching file(s) and they were all unlinked OK, then return
2208
 * true.  Otherwise return false (the requested file(s) did not exist, or
2209
 * could not be deleted), this allows the caller to know if they were the one
2210
 * to successfully delete the file(s).
2211
 */
2212
function unlink_if_exists($fn) {
2213
	$to_do = glob($fn);
2214
	if (is_array($to_do) && count($to_do) > 0) {
2215
		// Returns an array of boolean indicating if each unlink worked
2216
		$results = @array_map("unlink", $to_do);
2217
		// If there is no false in the array, then all went well
2218
		$result = !in_array(false, $results, true);
2219
	} else {
2220
		$result = @unlink($fn);
2221
	}
2222
	return $result;
2223
}
2224

    
2225
/**
2226
 * Make a global alias table for faster lookups. Does not include
2227
 * system aliases since those are handled separately from aliases
2228
 * configured by users.
2229
 * @return void
2230
 */
2231
function alias_make_table() {
2232
	global $aliastable;
2233

    
2234
	$aliastable = array();
2235
	$aliases = config_get_path('aliases/alias', []);
2236

    
2237
	// Reserved aliases take precedence.
2238
	$reserved_alias = get_reserved_table_names();
2239
	if (!empty($reserved_alias)) {
2240
		foreach ($aliases as $id => $info) {
2241
			if (!isset($reserved_alias[$info['name']])) {
2242
				continue;
2243
			}
2244
			$aliases[$id] = $reserved_alias[$info['name']];
2245
			unset($reserved_alias[$info['name']]);
2246
		}
2247

    
2248
		if (!empty($reserved_alias)) {
2249
			$aliases = array_merge($aliases, array_values($reserved_alias));
2250
		}
2251
	}
2252

    
2253
	foreach ($aliases as $alias) {
2254
		if (!is_array($alias) || empty($alias)) {
2255
			continue;
2256
		}
2257
		if ($alias['name']) {
2258
			$aliastable[$alias['name']] = $alias['address'];
2259
		}
2260
	}
2261
}
2262

    
2263
/**
2264
 * Check if an alias exists. Used in form input validation.
2265
 * @param mixed $name The alias name.
2266
 * @return bool False is not a user or system alias.
2267
 */
2268
function is_alias($name) {
2269
	if (empty($name)) {
2270
		return false;
2271
	}
2272

    
2273
	global $aliastable;
2274

    
2275
	if (!empty(get_reserved_table_names($name))) {
2276
		return true;
2277
	}
2278

    
2279
	return isset($aliastable[$name]);
2280
}
2281

    
2282
/**
2283
 * Get the type of a user or system alias.
2284
 * @param mixed $name Alias name.
2285
 * @return string The alias type.
2286
 */
2287
function alias_get_type($name) {
2288
	if (empty($name)) {
2289
		return '';
2290
	}
2291

    
2292
	$aliases = config_get_path('aliases/alias', []);
2293

    
2294
	// Reserved aliases take precedence.
2295
	$reserved_alias = get_reserved_table_names($name);
2296
	if (!empty($reserved_alias)) {
2297
		return $reserved_alias[array_key_first($reserved_alias)]['type'];
2298
	}
2299

    
2300
	foreach ($aliases as $alias) {
2301
		if ($name == $alias['name']) {
2302
			return $alias['type'];
2303
		}
2304
	}
2305

    
2306
	return "";
2307
}
2308

    
2309
/**
2310
 * Expand a URL user or system alias, if necessary.
2311
 * @param mixed $name Alias name.
2312
 * @return string|null null if the alias cannot be expanded.
2313
 */
2314
function alias_expand($name) {
2315
	if (empty($name)) {
2316
		return null;
2317
	}
2318
	global $aliastable;
2319
	$urltable_prefix = "/var/db/aliastables/";
2320
	$urltable_filename = $urltable_prefix . $name . ".txt";
2321

    
2322
	$aliases = config_get_path('aliases/alias', []);
2323

    
2324
	// Reserved aliases take precedence.
2325
	$reserved_alias = get_reserved_table_names($name);
2326
	if (!empty($reserved_alias)) {
2327
		if (!in_array($reserved_alias[array_key_first($reserved_alias)]['type'], ['url', 'url_ports', 'urltable', "urltable_ports"])) {
2328
			if (in_array($reserved_alias[array_key_first($reserved_alias)]['type'], ['port'])) {
2329
				return "\${$name}";
2330
			} else {
2331
				return "<{$name}>";
2332
			}
2333
		}
2334

    
2335
		// Handle URL* type system aliases that specify a file path.
2336
		if (!empty($reserved_alias[array_key_first($reserved_alias)]['url']) && !is_URL($reserved_alias[array_key_first($reserved_alias)]['url'])) {
2337
			$urltable_filename = $reserved_alias[array_key_first($reserved_alias)]['url'];
2338
			if (file_exists($urltable_filename) && !empty(trim(file_get_contents($urltable_filename)))) {
2339
				if (in_array($reserved_alias[array_key_first($reserved_alias)]['type'], ['url_ports', 'urltable_ports'])) {
2340
					// pf does not support port tables - use a macro instead.
2341
					return "\${$name}";
2342
				} else {
2343
					return "<{$name}>";
2344
				}
2345
			} else {
2346
				return null;
2347
			}
2348
		}
2349

    
2350
		// Replace user alias with system alias.
2351
		foreach ($aliases as $id => $info) {
2352
			if (!isset($reserved_alias[$info['name']])) {
2353
				continue;
2354
			}
2355
			$aliases[$id] = $reserved_alias[$info['name']];
2356
			unset($reserved_alias[$info['name']]);
2357
			break;
2358
		}
2359
		if (!empty($reserved_alias)) {
2360
			$aliases = array_merge($aliases, array_values($reserved_alias));
2361
		}
2362
	}
2363

    
2364
	if (isset($aliastable[$name])) {
2365
		// alias names cannot be strictly numeric. redmine #4289
2366
		if (is_numericint($name)) {
2367
			return null;
2368
		}
2369

    
2370
		/*
2371
		 * make sure if it's a url alias, it actually exists and valid URLs.
2372
		 * redmine #5845, #13068
2373
		 */
2374
		foreach ($aliases as $alias) {
2375
			if ($alias['name'] == $name) {
2376
				if (in_array($alias['type'], ['url', 'url_ports', 'urltable', "urltable_ports"])) {
2377
					if (is_URL($alias['url']) && file_exists($urltable_filename) && !empty(trim(file_get_contents($urltable_filename)))) {
2378
						return "\${$name}";
2379
					} elseif (is_array($alias['aliasurl'])) {
2380
						foreach ($alias['aliasurl'] as $aliasurl) {
2381
							if (!is_URL($aliasurl)) {
2382
								return null;
2383
							}
2384
						}
2385
						return "\${$name}";
2386
					} else {
2387
						return null;
2388
					}
2389
				}
2390
				break;
2391
			}
2392
		}
2393
		return "\${$name}";
2394
	} else if (is_ipaddr($name) || is_subnet($name) ||
2395
	    is_port_or_range($name)) {
2396
		return "{$name}";
2397
	} else {
2398
		return null;
2399
	}
2400
}
2401

    
2402
/**
2403
 * Get the file path for a user or system urltable alias.
2404
 * @param mixed $name Alias file path.
2405
 * @return string|null null if the alias file does not exist.
2406
 */
2407
function alias_expand_urltable($name) {
2408
	if (empty($name)) {
2409
		return null;
2410
	}
2411
	$urltable_prefix = "/var/db/aliastables/";
2412
	$urltable_filename = $urltable_prefix . $name . ".txt";
2413

    
2414
	$aliases = config_get_path('aliases/alias', []);
2415

    
2416
	// Reserved aliases take precedence.
2417
	$reserved_alias = get_reserved_table_names($name, 'urltable');
2418
	if (!empty($reserved_alias)) {
2419
		// Handle URL* type system aliases that specify a file path.
2420
		if (!empty($reserved_alias[array_key_first($reserved_alias)]['url']) && !is_URL($reserved_alias[array_key_first($reserved_alias)]['url'])) {
2421
			$urltable_filename = $reserved_alias[array_key_first($reserved_alias)]['url'];
2422
			if (file_exists($urltable_filename)) {
2423
				return $urltable_filename;
2424
			} else {
2425
				return null;
2426
			}
2427
		}
2428

    
2429
		// Replace user alias with system alias.
2430
		foreach ($aliases as $id => $info) {
2431
			if (!isset($reserved_alias[$info['name']])) {
2432
				continue;
2433
			}
2434
			$aliases[$id] = $reserved_alias[$info['name']];
2435
			unset($reserved_alias[$info['name']]);
2436
			break;
2437
		}
2438
		if (!empty($reserved_alias)) {
2439
			$aliases = array_merge($aliases, array_values($reserved_alias));
2440
		}
2441
	}
2442

    
2443
	foreach ($aliases as $alias) {
2444
		if (!preg_match("/urltable/i", $alias['type']) ||
2445
		    ($alias['name'] != $name)) {
2446
			continue;
2447
		}
2448

    
2449
		if (is_URL($alias["url"]) && file_exists($urltable_filename)) {
2450
			if (!filesize($urltable_filename)) {
2451
				// file exists, but is empty, try to sync
2452
				send_event("service sync alias {$name}");
2453
			}
2454
			return $urltable_filename;
2455
		} else {
2456
			send_event("service sync alias {$name}");
2457
			break;
2458
		}
2459
	}
2460
	return null;
2461
}
2462

    
2463
/* obtain MAC address given an IP address by looking at the ARP/NDP table */
2464
function arp_get_mac_by_ip($ip, $do_ping = true) {
2465
	unset($macaddr);
2466
	$retval = 1;
2467
	switch (is_ipaddr($ip)) {
2468
		case 4:
2469
			if ($do_ping === true) {
2470
				mwexec("/sbin/ping -c 1 -t 1 " . escapeshellarg($ip), true);
2471
			}
2472
			$macaddr = exec("/usr/sbin/arp -n " . escapeshellarg($ip) . " | /usr/bin/awk '{print $4}'", $output, $retval);
2473
			break;
2474
		case 6:
2475
			if ($do_ping === true) {
2476
				mwexec("/sbin/ping6 -c 1 -X 1 " . escapeshellarg($ip), true);
2477
			}
2478
			$macaddr = exec("/usr/sbin/ndp -n " . escapeshellarg($ip) . " | /usr/bin/awk '{print $2}'", $output, $retval);
2479
			break;
2480
	}
2481
	if ($retval == 0 && is_macaddr($macaddr)) {
2482
		return $macaddr;
2483
	} else {
2484
		return false;
2485
	}
2486
}
2487

    
2488
/* return a fieldname that is safe for xml usage */
2489
function xml_safe_fieldname($fieldname) {
2490
	$replace = array(
2491
	    '/', '-', ' ', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
2492
	    '_', '+', '=', '{', '}', '[', ']', '|', '/', '<', '>', '?',
2493
	    ':', ',', '.', '\'', '\\'
2494
	);
2495
	return strtolower(str_replace($replace, "", $fieldname));
2496
}
2497

    
2498
function mac_format($clientmac) {
2499
	global $cpzone;
2500

    
2501
	$mac = explode(":", $clientmac);
2502
	$mac_format = $cpzone ? config_get_path("captiveportal/{$cpzone}/radmac_format") : false;
2503

    
2504
	switch ($mac_format) {
2505
		case 'singledash':
2506
			return "$mac[0]$mac[1]$mac[2]-$mac[3]$mac[4]$mac[5]";
2507

    
2508
		case 'ietf':
2509
			return "$mac[0]-$mac[1]-$mac[2]-$mac[3]-$mac[4]-$mac[5]";
2510

    
2511
		case 'cisco':
2512
			return "$mac[0]$mac[1].$mac[2]$mac[3].$mac[4]$mac[5]";
2513

    
2514
		case 'unformatted':
2515
			return "$mac[0]$mac[1]$mac[2]$mac[3]$mac[4]$mac[5]";
2516

    
2517
		default:
2518
			return $clientmac;
2519
	}
2520
}
2521

    
2522
function resolve_retry($hostname, $protocol = 'inet') {
2523
	$retries = 10;
2524
	$numrecords = 1;
2525
	$recresult = array();
2526

    
2527
	switch ($protocol) {
2528
		case 'any':
2529
			$checkproto = 'is_ipaddr';
2530
			$dnsproto = DNS_ANY;
2531
			$dnstype = array('A', 'AAAA');
2532
			break;
2533
		case 'inet6':
2534
			$checkproto = 'is_ipaddrv6';
2535
			$dnsproto = DNS_AAAA;
2536
			$dnstype = array('AAAA');
2537
			break;
2538
		case 'inet':
2539
		default:
2540
			$checkproto = 'is_ipaddrv4';
2541
			$dnsproto = DNS_A;
2542
			$dnstype = array('A');
2543
			break;
2544
	}
2545

    
2546
	for ($i = 0; $i < $retries; $i++) {
2547
		if ($checkproto($hostname)) {
2548
			return $hostname;
2549
		}
2550

    
2551
		$dnsresult = @dns_get_record($hostname, $dnsproto);
2552

    
2553
		if (!empty($dnsresult)) {
2554
			foreach ($dnsresult as $ip) {
2555
				if (is_array($ip)) {
2556
					if (in_array($ip['type'], $dnstype)) {
2557
						if ($checkproto($ip['ip'])) {
2558
							$recresult[] = $ip['ip'];
2559
						}
2560

    
2561
						if ($checkproto($ip['ipv6'])) {
2562
							$recresult[] = $ip['ipv6'];
2563
						}
2564
					}
2565
				}
2566
			}
2567
		}
2568

    
2569
		// Return on success
2570
		if (!empty($recresult)) {
2571
			if ($numrecords == 1) {
2572
				return $recresult[0];
2573
			} else {
2574
				return array_slice($recresult, 0, $numrecords);
2575
			}
2576
		}
2577

    
2578
		usleep(100000);
2579
	}
2580

    
2581
	return false;
2582
}
2583

    
2584
function format_bytes($bytes) {
2585
	if ($bytes >= 1099511627776) {
2586
		return sprintf("%.2f TiB", $bytes/1099511627776);
2587
	} else if ($bytes >= 1073741824) {
2588
		return sprintf("%.2f GiB", $bytes/1073741824);
2589
	} else if ($bytes >= 1048576) {
2590
		return sprintf("%.2f MiB", $bytes/1048576);
2591
	} else if ($bytes >= 1024) {
2592
		return sprintf("%.0f KiB", $bytes/1024);
2593
	} else {
2594
		return sprintf("%d B", $bytes);
2595
	}
2596
}
2597

    
2598
function format_number(int $num, int $precision = 3): string
2599
{
2600
    $units = ['', 'K', 'M', 'G', 'T'];
2601
    for ($i = 0; $num >= 1000; $i++) {
2602
        $num /= 1000;
2603
    }
2604
    return (round($num, $precision) . $units[$i]);
2605
}
2606

    
2607
function unformat_number($formated_num) {
2608
	$num = strtoupper($formated_num);
2609

    
2610
	if ( strpos($num,"T") !== false ) {
2611
		$num = str_replace("T","",$num) * 1000 * 1000 * 1000 * 1000;
2612
	} else if ( strpos($num,"G") !== false ) {
2613
		$num = str_replace("G","",$num) * 1000 * 1000 * 1000;
2614
	} else if ( strpos($num,"M") !== false ) {
2615
		$num = str_replace("M","",$num) * 1000 * 1000;
2616
	} else if ( strpos($num,"K") !== false ) {
2617
		$num = str_replace("K","",$num) * 1000;
2618
	}
2619

    
2620
	return $num;
2621
}
2622

    
2623
function update_filter_reload_status($text, $new=false) {
2624
	global $g;
2625

    
2626
	if ($new) {
2627
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL);
2628
	} else {
2629
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL, FILE_APPEND);
2630
	}
2631
}
2632

    
2633
/****** util/return_dir_as_array
2634
 * NAME
2635
 *   return_dir_as_array - Return a directory's contents as an array.
2636
 * INPUTS
2637
 *   $dir          - string containing the path to the desired directory.
2638
 *   $filter_regex - string containing a regular expression to filter file names. Default empty.
2639
 * RESULT
2640
 *   $dir_array - array containing the directory's contents. This array will be empty if the path specified is invalid.
2641
 ******/
2642
function return_dir_as_array($dir, $filter_regex = '') {
2643
	$dir_array = array();
2644
	if (is_dir($dir)) {
2645
		if ($dh = opendir($dir)) {
2646
			while (($file = readdir($dh)) !== false) {
2647
				if (($file == ".") || ($file == "..")) {
2648
					continue;
2649
				}
2650

    
2651
				if (empty($filter_regex) || preg_match($filter_regex, $file)) {
2652
					array_push($dir_array, $file);
2653
				}
2654
			}
2655
			closedir($dh);
2656
		}
2657
	}
2658
	return $dir_array;
2659
}
2660

    
2661
function run_plugins($directory) {
2662
	/* process packager manager custom rules */
2663
	$files = return_dir_as_array($directory);
2664
	if (is_array($files)) {
2665
		foreach ($files as $file) {
2666
			if (stristr($file, ".sh") == true) {
2667
				mwexec($directory . $file . " start");
2668
			} else if (!is_dir($directory . "/" . $file) && stristr($file, ".inc")) {
2669
				require_once($directory . "/" . $file);
2670
			}
2671
		}
2672
	}
2673
}
2674

    
2675
/*
2676
 *    safe_mkdir($path, $mode = 0755)
2677
 *    create directory if it doesn't already exist and isn't a file!
2678
 */
2679
function safe_mkdir($path, $mode = 0755) {
2680
	if (!is_file($path) && !is_dir($path)) {
2681
		return @mkdir($path, $mode, true);
2682
	} else {
2683
		return false;
2684
	}
2685
}
2686

    
2687
/*
2688
 * get_sysctl($names)
2689
 * Get values of sysctl OID's listed in $names (accepts an array or a single
2690
 * name) and return an array of key/value pairs set for those that exist
2691
 */
2692
function get_sysctl($names) {
2693
	if (empty($names)) {
2694
		return array();
2695
	}
2696

    
2697
	if (is_array($names)) {
2698
		$name_list = array();
2699
		foreach ($names as $name) {
2700
			$name_list[] = escapeshellarg($name);
2701
		}
2702
	} else {
2703
		$name_list = array(escapeshellarg($names));
2704
	}
2705

    
2706
	/* https://redmine.pfsense.org/issues/14648
2707
	 * exec()'ing sysctl must never fail and valid output is expected, except
2708
	 * when the OID is absent for the system. Retry up to 2 times and log sysctl
2709
	 * failure for troubleshooting */
2710
	$retries = 2;
2711
	do {
2712
		$ret = exec("/sbin/sysctl -iq " . implode(" ", $name_list), $output, $status);
2713
		if ($ret === false || $status != 0) {
2714
			log_error("Warning: \"/sbin/sysctl -iq " .
2715
			    implode(" ", $name_list) . "\" returned status: {$status}.  Trying again.");
2716
			sleep(1);
2717
		}
2718
	} while (($ret === false || $status != 0) && $retries-- > 0);
2719
	if ($ret === false || $status != 0) {
2720
		log_error("Warning: \"/sbin/sysctl -iq " . implode(" ", $name_list) . "\" returned status: {$status}");
2721
	}
2722

    
2723
	$values = array();
2724
	foreach ($output as $line) {
2725
		$line = explode(": ", $line, 2);
2726
		if (count($line) == 2) {
2727
			$values[$line[0]] = $line[1];
2728
		}
2729
	}
2730

    
2731
	return $values;
2732
}
2733

    
2734
/*
2735
 * get_single_sysctl($name)
2736
 * Wrapper for get_sysctl() to simplify read of a single sysctl value
2737
 * return the value for sysctl $name or empty string if it doesn't exist
2738
 */
2739
function get_single_sysctl($name) {
2740
	if (empty($name)) {
2741
		return "";
2742
	}
2743

    
2744
	$value = get_sysctl($name);
2745
	if (empty($value) || !isset($value[$name])) {
2746
		return "";
2747
	}
2748

    
2749
	return $value[$name];
2750
}
2751

    
2752
/*
2753
 * set_sysctl($value_list)
2754
 * Set sysctl OID's listed as key/value pairs and return
2755
 * an array with keys set for those that succeeded
2756
 */
2757
function set_sysctl($values) {
2758
	if (empty($values)) {
2759
		return array();
2760
	}
2761

    
2762
	$value_list = array();
2763
	foreach ($values as $key => $value) {
2764
		$value_list[] = escapeshellarg($key) . "=" . escapeshellarg($value);
2765
	}
2766

    
2767
	exec("/sbin/sysctl -iq " . implode(" ", $value_list), $output, $success);
2768

    
2769
	/* Retry individually if failed (one or more read-only) */
2770
	if ($success <> 0 && count($value_list) > 1) {
2771
		foreach ($value_list as $value) {
2772
			exec("/sbin/sysctl -iq " . $value, $output);
2773
		}
2774
	}
2775

    
2776
	$ret = array();
2777
	foreach ($output as $line) {
2778
		$line = explode(": ", $line, 2);
2779
		if (count($line) == 2) {
2780
			$ret[$line[0]] = true;
2781
		}
2782
	}
2783

    
2784
	return $ret;
2785
}
2786

    
2787
/*
2788
 * set_single_sysctl($name, $value)
2789
 * Wrapper to set_sysctl() to make it simple to set only one sysctl
2790
 * returns boolean meaning if it succeeded
2791
 */
2792
function set_single_sysctl($name, $value) {
2793
	if (empty($name)) {
2794
		return false;
2795
	}
2796

    
2797
	$result = set_sysctl(array($name => $value));
2798

    
2799
	if (!isset($result[$name]) || $result[$name] != $value) {
2800
		return false;
2801
	}
2802

    
2803
	return true;
2804
}
2805

    
2806
/*
2807
 *     get_memory()
2808
 *     returns an array listing the amount of
2809
 *     memory installed in the hardware
2810
 *     [0] net memory available for the OS (FreeBSD) after some is taken by BIOS, video or whatever - e.g. 235 MBytes
2811
 *     [1] real (actual) memory of the system, should be the size of the RAM card/s - e.g. 256 MBytes
2812
 */
2813
function get_memory() {
2814
	$physmem = get_single_sysctl("hw.physmem");
2815
	$realmem = get_single_sysctl("hw.realmem");
2816
	/* Ensure $physmem has a sane value */
2817
	if (!is_numericint($physmem) &&
2818
	    is_numericint($realmem)) {
2819
		$physmem = $realmem;
2820
	}
2821
	/* Ensure $realmem has a sane value */
2822
	if (!is_numericint($realmem) &&
2823
	    is_numericint($physmem)) {
2824
		$realmem = $physmem;
2825
	}
2826
	/* If both are invalid, something deeper is wrong */
2827
	if (!is_numericint($physmem) &&
2828
	    is_numericint($realmem)) {
2829
		/* Try checking by pages instead */
2830
		$membypages = (int) get_single_sysctl("vm.stats.vm.v_page_count") * (int) get_single_sysctl("vm.stats.vm.v_page_size");
2831
		if (is_numericint($membypages)) {
2832
			$physmem = $membypages;
2833
			$realmem = $membypages;
2834
		} else {
2835
			/* Everything failed, return zeroes */
2836
			$physmem = 0;
2837
			$realmem = 0;
2838
		}
2839
	}
2840
	/* convert from bytes to megabytes */
2841
	return array(((int) $physmem/1048576), ((int) $realmem/1048576));
2842
}
2843

    
2844
function get_php_default_memory($ARCH) {
2845
	if ($ARCH == "amd64") {
2846
		$default_mem = 512;
2847
	} else {
2848
		return 128;
2849
	}
2850

    
2851
	// Ensure the default isn't taking all of the available RAM with a minimum of 128M
2852
	$system_mem = get_memory()[0];
2853
	if ($default_mem >= $system_mem) {
2854
		$default_mem = $system_mem / 2;
2855
		if ($default_mem < 128) {
2856
			$default_mem = 128;
2857
		}
2858
	}
2859

    
2860
	return floor($default_mem);
2861
}
2862

    
2863
function get_php_max_memory() {
2864
	$system_mem = floor(get_memory()[0]);
2865
	$max_mem = $system_mem - 512;
2866
	$default_mem = get_php_default_memory(php_uname("m"));
2867

    
2868
	// Ensure the max memory isn't zero or negative, in case 512M or less is available
2869
	if ($max_mem <= 0) {
2870
		$max_mem = $system_mem - 128;
2871

    
2872
		// If there still isn't enough RAM available, disable the ability to increase the PHP limit
2873
		if ($max_mem  < 128) {
2874
			return get_php_default_memory("");
2875
		}
2876
	} elseif ($max_mem < $default_mem) {
2877
		/* Do not restrict the maximum to less than the default */
2878
		$max_mem = $default_mem;
2879
	}
2880

    
2881
	return $max_mem;
2882
}
2883

    
2884
function mute_kernel_msgs() {
2885
	if (config_path_enabled('system','enableserial')) {
2886
		return;
2887
	}
2888
	exec("/sbin/conscontrol mute on");
2889
}
2890

    
2891
function unmute_kernel_msgs() {
2892
	exec("/sbin/conscontrol mute off");
2893
}
2894

    
2895
function start_devd() {
2896
	global $g;
2897

    
2898
	/* Generate hints for the kernel loader. */
2899
	$module_paths = explode(";", get_single_sysctl("kern.module_path"));
2900
	foreach ($module_paths as $path) {
2901
		if (!is_dir($path) ||
2902
		    (($files = scandir($path)) == false)) {
2903
			continue;
2904
		}
2905
		$found = false;
2906
		foreach ($files as $file) {
2907
			if (strlen($file) > 3 &&
2908
			    strcasecmp(substr($file, -3), ".ko") == 0) {
2909
				$found = true;
2910
				break;
2911
			}
2912
		}
2913
		if ($found == false) {
2914
			continue;
2915
		}
2916
		$_gb = exec("/usr/sbin/kldxref $path");
2917
		unset($_gb);
2918
	}
2919

    
2920
	/* Use the undocumented -q options of devd to quiet its log spamming */
2921
	$_gb = exec("/sbin/devd -q -f /etc/{$g['product_name']}-devd.conf");
2922
	sleep(1);
2923
	unset($_gb);
2924
}
2925

    
2926
function is_interface_vlan_mismatch() {
2927
	foreach (config_get_path('vlans/vlan', []) as $vlan) {
2928
		if (substr($vlan['if'], 0, 4) == "lagg") {
2929
			return false;
2930
		}
2931
		if (does_interface_exist($vlan['if']) == false) {
2932
			return true;
2933
		}
2934
	}
2935

    
2936
	return false;
2937
}
2938

    
2939
function is_interface_mismatch() {
2940
	global $g;
2941

    
2942
	$do_assign = false;
2943
	$i = 0;
2944
	$missing_interfaces = array();
2945
	if (is_array(config_get_path('interfaces'))) {
2946
		foreach (config_get_path('interfaces', []) as $ifcfg) {
2947
			if (interface_is_vlan($ifcfg['if']) != NULL ||
2948
			    interface_is_qinq($ifcfg['if']) != NULL ||
2949
			    preg_match("/^bridge|^cua|^enc|^gif|^gre|^ipsec|^l2tp|^lagg|^ngeth|^ovpn|^ppp|^pptp|^tap|^tun|^ue|vlan|^wg|_wlan|_\d{0,4}_\d{0,4}$/i", $ifcfg['if'])) {
2950
				// Do not check these interfaces.
2951
				$i++;
2952
				continue;
2953
			} else if (does_interface_exist($ifcfg['if']) == false) {
2954
				$missing_interfaces[] = $ifcfg['if'];
2955
				$do_assign = true;
2956
			} else {
2957
				$i++;
2958
			}
2959
		}
2960
	}
2961

    
2962
	/* VLAN/QinQ-only interface mismatch detection
2963
	 * see https://redmine.pfsense.org/issues/12170 */
2964
	foreach (config_get_path('vlans/vlan', []) as $vlan) {
2965
		if (!does_interface_exist($vlan['if']) &&
2966
		    !preg_match("/^enc|^cua|^tun|^tap|^l2tp|^pptp|^ppp|^ovpn|^ipsec|^gif|^gre|^lagg|^bridge|vlan|_wlan|_\d{0,4}_\d{0,4}$/i", $vlan['if'])) {
2967
			$missing_interfaces[] = $vlan['if'];
2968
			$do_assign = true;
2969
		}
2970
	}
2971
	foreach (config_get_path('qinqs/qinqentry', []) as $qinq) {
2972
		if (!does_interface_exist($qinq['if']) &&
2973
		    !preg_match("/^enc|^cua|^tun|^tap|^l2tp|^pptp|^ppp|^ovpn|^ipsec|^gif|^gre|^lagg|^bridge|vlan|_wlan|_\d{0,4}_\d{0,4}$/i", $qinq['if'])) {
2974
			$missing_interfaces[] = $qinq['if'];
2975
			$do_assign = true;
2976
		}
2977
	}
2978

    
2979
	if (file_exists("{$g['tmp_path']}/assign_complete")) {
2980
		$do_assign = false;
2981
	}
2982

    
2983
	if (!empty($missing_interfaces) && $do_assign) {
2984
		file_put_contents("{$g['tmp_path']}/missing_interfaces", implode(' ', $missing_interfaces));
2985
	} else {
2986
		@unlink("{$g['tmp_path']}/missing_interfaces");
2987
	}
2988

    
2989
	return $do_assign;
2990
}
2991

    
2992
/* sync carp entries to other firewalls */
2993
function carp_sync_client() {
2994
	send_event("filter sync");
2995
}
2996

    
2997
/****f* util/isAjax
2998
 * NAME
2999
 *   isAjax - reports if the request is driven from prototype
3000
 * INPUTS
3001
 *   none
3002
 * RESULT
3003
 *   true/false
3004
 ******/
3005
function isAjax() {
3006
	return isset ($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
3007
}
3008

    
3009
/****f* util/timeout
3010
 * NAME
3011
 *   timeout - console input with timeout countdown. Note: erases 2 char of screen for timer. Leave space.
3012
 * INPUTS
3013
 *   optional, seconds to wait before timeout. Default 9 seconds.
3014
 * RESULT
3015
 *   returns 1 char of user input or null if no input.
3016
 ******/
3017
function timeout($timer = 9) {
3018
	while (!isset($key)) {
3019
		if ($timer >= 9) {
3020
			echo chr(8) . chr(8) . ($timer == 9 ? chr(32) : null) . "{$timer}";
3021
		} else {
3022
			echo chr(8). "{$timer}";
3023
		}
3024
		`/bin/stty -icanon min 0 time 25`;
3025
		$key = trim(`KEY=\`dd count=1 2>/dev/null\`; echo \$KEY`);
3026
		`/bin/stty icanon`;
3027
		if ($key == '') {
3028
			unset($key);
3029
		}
3030
		$timer--;
3031
		if ($timer == 0) {
3032
			break;
3033
		}
3034
	}
3035
	return $key;
3036
}
3037

    
3038
/****f* util/msort
3039
 * NAME
3040
 *   msort - sort array
3041
 * INPUTS
3042
 *   $array to be sorted, field to sort by, direction of sort
3043
 * RESULT
3044
 *   returns newly sorted array
3045
 ******/
3046
function msort($array, $id = "id", $sort_ascending = true) {
3047
	$temp_array = array();
3048
	if (!is_array($array)) {
3049
		return $temp_array;
3050
	}
3051
	while (count($array)>0) {
3052
		$lowest_id = 0;
3053
		$index = 0;
3054
		foreach ($array as $item) {
3055
			if (isset($item[$id])) {
3056
				if ($array[$lowest_id][$id]) {
3057
					if (strtolower($item[$id]) < strtolower($array[$lowest_id][$id])) {
3058
						$lowest_id = $index;
3059
					}
3060
				}
3061
			}
3062
			$index++;
3063
		}
3064
		$temp_array[] = $array[$lowest_id];
3065
		$array = array_merge(array_slice($array, 0, $lowest_id), array_slice($array, $lowest_id + 1));
3066
	}
3067
	if ($sort_ascending) {
3068
		return $temp_array;
3069
	} else {
3070
		return array_reverse($temp_array);
3071
	}
3072
}
3073

    
3074
/****f* util/is_URL
3075
 * NAME
3076
 *   is_URL
3077
 * INPUTS
3078
 *   $url: string to check
3079
 *   $httponly: Only allow HTTP or HTTPS scheme
3080
 * RESULT
3081
 *   Returns true if item is a URL
3082
 ******/
3083
function is_URL($url, $httponly = false) {
3084
	if (filter_var($url, FILTER_VALIDATE_URL)) {
3085
		if ($httponly) {
3086
			return in_array(strtolower(parse_url($url, PHP_URL_SCHEME)), ['http', 'https']);
3087
		}
3088
		return true;
3089
	}
3090
	return false;
3091
}
3092

    
3093
function is_file_included($file = "") {
3094
	$files = get_included_files();
3095
	if (in_array($file, $files)) {
3096
		return true;
3097
	}
3098

    
3099
	return false;
3100
}
3101

    
3102
/*
3103
 * Replace a value on a deep associative array using regex
3104
 */
3105
function array_replace_values_recursive($data, $match, $replace) {
3106
	if (empty($data)) {
3107
		return $data;
3108
	}
3109

    
3110
	if (is_string($data)) {
3111
		$data = preg_replace("/{$match}/", $replace, $data);
3112
	} else if (is_array($data)) {
3113
		foreach ($data as $k => $v) {
3114
			$data[$k] = array_replace_values_recursive($v, $match, $replace);
3115
		}
3116
	}
3117

    
3118
	return $data;
3119
}
3120

    
3121
/*
3122
	This function was borrowed from a comment on PHP.net at the following URL:
3123
	https://www.php.net/manual/en/function.array-merge-recursive.php#73843
3124
 */
3125
function array_merge_recursive_unique($array0, $array1) {
3126

    
3127
	$arrays = func_get_args();
3128
	$remains = $arrays;
3129

    
3130
	// We walk through each arrays and put value in the results (without
3131
	// considering previous value).
3132
	$result = array();
3133

    
3134
	// loop available array
3135
	foreach ($arrays as $array) {
3136

    
3137
		// The first remaining array is $array. We are processing it. So
3138
		// we remove it from remaining arrays.
3139
		array_shift($remains);
3140

    
3141
		// We don't care non array param, like array_merge since PHP 5.0.
3142
		if (is_array($array)) {
3143
			// Loop values
3144
			foreach ($array as $key => $value) {
3145
				if (is_array($value)) {
3146
					// we gather all remaining arrays that have such key available
3147
					$args = array();
3148
					foreach ($remains as $remain) {
3149
						if (array_key_exists($key, $remain)) {
3150
							array_push($args, $remain[$key]);
3151
						}
3152
					}
3153

    
3154
					if (count($args) > 2) {
3155
						// put the recursion
3156
						$result[$key] = call_user_func_array(__FUNCTION__, $args);
3157
					} else {
3158
						foreach ($value as $vkey => $vval) {
3159
							if (!is_array($result[$key])) {
3160
								$result[$key] = array();
3161
							}
3162
							$result[$key][$vkey] = $vval;
3163
						}
3164
					}
3165
				} else {
3166
					// simply put the value
3167
					$result[$key] = $value;
3168
				}
3169
			}
3170
		}
3171
	}
3172
	return $result;
3173
}
3174

    
3175

    
3176
/*
3177
 * converts a string like "a,b,c,d"
3178
 * into an array like array("a" => "b", "c" => "d")
3179
 */
3180
function explode_assoc($delimiter, $string) {
3181
	$array = explode($delimiter, $string);
3182
	$result = array();
3183
	$numkeys = floor(count($array) / 2);
3184
	for ($i = 0; $i < $numkeys; $i += 1) {
3185
		$result[$array[$i * 2]] = $array[$i * 2 + 1];
3186
	}
3187
	return $result;
3188
}
3189

    
3190
/*
3191
 * Given a string of text with some delimiter, look for occurrences
3192
 * of some string and replace all of those.
3193
 * $text - the text string (e.g. "abc,defg,x123,ipv4,xyz")
3194
 * $delimiter - the delimiter (e.g. ",")
3195
 * $element - the element to match (e.g. "defg")
3196
 * $replacement - the string to replace it with (e.g. "42")
3197
 * Returns the resulting delimited string (e.g. "abc,42,x123,ipv4,xyz")
3198
 */
3199
function replace_element_in_list($text, $delimiter, $element, $replacement) {
3200
	$textArray = explode($delimiter, $text);
3201
	while (($entry = array_search($element, $textArray)) !== false) {
3202
		$textArray[$entry] = $replacement;
3203
	}
3204
	return implode(',', $textArray);
3205
}
3206

    
3207
/* Return system's route table */
3208
function route_table() {
3209
	exec("/usr/bin/netstat --libxo json -nWr", $rawdata, $rc);
3210

    
3211
	if ($rc != 0) {
3212
		return array();
3213
	}
3214

    
3215
	$netstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
3216
	$netstatarr = $netstatarr['statistics']['route-information']
3217
	    ['route-table']['rt-family'];
3218

    
3219
	$result = array();
3220
	$result['inet'] = array();
3221
	$result['inet6'] = array();
3222
	foreach ($netstatarr as $item) {
3223
		if ($item['address-family'] == 'Internet') {
3224
			$result['inet'] = $item['rt-entry'];
3225
		} else if ($item['address-family'] == 'Internet6') {
3226
			$result['inet6'] = $item['rt-entry'];
3227
		}
3228
	}
3229
	unset($netstatarr);
3230

    
3231
	return $result;
3232
}
3233

    
3234
/**
3235
 * Checks if a route was sourced from a dynamic routing protocol like
3236
 * BGP or OSPF by looking at the route flags.
3237
 * 
3238
 * @param string $target IP address or subnet, or 'default' for the default route
3239
 * @param string $ipprotocol either 'inet' for IPv4, or 'inet6' for IPv6
3240
 * 
3241
 * @return bool true if it's a dynamic route
3242
 */
3243
function is_dynamic_route(string $target, string $ipprotocol = ''): bool {
3244
	if (is_v4($target) || (($target == 'default') && ($ipprotocol == 'inet'))) {
3245
		$inet = '4';
3246
	} elseif (is_v6($target) || (($target == 'default') && ($ipprotocol == 'inet6'))) {
3247
		$inet = '6';
3248
	} else {
3249
		return false;
3250
	}
3251

    
3252
	/* check for the PROTO* flag instead of STATIC
3253
	 * see https://redmine.pfsense.org/issues/14717
3254
	 * mwexec() returns false if the flag exists, so invert it */
3255
	if (!mwexec("/sbin/route -n{$inet} get " . escapeshellarg($target) . " 2>/dev/null | /usr/bin/egrep 'flags: <.*PROTO.*>'", true)) {
3256
		return true;
3257
	}
3258

    
3259
	return false;
3260
}
3261

    
3262
/* Get static route for specific destination */
3263
function route_get($target, $ipprotocol = '', $useroute = false) {
3264
	if (!empty($ipprotocol)) {
3265
		$family = $ipprotocol;
3266
	} else if (is_v4($target)) {
3267
		$family = 'inet';
3268
	} else if (is_v6($target)) {
3269
		$family = 'inet6';
3270
	}
3271

    
3272
	if (empty($family)) {
3273
		return array();
3274
	}
3275

    
3276
	if ($useroute) {
3277
		if ($family == 'inet') {
3278
			$inet = '4';
3279
		} else {
3280
			$inet = '6';
3281
		}
3282
		$interface = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/interface:/{print $2}'");
3283
		if (empty($interface)) {
3284
			return array();
3285
		} elseif ($interface == 'lo0') {
3286
			// interface assigned IP address
3287
			foreach (array_keys(config_get_path('interfaces', [])) as $intf) {
3288
				if ((($inet == '4') && (get_interface_ip($intf) == $target)) ||
3289
				    (($inet == '6') && (get_interface_ipv6($intf) == $target))) {
3290
					$interface = convert_friendly_interface_to_real_interface_name($intf);
3291
					$gateway = $interface;
3292
					break;
3293
				}
3294
			}
3295
		} else {
3296
			$gateway = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/gateway:/{print $2}'");
3297
			if (!$gateway) {
3298
				// non-local gateway
3299
				$gateway = get_interface_mac($interface);
3300
			}
3301
		}
3302
		$result[] = array('gateway' => $gateway, 'interface-name' => $interface);
3303
	} else {
3304
		$rtable = route_table();
3305
		if (empty($rtable)) {
3306
			return array();
3307
		}
3308

    
3309
		$result = array();
3310
		foreach ($rtable[$family] as $item) {
3311
			if ($item['destination'] == $target ||
3312
			    ip_in_subnet($target, $item['destination'])) {
3313
				$result[] = $item;
3314
			}
3315
		}
3316
	}
3317

    
3318
	return $result;
3319
}
3320

    
3321
/* Get default route */
3322
function route_get_default($ipprotocol) {
3323
	if (empty($ipprotocol) || ($ipprotocol != 'inet' &&
3324
	    $ipprotocol != 'inet6')) {
3325
		return '';
3326
	}
3327

    
3328
	$route = route_get('default', $ipprotocol, true);
3329

    
3330
	if (empty($route)) {
3331
		return '';
3332
	}
3333

    
3334
	if (!isset($route[0]['gateway'])) {
3335
		return '';
3336
	}
3337

    
3338
	return $route[0]['gateway'];
3339
}
3340

    
3341
/* Delete a static route */
3342
function route_del($target, $ipprotocol = '') {
3343
	if (empty($target)) {
3344
		return;
3345
	}
3346

    
3347
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
3348
	    $ipprotocol != 'inet6') {
3349
		return false;
3350
	}
3351

    
3352
	$route = route_get($target, $ipprotocol, true);
3353

    
3354
	if (empty($route)) {
3355
		return;
3356
	}
3357

    
3358
	$target_prefix = '';
3359
	if (is_subnet($target)) {
3360
		$target_prefix = '-net';
3361
	} else if (is_ipaddr($target)) {
3362
		$target_prefix = '-host';
3363
	}
3364

    
3365
	if (!empty($ipprotocol)) {
3366
		$target_prefix .= " -{$ipprotocol}";
3367
	} else if (is_v6($target)) {
3368
		$target_prefix .= ' -inet6';
3369
	} else if (is_v4($target)) {
3370
		$target_prefix .= ' -inet';
3371
	}
3372

    
3373
	foreach ($route as $item) {
3374
		if (substr($item['gateway'], 0, 5) == 'link#') {
3375
			continue;
3376
		}
3377

    
3378
		if (is_macaddr($item['gateway'])) {
3379
			$gw = '-iface ' . $item['interface-name'];
3380
		} else {
3381
			$gw = $item['gateway'];
3382
		}
3383

    
3384
		exec(escapeshellcmd("/sbin/route del {$target_prefix} " .
3385
		    "{$target} {$gw}"), $output, $rc);
3386

    
3387
		if (config_path_enabled('system', 'route-debug')) {
3388
			log_error("ROUTING debug: " . microtime() .
3389
			    " - DEL RC={$rc} - {$target} - gw: " . $gw);
3390
			file_put_contents("/dev/console", "\n[" . getmypid() .
3391
			    "] ROUTE DEL: {$target_prefix} {$target} {$gw} " .
3392
			    "result: {$rc}");
3393
		}
3394
	}
3395
}
3396

    
3397
/*
3398
 * Add static route.  If it already exists, remove it and re-add
3399
 *
3400
 * $target - IP, subnet or 'default'
3401
 * $gw     - gateway address
3402
 * $iface  - Network interface
3403
 * $args   - Extra arguments for /sbin/route
3404
 * $ipprotocol - 'inet' or 'inet6'.  Mandatory when $target == 'default'
3405
 *
3406
 */
3407
function route_add_or_change($target, $gw, $iface = '', $args = '',
3408
    $ipprotocol = '') {
3409
	if (empty($target) || (empty($gw) && empty($iface))) {
3410
		return false;
3411
	}
3412

    
3413
	if ($target == 'default' && empty($ipprotocol)) {
3414
		return false;
3415
	}
3416

    
3417
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
3418
	    $ipprotocol != 'inet6') {
3419
		return false;
3420
	}
3421

    
3422
	/* use '-host' for IPv6 /128 routes, see https://redmine.pfsense.org/issues/11594 */
3423
	if (is_subnetv4($target) || (is_subnetv6($target) && (subnet_size($target) > 1))) {
3424
		$target_prefix = '-net';
3425
	} else if (is_ipaddr($target)) {
3426
		$target_prefix = '-host';
3427
	}
3428

    
3429
	if (!empty($ipprotocol)) {
3430
		$target_prefix .= " -{$ipprotocol}";
3431
	} else if (is_v6($target)) {
3432
		$target_prefix .= ' -inet6';
3433
	} else if (is_v4($target)) {
3434
		$target_prefix .= ' -inet';
3435
	}
3436

    
3437
	/* If there is another route to the same target, remove it */
3438
	route_del($target, $ipprotocol);
3439

    
3440
	$params = '';
3441
	if (!empty($iface) && does_interface_exist($iface)) {
3442
		$params .= " -iface {$iface}";
3443
	}
3444
	if (is_ipaddr($gw)) {
3445
		/* set correct linklocal gateway address,
3446
		 * see https://redmine.pfsense.org/issues/11713
3447
		 * and https://redmine.pfsense.org/issues/11806 */
3448
		if (is_ipaddrv6($gw) && is_linklocal($gw) && empty(get_ll_scope($gw))) {
3449
			$routeget = route_get($gw, 'inet6', true);
3450
			$gw .= "%" . $routeget[0]['interface-name'];
3451
		}
3452
		$params .= " " . $gw;
3453
	}
3454

    
3455
	if (empty($params)) {
3456
		if (!empty($gw) && !empty($iface)) {
3457
			log_error("route_add_or_change: Invalid gateway ({$gw}) and network interface was not found ({$iface})");
3458
		} elseif (!empty($gw)) {
3459
			log_error("route_add_or_change: Invalid gateway ({$gw})");
3460
		} else {
3461
			log_error("route_add_or_change: Network interface was not found ({$iface})");
3462
		}
3463
		return false;
3464
	}
3465

    
3466
	exec(escapeshellcmd("/sbin/route add {$target_prefix} " .
3467
	    "{$target} {$args} {$params}"), $output, $rc);
3468

    
3469
	if (config_path_enabled('system', 'route-debug')) {
3470
		log_error("ROUTING debug: " . microtime() .
3471
		    " - ADD RC={$rc} - {$target} {$args}");
3472
		file_put_contents("/dev/console", "\n[" . getmypid() .
3473
		    "] ROUTE ADD: {$target_prefix} {$target} {$args} " .
3474
		    "{$params} result: {$rc}");
3475
	}
3476

    
3477
	return ($rc == 0);
3478
}
3479

    
3480
function set_ipv6routes_mtu($interface, $mtu) {
3481
	$ipv6mturoutes = array();
3482
	$if = convert_real_interface_to_friendly_interface_name($interface);
3483
	if (empty($if) || !config_get_path("interfaces/{$if}/ipaddrv6")) {
3484
		return;
3485
	}
3486
	$a_gateways = get_gateways();
3487
	$a_staticroutes = get_staticroutes(false, false, true);
3488
	foreach ($a_gateways as $gate) {
3489
		foreach ($a_staticroutes as $sroute) {
3490
			if (($gate['interface'] == $interface) &&
3491
			    ($sroute['gateway'] == $gate['name']) &&
3492
			    (is_ipaddrv6($gate['gateway']))) {
3493
				$tgt = $sroute['network'];
3494
				$gateway = $gate['gateway'];
3495
				$ipv6mturoutes[$tgt] = $gateway;
3496
			}
3497
		}
3498
		if (($gate['interface'] == $interface) &&
3499
		    $gate['isdefaultgw'] && is_ipaddrv6($gate['gateway'])) {
3500
			$tgt = "default";
3501
			$gateway = $gate['gateway'];
3502
			$ipv6mturoutes[$tgt] = $gateway;
3503
		}
3504
	}
3505
	foreach ($ipv6mturoutes as $tgt => $gateway) {
3506
		mwexec("/sbin/route change -6 -mtu " . escapeshellarg($mtu) .
3507
		    " " . escapeshellarg($tgt) . " " .
3508
		    escapeshellarg($gateway));
3509
	}
3510
}
3511

    
3512
function alias_to_subnets_recursive($name, $returnhostnames = false) {
3513
	global $aliastable;
3514
	$result = array();
3515
	if (!isset($aliastable[$name])) {
3516
		return $result;
3517
	}
3518
	$subnets = preg_split('/\s+/', $aliastable[$name]);
3519
	foreach ($subnets as $net) {
3520
		if (is_alias($net)) {
3521
			$sub = alias_to_subnets_recursive($net, $returnhostnames);
3522
			$result = array_merge($result, $sub);
3523
			continue;
3524
		} elseif (!is_subnet($net)) {
3525
			if (is_ipaddrv4($net)) {
3526
				$net .= "/32";
3527
			} else if (is_ipaddrv6($net)) {
3528
				$net .= "/128";
3529
			} else if ($returnhostnames === false || !is_fqdn($net)) {
3530
				continue;
3531
			}
3532
		}
3533
		$result[] = $net;
3534
	}
3535
	return $result;
3536
}
3537

    
3538
function get_staticroutes($returnsubnetsonly = false, $returnhostnames = false, $returnenabledroutesonly = false) {
3539
	/* Bail if there are no routes, but return an array always so callers don't have to check. */
3540
	if (empty(config_get_path('staticroutes/route'))) {
3541
		return array();
3542
	}
3543

    
3544
	$allstaticroutes = array();
3545
	$allsubnets = array();
3546
	/* Loop through routes and expand aliases as we find them. */
3547
	foreach (config_get_path('staticroutes/route', []) as $route) {
3548
		if ($returnenabledroutesonly && isset($route['disabled'])) {
3549
			continue;
3550
		}
3551

    
3552
		if (is_alias($route['network'])) {
3553
			foreach (alias_to_subnets_recursive($route['network'], $returnhostnames) as $net) {
3554
				$temproute = $route;
3555
				$temproute['network'] = $net;
3556
				$allstaticroutes[] = $temproute;
3557
				$allsubnets[] = $net;
3558
			}
3559
		} elseif (is_subnet($route['network'])) {
3560
			$allstaticroutes[] = $route;
3561
			$allsubnets[] = $route['network'];
3562
		}
3563
	}
3564
	if ($returnsubnetsonly) {
3565
		return $allsubnets;
3566
	} else {
3567
		return $allstaticroutes;
3568
	}
3569
}
3570

    
3571
/****f* util/get_alias_list
3572
 * NAME
3573
 *   get_alias_list - Provide a list of aliases. Used when auto-completing GUI fields.
3574
 * INPUTS
3575
 *   $type          - Optional, can be a string or array specifying what type(s) of aliases you need.
3576
 * RESULT
3577
 *   Array containing list of aliases.
3578
 *   If $type is unspecified, all aliases are returned.
3579
 *   If $type is a comma-separated string, all aliases of the type(s) specified in $type are returned.
3580
 */
3581
function get_alias_list($type = '') {
3582
	$requested_types = [];
3583
	if (!empty($type)) {
3584
		if (!is_string($type)) {
3585
			return [];
3586
		}
3587
		$requested_types = explode(',', $type);
3588
	}
3589

    
3590
	$aliases = config_get_path('aliases/alias', []);
3591

    
3592
	// Reserved aliases take precedence.
3593
	$reserved_aliases = get_reserved_table_names();
3594
	if (!empty($reserved_aliases)) {
3595
		foreach ($aliases as $id => $info) {
3596
			if (isset($reserved_aliases[$info['name']])) {
3597
				$aliases[$id] = $reserved_aliases[$info['name']];
3598
				unset($reserved_alias[$info['name']]);
3599
			}
3600
		}
3601
		$aliases = array_merge($aliases, array_values($reserved_aliases));
3602
	}
3603

    
3604
	foreach ($aliases as $alias) {
3605
		if (empty($requested_types) || in_array($alias['type'], $requested_types)) {
3606
			$result[] = $alias['name'];
3607
		}
3608
	}
3609
	return $result;
3610
}
3611

    
3612
/* Returns the current alias contents sorted by name in case insensitive and
3613
 * natural order.
3614
 * https://redmine.pfsense.org/issues/14015 */
3615
function get_sorted_aliases() {
3616
	$aliases = config_get_path('aliases/alias', []);
3617
	uasort($aliases, function ($a, $b) { return strnatcasecmp($a['name'], $b['name']); });
3618
	return $aliases;
3619
}
3620

    
3621
/* returns an array consisting of every element of $haystack that is not equal to $needle. */
3622
function array_exclude($needle, $haystack) {
3623
	$result = array();
3624
	if (is_array($haystack)) {
3625
		foreach ($haystack as $thing) {
3626
			if ($needle !== $thing) {
3627
				$result[] = $thing;
3628
			}
3629
		}
3630
	}
3631
	return $result;
3632
}
3633

    
3634
/* Define what is preferred, IPv4 or IPv6 */
3635
function prefer_ipv4_or_ipv6() {
3636
	if (config_path_enabled('system', 'prefer_ipv4')) {
3637
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv4");
3638
	} else {
3639
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv6");
3640
	}
3641
}
3642

    
3643
/* Redirect to page passing parameters via POST */
3644
function post_redirect($page, $params) {
3645
	if (!is_array($params)) {
3646
		return;
3647
	}
3648

    
3649
	print "<html><body><form action=\"{$page}\" name=\"formredir\" method=\"post\">\n";
3650
	foreach ($params as $key => $value) {
3651
		print "<input type=\"hidden\" name=\"{$key}\" value=\"{$value}\" />\n";
3652
	}
3653
	print "</form>\n";
3654
	print "<script type=\"text/javascript\">\n";
3655
	print "//<![CDATA[\n";
3656
	print "document.formredir.submit();\n";
3657
	print "//]]>\n";
3658
	print "</script>\n";
3659
	print "</body></html>\n";
3660
}
3661

    
3662
/**
3663
 * Locate disks that can be queried for S.M.A.R.T. data.
3664
 * 
3665
 * @param bool $check_controller If true (default), use the controller
3666
 *                               device name; otherwise use the
3667
 *                               original disk name.
3668
 * 
3669
 * @return array List of drive names
3670
 */
3671
function get_drive_list(bool $check_controller = true) {
3672
	// list of drives to return
3673
	$drive_list = [];
3674

    
3675
	/* SMART supports some disks directly, and some controllers directly,
3676
	 * See https://redmine.pfsense.org/issues/9042 */
3677
	$supported_controllers = [
3678
		'nvme'
3679
	];
3680

    
3681
	// get disk devices; ignore sub-devices
3682
	$devices = [];
3683
	preg_match_all('/\b(?P<disks>[[:alpha:]]+\d+)\b/', get_single_sysctl("kern.disks"), $devices);
3684
	if (!empty($devices['disks']) && is_array($devices['disks'])) {
3685
		// ignore duplicate matches
3686
		$devices = array_unique($devices['disks']);
3687
		foreach ($devices as $device) {
3688
			// get device info
3689
			$output = shell_exec("/usr/sbin/diskinfo -v /dev/{$device}");
3690
			if (empty($output)) {
3691
				continue;
3692
			}
3693

    
3694
			// don't check the controller
3695
			if (!$check_controller) {
3696
				$drive_list[] = $device;
3697
				continue;
3698
			}
3699

    
3700
			// get device controller
3701
			$device_controller = [];
3702
			preg_match('/^\s+(?P<controller>[\w\-]+)\s+# Attachment/im', $output, $device_controller);
3703
			if (!empty($device_controller['controller']) && is_string($device_controller['controller'])) {
3704
				$device_controller = $device_controller['controller'];
3705

    
3706
				// supported controllers are queried directly
3707
				$regex_pattern = implode('|', array_values($supported_controllers));
3708
				$regex_pattern = "/\b(?:{$regex_pattern})\d+\b/";
3709
				if (preg_match($regex_pattern, $device_controller)) {
3710
					$drive_list[] = $device_controller;
3711
					// skip further processing for this device
3712
					continue;
3713
				}
3714
			}
3715

    
3716
			// no attachment, or other controller
3717
			$drive_list[] = $device;
3718
		}
3719
	}
3720

    
3721
	return $drive_list;
3722
}
3723

    
3724
// Validate a network address
3725
//	$addr: the address to validate
3726
//	$type: IPV4|IPV6|IPV4V6
3727
//	$label: the label used by the GUI to display this value. Required to compose an error message
3728
//	$err_msg: pointer to the callers error message array so that error messages can be added to it here
3729
//	$alias: are aliases permitted for this address?
3730
// Returns:
3731
//	IPV4 - if $addr is a valid IPv4 address
3732
//	IPV6 - if $addr is a valid IPv6 address
3733
//	ALIAS - if $alias=true and $addr is an alias
3734
//	false - otherwise
3735

    
3736
function validateipaddr(&$addr, $type, $label, &$err_msg, $alias=false) {
3737
	switch ($type) {
3738
		case IPV4:
3739
			if (is_ipaddrv4($addr)) {
3740
				return IPV4;
3741
			} else if ($alias) {
3742
				if (is_alias($addr)) {
3743
					return ALIAS;
3744
				} else {
3745
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address or alias."), $label);
3746
					return false;
3747
				}
3748
			} else {
3749
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address."), $label);
3750
				return false;
3751
			}
3752
		break;
3753
		case IPV6:
3754
			if (is_ipaddrv6($addr)) {
3755
				$addr = strtolower($addr);
3756
				return IPV6;
3757
			} else if ($alias) {
3758
				if (is_alias($addr)) {
3759
					return ALIAS;
3760
				} else {
3761
					$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address or alias."), $label);
3762
					return false;
3763
				}
3764
			} else {
3765
				$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address."), $label);
3766
				return false;
3767
			}
3768
		break;
3769
		case IPV4V6:
3770
			if (is_ipaddrv6($addr)) {
3771
				$addr = strtolower($addr);
3772
				return IPV6;
3773
			} else if (is_ipaddrv4($addr)) {
3774
				return IPV4;
3775
			} else if ($alias) {
3776
				if (is_alias($addr)) {
3777
					return ALIAS;
3778
				} else {
3779
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address or alias."), $label);
3780
					return false;
3781
				}
3782
			} else {
3783
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address."), $label);
3784
				return false;
3785
			}
3786
		break;
3787
	}
3788

    
3789
	return false;
3790
}
3791

    
3792
/* From DUID configuration inputs, format a string that looks (more) like the expected raw DUID format:
3793
 * 1) For DUIDs entered as a known DUID type, convert to a hexstring and prepend the DUID number, after having done the following:
3794
 *     a) For DUID-LLT and DUID-EN, convert the time/enterprise ID input to hex and append the link-layer address/identifier input.
3795
 *     b) For DUID-LLT and DUID-LL, prepend a hardware type of 1.
3796
 *     c) For DUID-UUID, remove any "-".
3797
 * 2) Replace any remaining "-" with ":".
3798
 * 3) If any components are input with just a single char (hex digit hopefully), put a "0" in front.
3799
 * 4) The first two components should be a 16-bit integer (little- or big-endian, depending on the current machine type) that
3800
 *    is equal to the number of other components. If not, prepend this as "nn:00" (all pfSense builds are little-endian).
3801
 *    This is convenience, because the DUID reported by dhcp6c in logs does not include this count, which corresponds to the
3802
 *    option-len field of DHCPv6's OPTION_CLIENTID option.
3803
 *
3804
 * The final result should be closer to:
3805
 *
3806
 * "nn:00:00:0n:nn:nn:nn:..."
3807
 *
3808
 * This function does not validate the input. is_duid() will do validation.
3809
 */
3810
function format_duid($duidtype, $duidpt1, $duidpt2=null) {
3811
	if ($duidpt2)
3812
		$duidpt1 = implode(':', str_split(str_pad(dechex($duidpt1), 8, '0', STR_PAD_LEFT), 2)) . ':' . $duidpt2;
3813

    
3814
	/* Make hexstrings */
3815
	if ($duidtype) {
3816
		switch ($duidtype) {
3817
		/* Add a hardware type to DUID-LLT and DUID-LL; assume Ethernet */
3818
		case 1:
3819
		case 3:
3820
			$duidpt1 = '00:01:' . $duidpt1;
3821
			break;
3822
		/* Remove '-' from given UUID and insert ':' every 2 characters */
3823
		case 4:
3824
			$duidpt1 = implode(':', str_split(str_replace('-', '', $duidpt1), 2));
3825
			break;
3826
		default:
3827
		}
3828
		$duidpt1 = '00:0' . $duidtype . ':' . $duidpt1;
3829
	}
3830

    
3831
	$values = explode(':', strtolower(str_replace('-', ':', $duidpt1)));
3832

    
3833
	if (hexdec($values[0]) != count($values) - 2)
3834
		array_unshift($values, dechex(count($values)), '00');
3835

    
3836
	array_walk($values, function(&$value) {
3837
		$value = str_pad($value, 2, '0', STR_PAD_LEFT);
3838
	});
3839

    
3840
	return implode(":", $values);
3841
}
3842

    
3843
/* Returns true if $dhcp6duid is a valid DUID entry.
3844
 * Parse the entry to check for valid length according to known DUID types.
3845
 */
3846
function is_duid($dhcp6duid) {
3847
	$values = explode(":", $dhcp6duid);
3848
	if (hexdec($values[0]) == count($values) - 2) {
3849
		switch (hexdec($values[2] . $values[3])) {
3850
		case 0:
3851
			return false;
3852
			break;
3853
		case 1:
3854
			if (count($values) != 16 || strlen($dhcp6duid) != 47)
3855
				return false;
3856
			break;
3857
		case 3:
3858
			if (count($values) != 12 || strlen($dhcp6duid) != 35)
3859
				return false;
3860
			break;
3861
		case 4:
3862
			if (count($values) != 20 || strlen($dhcp6duid) != 59)
3863
				return false;
3864
			break;
3865
		/* DUID is up to 128 octets; allow 2 octets for type code, 2 more for option-len */
3866
		default:
3867
			if (count($values) > 132 || strlen($dhcp6duid) != count($values) * 3 - 1)
3868
				return false;
3869
		}
3870
	} else
3871
		return false;
3872

    
3873
	for ($i = 0; $i < count($values); $i++) {
3874
		if (ctype_xdigit($values[$i]) == false)
3875
			return false;
3876
		if (hexdec($values[$i]) < 0 || hexdec($values[$i]) > 255)
3877
			return false;
3878
	}
3879

    
3880
	return true;
3881
}
3882

    
3883
/* Write the DHCP6 DUID file */
3884
function write_dhcp6_duid($duidstring) {
3885
	// Create the hex array from the dhcp6duid config entry and write to file
3886
	global $g;
3887

    
3888
	if(!is_duid($duidstring)) {
3889
		log_error(gettext("Error: attempting to write DUID file - Invalid DUID detected"));
3890
		return false;
3891
	}
3892
	$temp = str_replace(":","",$duidstring);
3893
	$duid_binstring = pack("H*",$temp);
3894
	if ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "wb")) {
3895
		fwrite($fd, $duid_binstring);
3896
		fclose($fd);
3897
		return true;
3898
	}
3899
	log_error(gettext("Error: attempting to write DUID file - File write error"));
3900
	return false;
3901
}
3902

    
3903
/* returns duid string from 'vardb_path']}/dhcp6c_duid' */
3904
function get_duid_from_file() {
3905
	global $g;
3906

    
3907
	$duid_ASCII = "";
3908
	$count = 0;
3909

    
3910
	if (file_exists("{$g['vardb_path']}/dhcp6c_duid") &&
3911
	    ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "r"))) {
3912
		$fsize = filesize("{$g['vardb_path']}/dhcp6c_duid");
3913
		if ($fsize <= 132) {
3914
			$buffer = fread($fd, $fsize);
3915
			while($count < $fsize) {
3916
				$duid_ASCII .= bin2hex($buffer[$count]);
3917
				$count++;
3918
				if($count < $fsize) {
3919
					$duid_ASCII .= ":";
3920
				}
3921
			}
3922
		}
3923
		fclose($fd);
3924
	}
3925
	//if no file or error with read then the string returns blanked DUID string
3926
	if(!is_duid($duid_ASCII)) {
3927
		return "--:--:--:--:--:--:--:--:--:--:--:--:--:--:--:--";
3928
	}
3929
	return($duid_ASCII);
3930
}
3931

    
3932
/* Replaces the Mac OS 9 and earlier (\r) and DOS/Windows (\r\n) newlines with the Unix equivalent (\n). */
3933
function unixnewlines($text) {
3934
	return preg_replace('/\r\n?/', "\n", $text);
3935
}
3936

    
3937
function array_remove_duplicate($array, $field) {
3938
	$cmp = array();
3939
	foreach ($array as $sub) {
3940
		if (isset($sub[$field])) {
3941
			$cmp[] = $sub[$field];
3942
		}
3943
	}
3944
	$unique = array_unique(array_reverse($cmp, true));
3945
	foreach (array_keys($unique) as $k) {
3946
		$new[] = $array[$k];
3947
	}
3948
	return $new;
3949
}
3950

    
3951
/**
3952
 * Return a value specified by a path of keys in a nested array, if it exists.
3953
 * 
3954
 * @param array  $arr     Array to search
3955
 * @param string $path    Path with '/' separators
3956
 * @param mixed  $default Value to return if the path is not found
3957
 * 
3958
 * @return mixed Value at path or $default if the path does not exist or if the
3959
 *               path keys an empty string and $default is non-null
3960
 */
3961
function array_get_path(array &$arr, string $path, $default = null) {
3962
	$vpath = explode('/', $path);
3963
	$el = $arr;
3964
	foreach ($vpath as $key) {
3965
		if (mb_strlen($key) == 0) {
3966
			continue;
3967
		}
3968
		if (is_array($el) && array_key_exists($key, $el)) {
3969
			$el = $el[$key];
3970
		} else {
3971
			return ($default);
3972
		}
3973
	}
3974

    
3975
	if (($default !== null) && ($el === '')) {
3976
		return ($default);
3977
	}
3978

    
3979
	return ($el);
3980
}
3981

    
3982
/**
3983
 * Initialize an arbitrary array multiple levels deep. Any scalars in the path,
3984
 * including the leaf node, are overridden with an array.
3985
 * 
3986
 * @param mixed  $arr  Top of array
3987
 * @param string $path Path with '/' separators
3988
 */
3989
function array_init_path(mixed &$arr, ?string $path) {
3990
	$value = [];
3991
	$vpath = explode('/', $path);
3992

    
3993
	// get the path's leaf node; ignore trailing and contiguous forward-slashes
3994
	do {
3995
		$vkey = array_pop($vpath);
3996
	} while (isset($vkey) && (mb_strlen($vkey) == 0));
3997

    
3998
	// make sure $arr is always an array
3999
	if (!is_array($arr)) {
4000
		$arr = $value;
4001
	}
4002

    
4003
	if ($vkey == null) {
4004
		// path is root, nothing else to do
4005
		return true;
4006
	}
4007

    
4008
	// traverse the array and replace any scalars along the path
4009
	$el = &$arr;
4010
	foreach ($vpath as $key) {
4011
		if (mb_strlen($key) == 0) {
4012
			continue;
4013
		}
4014
		if (!array_key_exists($key, $el) || !is_array($el[$key])) {
4015
			$el[$key] = [];
4016
		}
4017
		$el = &$el[$key];
4018
	}
4019

    
4020
	// set the leaf node value
4021
	if (!array_key_exists($vkey, $el) || !is_array($el[$vkey])) {
4022
		$el[$vkey] = $value;
4023
	}
4024
	
4025
	return true;
4026
}
4027

    
4028
/**
4029
 * Set a value by path in a nested array, creating arrays for intermediary keys
4030
 * as necessary. If the path cannot be reached because an intermediary exists
4031
 * but is not empty or an array, return $default. A trailing '/' in the path
4032
 * will add $value to the leaf node array; if not an array, the leaf node is
4033
 * overwritten to be an array containing $value.
4034
 * 
4035
 * @param array  $arr     Array to search
4036
 * @param string $path    Path with '/' separators
4037
 * @param mixed  $value   Value to set
4038
 * @param mixed  $default Value to return if the path is not found
4039
 * 
4040
 * @return mixed $value or $default if the path prefix does not exist
4041
 */
4042
function array_set_path(array &$arr, string $path, $value, $default = null) {
4043
	$vpath = explode('/', $path);
4044
	// check for trailing forward-slash
4045
	if (($path != '') && ($vpath[array_key_last($vpath)] === '')) {
4046
		$append = true;
4047
	} else {
4048
		$append = false;
4049
	}
4050

    
4051
	// get the path's leaf node; ignore trailing and contiguous forward-slashes
4052
	do {
4053
		$vkey = array_pop($vpath);
4054
	} while (isset($vkey) && (mb_strlen($vkey) == 0));
4055

    
4056
	// path is root, make sure $arr is always an array
4057
	if ($vkey == null) {
4058
		// avoid creating a single-element root (e.g. [0 => []])
4059
		if ($append) {
4060
			$arr[] = (is_array($value)) ? $value : [$value];
4061
		} else {
4062
			$arr = (is_array($value)) ? $value : [$value];
4063
		}
4064
		return ($value);
4065
	}
4066

    
4067
	// traverse the array and get reference to the leaf node
4068
	$el =& $arr;
4069
	foreach ($vpath as $key) {
4070
		if (mb_strlen($key) == 0) {
4071
			continue;
4072
		}
4073
		if (array_key_exists($key, $el) && !empty($el[$key])) {
4074
			if (!is_array($el[$key])) {
4075
				return ($default);
4076
			}
4077
		} else {
4078
			$el[$key] = [];
4079
		}
4080
		$el =& $el[$key];
4081
	}
4082

    
4083
	// set the leaf node value
4084
	if ($append) {
4085
		if (is_array($el[$vkey])) {
4086
			$el[$vkey][] = $value;
4087
		} else {
4088
			$el[$vkey] = [$value];
4089
		}
4090
	} else {
4091
		$el[$vkey] = $value;
4092
	}
4093
	
4094
	return ($value);
4095
}
4096

    
4097
/**
4098
 * Determine whether a path in a nested array has a non-null value keyed by
4099
 * $enable_key.
4100
 * 
4101
 * @param array  $arr        Array to search
4102
 * @param string $path       Path with '/' separators
4103
 * @param string $enable_key Optional alternative key value for the enable key
4104
 * @param mixed  $default    Value to return if the path is not found
4105
 * 
4106
 * @return mixed|bool true if $enable_key exists in the array at $path, and has
4107
 *                    a non-null value, otherwise false; or $default if the
4108
 *                    path prefix does not exist.
4109
 */
4110
function array_path_enabled(array &$arr, string $path, $enable_key = "enable", $default = false) {
4111
	$el = array_get_path($arr, $path, $default);
4112
	return is_array($el) ? isset($el[$enable_key]) : $default;
4113
}
4114

    
4115
/**
4116
 * Remove a key from the nested array by path.
4117
 * 
4118
 * @param array  $arr     Array to search
4119
 * @param string $path    Path with '/' separators
4120
 * @param mixed  $default Value to return if the path is not found
4121
 * 
4122
 * @return mixed|array Copy of the removed value or null; or $default if the
4123
 *                     path prefix does not exist.
4124
 */
4125
function array_del_path(array &$arr, string $path, $default = null) {
4126
	$vpath = explode('/', $path);
4127
	$vkey = array_pop($vpath);
4128
	$el =& $arr;
4129
	foreach($vpath as $key) {
4130
		if (mb_strlen($key) == 0) {
4131
			continue;
4132
		}
4133
		if (is_array($el) && array_key_exists($key, $el)) {
4134
			$el =& $el[$key];
4135
		} else {
4136
			return $default;
4137
		}
4138
	}
4139

    
4140
	if (!(is_array($el) && array_key_exists($vkey, $el))) {
4141
		return $default;
4142
	}
4143

    
4144
	$ret = $el[$vkey];
4145
	unset($el[$vkey]);
4146
	return ($ret);
4147
}
4148

    
4149
/**
4150
 * Alternate version of array_merge_recursive() that replaces the values
4151
 * for string keys instead of merging them into a new array.
4152
 */
4153
function array_merge_override_recursive(array ...$arrays): array {
4154
	$merged = [];
4155

    
4156
	foreach ($arrays as $array) {
4157
		foreach ($array as $key => $value) {
4158
			if (!isset($merged[$key]) || !is_array($merged[$key]) || !is_array($value)) {
4159
				// Override with the new value
4160
				$merged[$key] = $value;
4161
				continue;
4162
			}
4163

    
4164
			if (is_numericint($key)) {
4165
				// Append lists
4166
				$merged[] = $value;
4167
			} else {
4168
				$merged[$key] = array_merge_override_recursive($merged[$key], $value);
4169
			}
4170
		}
4171
	}
4172

    
4173
	return ($merged);
4174
}
4175

    
4176
function dhcpd_date_adjust_gmt($dt) {
4177
	foreach (config_get_path('dhcpd', []) as $dhcpditem) {
4178
		if (empty($dhcpditem)) {
4179
			continue;
4180
		}
4181
		if ($dhcpditem['dhcpleaseinlocaltime'] == "yes") {
4182
			$ts = strtotime($dt . " GMT");
4183
			if ($ts !== false) {
4184
				return date("Y/m/d H:i:s", $ts);
4185
			}
4186
		}
4187
	}
4188

    
4189
	/*
4190
	 * If we did not need to convert to local time or the conversion
4191
	 * failed, just return the input.
4192
	 */
4193
	return $dt;
4194
}
4195

    
4196
global $supported_image_types;
4197
$supported_image_types = array(
4198
	IMAGETYPE_JPEG,
4199
	IMAGETYPE_PNG,
4200
	IMAGETYPE_GIF,
4201
	IMAGETYPE_WEBP
4202
);
4203

    
4204
function is_supported_image($image_filename) {
4205
	global $supported_image_types;
4206
	$img_info = getimagesize($image_filename);
4207

    
4208
	/* If it's not an image, or it isn't in the supported list, return false */
4209
	if (($img_info === false) ||
4210
	    !in_array($img_info[2], array_keys($supported_image_types))) {
4211
		return false;
4212
	} else {
4213
		return $img_info[2];
4214
	}
4215
}
4216

    
4217
function get_lagg_ports ($laggport) {
4218
	$laggp = array();
4219
	foreach ($laggport as $lgp) {
4220
		list($lpname, $lpinfo) = explode(" ", $lgp);
4221
		preg_match('~<(.+)>~', $lpinfo, $lgportmode);
4222
		if ($lgportmode[1]) {
4223
			$laggp[] = $lpname . " (" . $lgportmode[1] . ")";
4224
		} else {
4225
			$laggp[] = $lpname;
4226
		}
4227
	}
4228
	if ($laggp) {
4229
		return implode(", ", $laggp);
4230
	} else {
4231
		return false;
4232
	}
4233
}
4234

    
4235
function cisco_to_cidr($addr) {
4236
	if (!is_ipaddr($addr)) {
4237
		throw new Exception('Value is not in dotted quad notation.');
4238
	}
4239

    
4240
	$mask = decbin(~ip2long($addr));
4241
	$mask = substr($mask, -32);
4242
	$k = 0;
4243
	for ($i = 0; $i <= 32; $i++) {
4244
		$k += intval($mask[$i]);
4245
	}
4246
	return $k;
4247
}
4248

    
4249
function cisco_extract_index($prule) {
4250
	$index = explode("#", $prule);
4251
	if (is_numeric($index[1])) {
4252
		return intval($index[1]);
4253
	} else {
4254
		syslog(LOG_WARNING, "Error parsing rule {$prule}: Could not extract index");
4255
	}
4256
	return -1;;
4257
}
4258

    
4259
function parse_cisco_acl_rule($rule, $devname, $dir, $proto) {
4260
	$rule_orig = $rule;
4261
	$rule = explode(" ", $rule);
4262
	$tmprule = "";
4263
	$index = 0;
4264

    
4265
	if ($rule[$index] == "permit") {
4266
		$startrule = "pass {$dir} quick on {$devname} ";
4267
	} else if ($rule[$index] == "deny") {
4268
		$startrule = "block {$dir} quick on {$devname} ";
4269
	} else {
4270
		return;
4271
	}
4272

    
4273
	$index++;
4274

    
4275
	switch ($rule[$index]) {
4276
		case "ip":
4277
			break;
4278
		case "icmp":
4279
			$icmp = ($proto == "inet") ? "icmp" : "ipv6-icmp";
4280
			$tmprule .= "proto {$icmp} ";
4281
			break;
4282
		case "tcp":
4283
		case "udp":
4284
			$tmprule .= "proto {$rule[$index]} ";
4285
			break;
4286
		default:
4287
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid protocol.");
4288
			return;
4289
	}
4290
	$index++;
4291

    
4292
	/* Source */
4293
	if (trim($rule[$index]) == "host") {
4294
		$index++;
4295
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
4296
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
4297
			if ($GLOBALS['attributes']['framed_ip']) {
4298
				$tmprule .= "from {$GLOBALS['attributes']['framed_ip']} ";
4299
			} else {
4300
				$tmprule .= "from {$rule[$index]} ";
4301
			}
4302
			$index++;
4303
		} else {
4304
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source host '{$rule[$index]}'.");
4305
			return;
4306
		}
4307
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
4308
		$tmprule .= "from {$rule[$index]} ";
4309
		$index++;
4310
	} elseif (trim($rule[$index]) == "any") {
4311
		$tmprule .= "from any ";
4312
		$index++;
4313
	} else {
4314
		$network = $rule[$index];
4315
		$netmask = $rule[++$index];
4316

    
4317
		if (is_ipaddrv4($network) && ($proto == "inet")) {
4318
			try {
4319
				$netmask = cisco_to_cidr($netmask);
4320
			} catch(Exception $e) {
4321
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source netmask '$netmask' (" . $e->getMessage() . ").");
4322
				return;
4323
			}
4324
			$tmprule .= "from {$network}/{$netmask} ";
4325
		} else {
4326
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source network '$network'.");
4327
			return;
4328
		}
4329

    
4330
		$index++;
4331
	}
4332

    
4333
	/* Source Operator */
4334
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
4335
		switch(trim($rule[$index])) {
4336
			case "lt":
4337
				$operator = "<";
4338
				break;
4339
			case "gt":
4340
				$operator = ">";
4341
				break;
4342
			case "eq":
4343
				$operator = "=";
4344
				break;
4345
			case "neq":
4346
				$operator = "!=";
4347
				break;
4348
		}
4349

    
4350
		$port = $rule[++$index];
4351
		if (is_port($port)) {
4352
			$tmprule .= "port {$operator} {$port} ";
4353
		} else {
4354
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source port: '$port' not a numeric value between 0 and 65535.");
4355
			return;
4356
		}
4357
		$index++;
4358
	} else if (trim($rule[$index]) == "range") {
4359
		$port = array($rule[++$index], $rule[++$index]);
4360
		if (is_port($port[0]) && is_port($port[1])) {
4361
			$tmprule .= "port {$port[0]}:{$port[1]} ";
4362
		} else {
4363
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source ports: '$port[0]' & '$port[1]' one or both are not a numeric value between 0 and 65535.");
4364
			return;
4365
		}
4366
		$index++;
4367
	}
4368

    
4369
	/* Destination */
4370
	if (trim($rule[$index]) == "host") {
4371
		$index++;
4372
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
4373
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
4374
			$tmprule .= "to {$rule[$index]} ";
4375
			$index++;
4376
		} else {
4377
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination host '{$rule[$index]}'.");
4378
			return;
4379
		}
4380
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
4381
		$tmprule .= "to {$rule[$index]} ";
4382
		$index++;
4383
	} elseif (trim($rule[$index]) == "any") {
4384
		$tmprule .= "to any ";
4385
		$index++;
4386
	} else {
4387
		$network = $rule[$index];
4388
		$netmask = $rule[++$index];
4389

    
4390
		if (is_ipaddrv4($network) && ($proto == "inet")) {
4391
			try {
4392
				$netmask = cisco_to_cidr($netmask);
4393
			} catch(Exception $e) {
4394
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination netmask '$netmask' (" . $e->getMessage() . ").");
4395
				return;
4396
			}
4397
			$tmprule .= "to {$network}/{$netmask} ";
4398
		} else {
4399
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination network '$network'.");
4400
			return;
4401
		}
4402

    
4403
		$index++;
4404
	}
4405

    
4406
	/* Destination Operator */
4407
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
4408
		switch(trim($rule[$index])) {
4409
			case "lt":
4410
				$operator = "<";
4411
				break;
4412
			case "gt":
4413
				$operator = ">";
4414
				break;
4415
			case "eq":
4416
				$operator = "=";
4417
				break;
4418
			case "neq":
4419
				$operator = "!=";
4420
				break;
4421
		}
4422

    
4423
		$port = $rule[++$index];
4424
		if (is_port($port)) {
4425
			$tmprule .= "port {$operator} {$port} ";
4426
		} else {
4427
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination port: '$port' not a numeric value between 0 and 65535.");
4428
			return;
4429
		}
4430
		$index++;
4431
	} else if (trim($rule[$index]) == "range") {
4432
		$port = array($rule[++$index], $rule[++$index]);
4433
		if (is_port($port[0]) && is_port($port[1])) {
4434
			$tmprule .= "port {$port[0]}:{$port[1]} ";
4435
		} else {
4436
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination ports: '$port[0]' '$port[1]' one or both are not a numeric value between 0 and 65535.");
4437
			return;
4438
		}
4439
		$index++;
4440
	}
4441

    
4442
	$tmprule = $startrule . $proto . " " . $tmprule;
4443
	return $tmprule;
4444
}
4445

    
4446
function parse_cisco_acl($attribs, $dev) {
4447
	global $attributes;
4448

    
4449
	if (!is_array($attribs)) {
4450
		return "";
4451
	}
4452
	$finalrules = "";
4453
	if (is_array($attribs['ciscoavpair'])) {
4454
		$inrules = array('inet' => array(), 'inet6' => array());
4455
		$outrules = array('inet' => array(), 'inet6' => array());
4456
		foreach ($attribs['ciscoavpair'] as $avrules) {
4457
			$rule = explode("=", $avrules);
4458
			$dir = "";
4459
			if (strstr($rule[0], "inacl")) {
4460
				$dir = "in";
4461
			} else if (strstr($rule[0], "outacl")) {
4462
				$dir = "out";
4463
			} else if (strstr($rule[0], "dns-servers")) {
4464
				$attributes['dns-servers'] = explode(" ", $rule[1]);
4465
				continue;
4466
			} else if (strstr($rule[0], "route")) {
4467
				if (!is_array($attributes['routes'])) {
4468
					$attributes['routes'] = array();
4469
				}
4470
				$attributes['routes'][] = $rule[1];
4471
				continue;
4472
			}
4473
			$rindex = cisco_extract_index($rule[0]);
4474
			if ($rindex < 0) {
4475
				continue;
4476
			}
4477

    
4478
			if (strstr($rule[0], "ipv6")) {
4479
				$proto = "inet6";
4480
			} else {
4481
				$proto = "inet";
4482
			}
4483

    
4484
			$tmprule = parse_cisco_acl_rule($rule[1], $dev, $dir, $proto);
4485
			if (!empty($tmprule)) {
4486
				if ($dir == "in") {
4487
					$inrules[$proto][$rindex] = $tmprule;
4488
				} else if ($dir == "out") {
4489
					$outrules[$proto][$rindex] = $tmprule;
4490
				}
4491
			}
4492
		}
4493

    
4494

    
4495
		$state = "";
4496
		foreach (array('inet', 'inet6') as $ip) {
4497
			if (!empty($outrules[$ip])) {
4498
				$state = "no state";
4499
			}
4500
			ksort($inrules[$ip], SORT_NUMERIC);
4501
			foreach ($inrules[$ip] as $inrule) {
4502
				$finalrules .= "{$inrule} {$state}\n";
4503
			}
4504
			if (!empty($outrules[$ip])) {
4505
				ksort($outrules[$ip], SORT_NUMERIC);
4506
				foreach ($outrules[$ip] as $outrule) {
4507
					$finalrules .= "{$outrule} {$state}\n";
4508
				}
4509
			}
4510
		}
4511
	}
4512
	return $finalrules;
4513
}
4514

    
4515
function alias_idn_to_utf8($alias) {
4516
	if (is_alias($alias)) {
4517
		return $alias;
4518
	} else {
4519
		return idn_to_utf8($alias);
4520
	}
4521
}
4522

    
4523
function alias_idn_to_ascii($alias) {
4524
	if (is_alias($alias)) {
4525
		return $alias;
4526
	} else {
4527
		return idn_to_ascii($alias);
4528
	}
4529
}
4530

    
4531
// These functions were in guiconfig.inc but have been moved here so non GUI processes can use them
4532
function address_to_pconfig($adr, &$padr, &$pmask, &$pnot, &$pbeginport, &$pendport) {
4533
	if (isset($adr['any'])) {
4534
		$padr = "any";
4535
	} else if ($adr['network']) {
4536
		$padr = $adr['network'];
4537
	} else if ($adr['address']) {
4538
		list($padr, $pmask) = explode("/", $adr['address']);
4539
		if (!$pmask) {
4540
			if (is_ipaddrv6($padr)) {
4541
				$pmask = 128;
4542
			} else {
4543
				$pmask = 32;
4544
			}
4545
		}
4546
	}
4547

    
4548
	if (isset($adr['not'])) {
4549
		$pnot = 1;
4550
	} else {
4551
		$pnot = 0;
4552
	}
4553

    
4554
	if ($adr['port']) {
4555
		list($pbeginport, $pendport) = explode("-", $adr['port']);
4556
		if (!$pendport) {
4557
			$pendport = $pbeginport;
4558
		}
4559
	} else if (!is_alias($pbeginport) && !is_alias($pendport)) {
4560
		$pbeginport = "any";
4561
		$pendport = "any";
4562
	}
4563
}
4564

    
4565
// does not support specialnet VIPs
4566
function pconfig_to_address(&$adr, $padr, $pmask, $pnot = false, $pbeginport = 0, $pendport = 0, $addmask = false, $specialnet_flags = []) {
4567
	$adr = array();
4568

    
4569
	if ($padr == "any") {
4570
		$adr['any'] = true;
4571
	} else if (get_specialnet($padr, $specialnet_flags)) {
4572
		if ($addmask) {
4573
			$padr .= "/" . $pmask;
4574
		}
4575
		$adr['network'] = $padr;
4576
	} else {
4577
		$adr['address'] = $padr;
4578
		if (is_ipaddrv6($padr)) {
4579
			if ($pmask != 128) {
4580
				$adr['address'] .= "/" . $pmask;
4581
			}
4582
		} else {
4583
			if ($pmask != 32) {
4584
				$adr['address'] .= "/" . $pmask;
4585
			}
4586
		}
4587
	}
4588

    
4589
	if ($pnot) {
4590
		$adr['not'] = true;
4591
	} else {
4592
		unset($adr['not']);
4593
	}
4594

    
4595
	if (($pbeginport != 0) && ($pbeginport != "any")) {
4596
		if ($pbeginport != $pendport) {
4597
			$adr['port'] = $pbeginport . "-" . $pendport;
4598
		} else {
4599
			$adr['port'] = $pbeginport;
4600
		}
4601
	}
4602

    
4603
	/*
4604
	 * If the port is still unset, then it must not be numeric, but could
4605
	 * be an alias or a well-known/registered service.
4606
	 * See https://redmine.pfsense.org/issues/8410
4607
	 */
4608
	if (!isset($adr['port']) && is_port_or_alias($pbeginport)) {
4609
		$adr['port'] = $pbeginport;
4610
	}
4611
}
4612

    
4613
function get_specialnet(?string $net = '', array $flags = [], ?string $if = ''):bool|array {
4614
	if ($net === null || $if === null) {
4615
		return false;
4616
	}
4617
	$checkpermission = false;
4618
	$allnetflags = [SPECIALNET_ANY, SPECIALNET_SELF, SPECIALNET_CLIENTS,
4619
	                SPECIALNET_IFADDR, SPECIALNET_IFSUB, SPECIALNET_IFNET,
4620
	                SPECIALNET_GROUP, SPECIALNET_VIPS];
4621

    
4622
	// Special flags
4623
	if (in_array(SPECIALNET_IFADDR4, $flags)) {
4624
		$allnetflags[] = SPECIALNET_IFADDR4;
4625
	}
4626
	if (in_array(SPECIALNET_IFADDR6, $flags)) {
4627
		$allnetflags[] = SPECIALNET_IFADDR6;
4628
	}
4629
	if (in_array(SPECIALNET_VIPALIAS, $flags)) {
4630
		$allnetflags[] = SPECIALNET_VIPALIAS;
4631
	}
4632

    
4633
	// Specialnets to exclude
4634
	$exceptions = [
4635
		// Interface group exceptions
4636
		'ifgroups' => array_flip([
4637
			'WireGuard', // WireGuard group does not populate members in the config
4638
			'Tailscale', // Tailscale group does not populate members in the config
4639
		]),
4640
	];
4641

    
4642
	if (empty($flags)) {
4643
		$flags = $allnetflags;
4644
	} else {
4645
		if (in_array(SPECIALNET_CHECKPERM, $flags)) {
4646
			$checkpermission = true;
4647
		}
4648
		if (in_array(SPECIALNET_EXCLUDE, $flags)) {
4649
			$flags = array_diff($allnetflags, $flags);
4650
		}
4651
	}
4652
	$specialnet = [];
4653
	if (in_array(SPECIALNET_IFADDR, $flags) || in_array(SPECIALNET_IFSUB, $flags) || in_array(SPECIALNET_IFNET, $flags) ||
4654
	    in_array(SPECIALNET_IFADDR4, $flags) || in_array(SPECIALNET_IFADDR6, $flags)) {
4655
		$ifmacros = get_configured_interface_with_descr();
4656
	}
4657
	foreach ($flags as $include) {
4658
		switch ($include) {
4659
			case SPECIALNET_NONE:
4660
				$specialnet = array_merge($specialnet, ['none' => gettext('None')]);
4661
				break;
4662
			case SPECIALNET_ANY:
4663
				$specialnet = array_merge($specialnet, ['any' => gettext('Any')]);
4664
				break;
4665
			case SPECIALNET_COMPAT_ADDR:
4666
			case SPECIALNET_ADDR:
4667
				if ($include == SPECIALNET_COMPAT_ADDR) {
4668
					$ifname = 'single';
4669
				} else {
4670
					$ifname = 'address';
4671
				}
4672
				$specialnet = array_merge($specialnet, [$ifname => gettext('Address')]);
4673
				break;
4674
			case SPECIALNET_COMPAT_ADDRAL:
4675
			case SPECIALNET_ADDRAL:
4676
				if ($include == SPECIALNET_COMPAT_ADDRAL) {
4677
					$ifname = 'single';
4678
				} else {
4679
					$ifname = 'address';
4680
				}
4681
				$specialnet = array_merge($specialnet, [$ifname => gettext('Address or Alias')]);
4682
				break;
4683
			case SPECIALNET_NET:
4684
			case SPECIALNET_NETAL:
4685
				if ($include == SPECIALNET_NET) {
4686
					$ifdescr = 'Network';
4687
				} else {
4688
					$ifdescr = 'Network or Alias';
4689
				}
4690
				$specialnet = array_merge($specialnet, ['network' => gettext($ifdescr)]);
4691
				break;
4692
			case SPECIALNET_SELF:
4693
				$specialnet = array_merge($specialnet, ['(self)' => gettext('This Firewall (self)')]);
4694
				break;
4695
			case SPECIALNET_CLIENTS:
4696
				if ($checkpermission) {
4697
					$ifallowed = [];
4698
					if (have_ruleint_access("pptp")) {
4699
						$ifallowed['pptp'] = gettext('PPTP clients');
4700
					}
4701
					if (have_ruleint_access("pppoe")) {
4702
						$ifallowed['pppoe'] = gettext('PPPoE clients');
4703
					}
4704
					if (have_ruleint_access("l2tp")) {
4705
						$ifallowed['l2tp'] = gettext('L2TP clients');
4706
					}
4707
					$specialnet = array_merge($specialnet, $ifallowed);
4708
				} else {
4709
					$specialnet = array_merge($specialnet, [
4710
					                          'pptp' => gettext('PPTP clients'),
4711
					                          'pppoe' => gettext('PPPoE clients'),
4712
					                          'l2tp' => gettext('L2TP clients')]);
4713
				}
4714
				break;
4715
			case SPECIALNET_IFADDR4:
4716
			case SPECIALNET_IFADDR6:
4717
				foreach ($ifmacros as $ifname => $ifdescr) {
4718
					// Only add macros for allowed interfaces when checking permissions
4719
					if ($checkpermission && !have_ruleint_access($ifname)) {
4720
						continue;
4721
					}
4722
					// If an interface is specified, only add interface macros for that interface
4723
					if (!empty($if) && ($if != $ifname)) {
4724
						continue;
4725
					}
4726

    
4727
					$ifname_suffix = 'ip' . (($include == SPECIALNET_IFADDR4) ? '4' : '6');
4728
					$ifdescr_suffix = ' address ' . (($include == SPECIALNET_IFADDR4) ? '(IPv4)' : '(IPv6)');
4729
					$specialnet[$ifname . $ifname_suffix] = $ifdescr . $ifdescr_suffix;
4730
				}
4731
				break;
4732
			case SPECIALNET_IFADDR:
4733
			case SPECIALNET_IFSUB:
4734
			case SPECIALNET_IFNET:
4735
				if ($include == SPECIALNET_IFADDR) {
4736
					$ifname_suffix = 'ip';
4737
					$ifdescr_suffix = ' address';
4738
				} elseif ($include == SPECIALNET_IFSUB) {
4739
					$ifname_suffix = '';
4740
					$ifdescr_suffix = ' subnet';
4741
				} else {
4742
					$ifname_suffix = '';
4743
					$ifdescr_suffix = ' subnets';
4744
				}
4745
				foreach ($ifmacros as $ifname => $ifdescr) {
4746
					// Only add macros for allowed interfaces when checking permissions
4747
					if ($checkpermission && !have_ruleint_access($ifname)) {
4748
						continue;
4749
					}
4750
					// If an interface is specified, only add interface macros for that interface
4751
					if (!empty($if)) {
4752
						if ($if != $ifname) {
4753
							continue;
4754
						} else {
4755
							$specialnet[$ifname . $ifname_suffix] = $ifdescr . $ifdescr_suffix;
4756
							break;
4757
						}
4758
					} else {
4759
						$specialnet[$ifname . $ifname_suffix] = $ifdescr . $ifdescr_suffix;
4760
					}
4761
				}
4762
				break;
4763
			case SPECIALNET_GROUP:
4764
				foreach (config_get_path('ifgroups/ifgroupentry', []) as $ifgen) {
4765
					if (isset($exceptions['ifgroups'][$ifgen['ifname']])) {
4766
						continue;
4767
					}
4768
					$ifgenmembers = explode(' ', $ifgen['members']);
4769
					foreach ($ifgenmembers as $ifgmember) {
4770
						// Skip interface groups with members which are not allowed
4771
						if ($checkpermission && !have_ruleint_access($ifgmember)) {
4772
							continue 2;
4773
						}
4774
						// Skip interface groups which do not include the specified interface
4775
						if (!empty($if) && $if != $ifgmember &&
4776
						    $ifgmember == $ifgenmembers[array_key_last($ifgenmembers)]) {
4777
							continue 2;
4778
						}
4779
					}
4780
					$specialnet[$ifgen['ifname']] = $ifgen['ifname'] . ' networks';
4781
				}
4782
				break;
4783
			case SPECIALNET_VIPS:
4784
				$tmp_vips = [];
4785
				foreach (config_get_path('virtualip/vip', []) as $vip) {
4786
					// Only add VIPs for allowed interfaces when checking permissions
4787
					if ($checkpermission && !empty($vip['interface']) && !have_ruleint_access($vip['interface'])) {
4788
						continue;
4789
					}
4790
					$tmp_vips[$vip['subnet'] . '/' . $vip['subnet_bits']] = $vip;
4791
				}
4792
				asort($tmp_vips, SORT_NATURAL);
4793
				foreach ($tmp_vips as $vipsn => $sn) {
4794
					if (($sn['mode'] == "proxyarp" || $sn['mode'] == "other") && $sn['type'] == "network") {
4795
						$specialnet[$vipsn] = 'Subnet: ' . $sn['subnet'] . '/' . $sn['subnet_bits'] . ' (' . $sn['descr'] . ')';
4796
						// Skip expanding IPv4 VIPs with the option, as well as any IPv6 network
4797
						if (isset($sn['noexpand']) || strstr($sn['subnet'], ':' !== false)) {
4798
							continue;
4799
						}
4800
						$start = ip2long32(gen_subnet($sn['subnet'], $sn['subnet_bits']));
4801
						$end = ip2long32(gen_subnet_max($sn['subnet'], $sn['subnet_bits']));
4802
						$len = $end - $start;
4803
						for ($i = 0; $i <= $len; $i++) {
4804
							$snip = ip2long32($start+$i);
4805

    
4806
							$specialnet[$snip] = $snip . ' (' . $sn['descr'] . ')';
4807
						}
4808
					} else {
4809
						$specialnet[$sn['subnet']] = $sn['subnet'] . ' (' . $sn['descr'] . ')';
4810
					}
4811
				}
4812
				break;
4813
			case SPECIALNET_VIPALIAS:
4814
				$tmp_vips = [];
4815
				foreach (config_get_path('virtualip/vip', []) as $vip) {
4816
					if (empty($vip['interface'])) {
4817
						continue;
4818
					}
4819
					// Only add IP alias VIPs without a parent; i.e. addresses that remain on the interface
4820
					if (($vip['mode'] != 'ipalias') || str_starts_with($vip['interface'], '_vip')) {
4821
						continue;
4822
					}
4823
					// Only add VIPs for allowed interfaces when checking permissions
4824
					if ($checkpermission && !have_ruleint_access($vip['interface'])) {
4825
						continue;
4826
					}
4827
					$tmp_vips[$vip['subnet'] . '/' . $vip['subnet_bits']] = $vip;
4828
				}
4829
				asort($tmp_vips, SORT_NATURAL);
4830
				foreach ($tmp_vips as $vipsn => $sn) {
4831
					$specialnet[$sn['subnet']] = $sn['subnet'] . ' (' . $sn['descr'] . ')';
4832
				}
4833
				break;
4834
			default:
4835
				break;
4836
		}
4837
	}
4838

    
4839
	if (empty($net)) {
4840
		return $specialnet;
4841
	} elseif (array_key_exists($net, $specialnet)) {
4842
		return true;
4843
	} else {
4844
		return false;
4845
	}
4846
}
4847

    
4848
function get_specialnet_interface($specialnet, $address_family = null) {
4849
	if (empty($specialnet) || !is_string($specialnet)) {
4850
		return false;
4851
	}
4852

    
4853
	$matches = [];
4854
	preg_match('/^(?:wan|lan|opt[[:digit:]]+)/', $specialnet, $matches);
4855
	if (isset($matches[0])) {
4856
		$interface = match ($address_family) {
4857
			AF_INET => "{$matches[0]}ip4",
4858
			AF_INET6 => "{$matches[0]}ip6",
4859
			default => $matches[0]
4860
		};
4861
		if (!isset($address_family) || ($specialnet == $interface)) {
4862
			return $matches[0];
4863
		}
4864
	}
4865

    
4866
	return false;
4867
}
4868

    
4869
function get_specialnet_address($specialnet) {
4870
	if (empty($specialnet) || !is_string($specialnet)) {
4871
		return false;
4872
	}
4873

    
4874
	if (!get_specialnet($specialnet, [SPECIALNET_IFADDR4, SPECIALNET_IFADDR6, SPECIALNET_VIPS, SPECIALNET_VIPALIAS])) {
4875
		return false;
4876
	}
4877

    
4878
	if (is_ipaddr($specialnet)) {
4879
		return $specialnet;
4880
	}
4881

    
4882
	$interface = get_specialnet_interface($specialnet);
4883
	if (empty($interface)) {
4884
		return false;
4885
	}
4886
	$interface = convert_friendly_interface_to_real_interface_name($interface);
4887
	if (empty($interface)) {
4888
		return false;
4889
	}
4890
	$interface_addresses = get_interface_addresses($interface);
4891
	if (!isset($interface_addresses)) {
4892
		return false;
4893
	}
4894

    
4895
	if (str_ends_with($specialnet, 'ip4')) {
4896
		if (isset($interface_addresses['ipaddr']) && is_ipaddrv4($interface_addresses['ipaddr'])) {
4897
			return $interface_addresses['ipaddr'];;
4898
		}
4899
	} elseif (str_ends_with($specialnet, 'ip6')) {
4900
		if (isset($interface_addresses['ipaddr6']) && is_ipaddrv6($interface_addresses['ipaddr6'])) {
4901
			return $interface_addresses['ipaddr6'];;
4902
		}
4903
	}
4904

    
4905
	return false;
4906
}
4907

    
4908
function is_host_address($address) {
4909
	return (is_host_address4($address) || is_host_address6($address) || is_host_address_fqdn($address));
4910
}
4911

    
4912
function is_host_address_fqdn($address) {
4913
	if (empty($address) || !is_string($address)) {
4914
		return false;
4915
	}
4916

    
4917
	if (str_contains($address, ':')) {
4918
		// Check Domain:Port address
4919
		list($name, $port) = explode(':', $address);
4920
		if (is_domain($name) && is_port($port, false)) {
4921
			return true;
4922
		}
4923
	} else {
4924
		// Check Domain address
4925
		if (is_domain($address)) {
4926
			return true;
4927
		}
4928
	}
4929

    
4930
	return false;
4931
}
4932

    
4933
function is_host_address4($address) {
4934
	if (empty($address) || !is_string($address)) {
4935
		return false;
4936
	}
4937

    
4938
	if (str_contains($address, ':')) {
4939
		// Check IPv4:Port address
4940
		list($ip, $port) = explode(':', $address);
4941
		if (is_ipaddrv4($ip) && is_port($port, false)) {
4942
			return true;
4943
		}
4944
	} else {
4945
		// Check IPv4 address
4946
		if (is_ipaddrv4($address)) {
4947
			return true;
4948
		}
4949
	}
4950

    
4951
	return false;
4952
}
4953

    
4954
function is_host_address6($address) {
4955
	if (empty($address) || !is_string($address)) {
4956
		return false;
4957
	}
4958

    
4959
	if (str_starts_with($address, '[')) {
4960
		// Check [IPv6]:Port address
4961
		$matches = [];
4962
		preg_match('/^\[((?:[[:xdigit:]]|:)+)\]:([[:digit:]]+)/', $address, $matches);
4963
		if (isset($matches[1]) && isset($matches[2]) &&
4964
		    is_ipaddrv6($matches[1]) && is_port($matches[2], false)) {
4965
			return true;
4966
		}
4967
	} else {
4968
		// Check IPv6 address
4969
		if (is_ipaddrv6($address)) {
4970
			return true;
4971
		}
4972
	}
4973

    
4974
	return false;
4975
}
4976

    
4977
function is_interface_ipaddr($interface) {
4978
	if (!empty(config_get_path("interfaces/{$interface}/ipaddr"))) {
4979
		return true;
4980
	}
4981
	return false;
4982
}
4983

    
4984
function is_interface_ipaddrv6($interface) {
4985
	if (!empty(config_get_path("interfaces/{$interface}/ipaddrv6"))) {
4986
		return true;
4987
	}
4988
	return false;
4989
}
4990

    
4991
function escape_filter_regex($filtertext) {
4992
	/* If the caller (user) has not already put a backslash before a slash, to escape it in the regex, */
4993
	/* then this will do it. Take out any "\/" already there, then turn all ordinary "/" into "\/".    */
4994
	return str_replace('/', '\/', str_replace('\/', '/', $filtertext));
4995
}
4996

    
4997
/*
4998
 * Check if a given pattern has the same number of two different unescaped
4999
 * characters.
5000
 * For example, it can ensure a pattern has balanced sets of parentheses,
5001
 * braces, and brackets.
5002
 */
5003
function is_pattern_balanced_char($pattern, $open, $close) {
5004
	/* First remove escaped versions */
5005
	$pattern = str_replace('\\' . $open, '', $pattern);
5006
	$pattern = str_replace('\\' . $close, '', $pattern);
5007
	/* Check if the counts of both characters match in the target pattern */
5008
	return (substr_count($pattern, $open) == substr_count($pattern, $close));
5009
}
5010

    
5011
/*
5012
 * Check if a pattern contains balanced sets of parentheses, braces, and
5013
 * brackets.
5014
 */
5015
function is_pattern_balanced($pattern) {
5016
	if (is_pattern_balanced_char($pattern, '(', ')') &&
5017
	    is_pattern_balanced_char($pattern, '{', '}') &&
5018
	    is_pattern_balanced_char($pattern, '[', ']')) {
5019
		/* Balanced if all are true */
5020
		return true;
5021
	}
5022
	return false;
5023
}
5024

    
5025
function cleanup_regex_pattern($filtertext) {
5026
	/* Cleanup filter to prevent backreferences. */
5027
	$filtertext = escape_filter_regex($filtertext);
5028

    
5029
	/* Remove \<digit>+ backreferences
5030
	 * To match \ it must be escaped as \\\\ in PHP for preg_replace() */
5031
	$filtertext = preg_replace('/\\\\\\d+/', '', $filtertext);
5032

    
5033
	/* Check for unbalanced parentheses, braces, and brackets which
5034
	 * may be an error or attempt to circumvent protections.
5035
	 * Also discard any pattern that attempts problematic duplication
5036
	 * methods. */
5037
	if (!is_pattern_balanced($filtertext) ||
5038
	    (substr_count($filtertext, ')*') > 0) ||
5039
	    (substr_count($filtertext, ')+') > 0) ||
5040
	    (substr_count($filtertext, '{') > 0)) {
5041
		return '';
5042
	}
5043

    
5044
	return $filtertext;
5045
}
5046

    
5047
function ip6_to_asn1($addr) {
5048
	/* IPv6 MIB uses an OCTET STRING of length 16 to represent
5049
	 * 128-bit IPv6 address in network byte order.
5050
	 * see https://datatracker.ietf.org/doc/html/rfc2465#section-3
5051
	 * i.e. fc00:3::4 = 252.0.0.3.0.0.0.0.0.0.0.0.0.0.0.4
5052
	 */
5053

    
5054
	if (!is_ipaddrv6($addr)) {
5055
		return false;
5056
	}
5057
	$ipv6str = "";
5058
	$octstr = "";
5059
	foreach (explode(':', Net_IPv6::uncompress($addr)) as $v) {
5060
		$ipv6str .= str_pad($v, 4, '0', STR_PAD_LEFT);
5061
	}
5062
	foreach (str_split($ipv6str, 2) as $v) {
5063
		$octstr .= base_convert($v, 16, 10) . '.';
5064
	}
5065

    
5066
	return $octstr;
5067
}
5068

    
5069
function interfaces_interrupts() {
5070
	exec("/usr/bin/vmstat -i --libxo json", $rawdata, $rc);
5071
	$interrupts = array();
5072
	if ($rc == 0) {
5073
		$vnstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
5074
		$interruptarr = $vnstatarr['interrupt-statistics']['interrupt'];
5075

    
5076
		foreach ($interruptarr as $int){
5077
			preg_match("/irq\d+: ([a-z0-9]+)/", $int['name'], $matches);
5078
			$name = $matches[1];
5079
			if (array_key_exists($name, $interrupts)) {
5080
				/* interface with multiple queues */
5081
				$interrupts[$name]['total'] += $int['total'];
5082
				$interrupts[$name]['rate'] += $int['rate'];
5083
			} else {
5084
				$interrupts[$name]['total'] = $int['total'];
5085
				$interrupts[$name]['rate'] = $int['rate'];
5086
			}
5087
		}
5088
	}
5089

    
5090
	return $interrupts;
5091
}
5092

    
5093
function dummynet_load_module($max_qlimit) {
5094
	if (!is_module_loaded("dummynet.ko")) {
5095
		mute_kernel_msgs();
5096
		mwexec("/sbin/kldload dummynet");
5097
		unmute_kernel_msgs();
5098
	}
5099
	$sysctls = (array(
5100
			"net.inet.ip.dummynet.io_fast" => "1",
5101
			"net.inet.ip.dummynet.hash_size" => "256",
5102
			"net.inet.ip.dummynet.pipe_slot_limit" => $max_qlimit
5103
	));
5104
	foreach (config_get_path('sysctl/item', []) as $item) {
5105
		if (preg_match('/net\.inet\.ip\.dummynet\./', $item['tunable'])) {
5106
			$sysctls[$item['tunable']] = $item['value'];
5107
		}
5108
	}
5109
	set_sysctl($sysctls);
5110
}
5111

    
5112
function get_interface_vip_ips($interface) {
5113
	$vipips = '';
5114

    
5115
	foreach (config_get_path('virtualip/vip', []) as $vip) {
5116
		if (($vip['interface'] == $interface) &&
5117
		    (($vip['mode'] == "carp") || ($vip['mode'] == "ipalias"))) {
5118
			$vipips .= $vip['subnet'] . ' ';
5119
		}
5120
	}
5121
	return $vipips;
5122
}
5123

    
5124
/**
5125
 * Checks for DNS availability.
5126
 * 
5127
 * @param int $protocol Address family to check with [AF_INET|AF_INET6]
5128
 * 
5129
 * @return bool true if DNS is available, false otherwise
5130
 */
5131
function get_dnsavailable(?int $protocol = null): bool {
5132
	switch ($protocol) {
5133
		case AF_INET:
5134
			$proto = '-4';
5135
			break;
5136
		case AF_INET6:
5137
			$proto = '-6';
5138
			break;
5139
		default:
5140
			$proto = '';
5141
			break;
5142
	}
5143

    
5144
	// The domain doesn't matter, only that there's an answer
5145
	exec("/usr/bin/host -W 1 {$proto} netgate.com.", $null, $result_code);
5146
	if ($result_code === 0) {
5147
		// DNS server responded
5148
		return true;
5149
	}
5150

    
5151
	// no answer
5152
	return false;
5153
}
5154

    
5155
/**
5156
 * Verifies the existence of a DNS A/AAAA/SRV/PTR record for an address.
5157
 * 
5158
 * @param string $address URL, name, or IPv4/6 address to check.
5159
 * 
5160
 * @return string The address can be resolved. If $address is an IP address,
5161
 *                return the reverse lookup name, otherwise return the
5162
 *                unmodified input if it's a host/domain name.
5163
 * @return false Cannot resolve - no records found.
5164
 */
5165
function resolve_address(?string $address): string|false {
5166
	if (empty($address)) {
5167
		return false;
5168
	}
5169

    
5170
	// Try once per server and timeout after 3 seconds per attempt - see resolver(5)
5171
	putenv("RES_OPTIONS=timeout:3 attempts:1");
5172
	if (filter_var($address, FILTER_VALIDATE_IP)) {
5173
		$result = gethostbyaddr($address);
5174
		if (empty($result) || ($result == $address)) {
5175
			$result = null;
5176
		}
5177
	} elseif (filter_var($address, FILTER_VALIDATE_URL)) {
5178
		if (!empty(dns_get_record(parse_url($address, PHP_URL_HOST), DNS_A | DNS_AAAA | DNS_SRV))) {
5179
			$result = $address;
5180
		}
5181
	} elseif (filter_var($address, FILTER_VALIDATE_DOMAIN)) {
5182
		if (!empty(dns_get_record($address, DNS_A | DNS_AAAA | DNS_SRV))) {
5183
			$result = $address;
5184
		}
5185
	}
5186
	putenv('RES_OPTIONS');
5187

    
5188
	if (!empty($result)) {
5189
		return $result;
5190
	}
5191

    
5192
	return false;
5193
}
5194

    
5195
function compare_files($f1, $f2) {
5196
	/* One file doesn't exist or files are different sizes, so they can't
5197
	 * be the same */
5198
	if (!file_exists($f1) ||
5199
	    !file_exists($f2) ||
5200
	    (filesize($f1) != filesize($f2))) {
5201
		return false;
5202
	}
5203
	/* Return result of comparing hashes. */
5204
	return (hash_file('sha256', $f1) == hash_file('sha256', $f2));
5205
}
5206

    
5207
/**
5208
 * Helper function for unserialize() with error handling.
5209
 * 
5210
 * @param ?string $path    Data string to unserialize
5211
 * @param mixed  $default Value to return in case of failure
5212
 * @param ?array  $options Options to pass to unserialize()
5213
 * 
5214
 * @return mixed $data The unserialized data
5215
 */
5216
function unserialize_data(?string $path, mixed $default = null, ?array $options = []):mixed {
5217
	if (empty($path) || !isset($options)) {
5218
		return $default;
5219
	}
5220

    
5221
	$data = @unserialize($path, $options);
5222

    
5223
	// check if the string was not unserialized
5224
	if (($data === false) && ($data == serialize(false))) {
5225
		return $default;
5226
	}
5227

    
5228
	return $data;
5229
}
5230

    
5231
/* Get an array of active widgets and metadata from user settings */
5232
function get_active_widgets($user_settings) {
5233
	$widgets = [];
5234

    
5235
	/* Break up the sequence string into an array of widget definitions */
5236
	$widget_sep = ',';
5237
	$widget_seq_array = explode($widget_sep, rtrim($user_settings['widgets']['sequence'], $widget_sep));
5238

    
5239
	foreach ($widget_seq_array as $widget_seq_data) {
5240
		/* Break each widget definition into its component values */
5241
		[$name, $column, $display, $instance] = explode(':', $widget_seq_data);
5242
		if (is_null($instance)) {
5243
			$instance = 0;
5244
		}
5245
		$widgets[] = [
5246
			'name'     => $name,
5247
			'column'   => $column,
5248
			'display'  => $display,
5249
			'instance' => $instance
5250
		];
5251
	}
5252
	return $widgets;
5253
}
5254

    
5255
/* Test the validity of a given widget key based on user settings. */
5256
function is_valid_widgetkey($widgetkey, $user_settings, $widgetfile = null) {
5257
	/* Proper form of a widgetkey is <widget-name>-<instance-id>
5258
	 * Where:
5259
	 *   widget-name : Name of an active widget, which should be found in
5260
	 *                 the current sequence list.
5261
	 *   instance-id : An integer 0 or higher identifying a widget instance
5262
	 *
5263
	 * Additionally, for a widget to be valid in this context it must also
5264
	 * be present on the current Dashboard layout.
5265
	 */
5266

    
5267
	/* Break the given widgetkey into its component parts */
5268
	[$wname, $wid] = explode('-', $widgetkey, 2);
5269

    
5270
	/* Test for basic validity conditions */
5271
	if (empty($wname) ||
5272
	    !is_numericint($wid) ||
5273
	    empty($user_settings)) {
5274
		return false;
5275
	}
5276

    
5277
	/* Check if this widget also matches a specific widget name */
5278
	if (!empty($widgetfile) &&
5279
	    ($wname != basename($widgetfile, '.widget.php'))) {
5280
		return false;
5281
	}
5282

    
5283
	/* Ensure the key is for a widget which is in the Dashboard
5284
	 * configuration. */
5285
	$widgets = get_active_widgets($user_settings);
5286
	foreach ($widgets as $widget) {
5287
		if (($widget['name'] == $wname) &&
5288
		    ($widget['instance'] == $wid)) {
5289
			return true;
5290
		}
5291
	}
5292
	return false;
5293
}
(54-54/61)