Project

General

Profile

Download (118 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-2023 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
//require_once('interfaces.inc');
28
require_once('Net/IPv6.php');
29
define('VIP_ALL', 1);
30
define('VIP_CARP', 2);
31
define('VIP_IPALIAS', 3);
32
// Special networks flags
33
define('SPECIALNET_NONE', 0);
34
define('SPECIALNET_ANY', 1);
35
define('SPECIALNET_COMPAT_ADDR', 22);
36
define('SPECIALNET_ADDR', 2);
37
define('SPECIALNET_COMPAT_ADDRAL', 33);
38
define('SPECIALNET_ADDRAL', 3);
39
define('SPECIALNET_NET', 4);
40
define('SPECIALNET_NETAL', 5);
41
define('SPECIALNET_SELF', 6);
42
define('SPECIALNET_CLIENTS', 7);
43
define('SPECIALNET_IFADDR', 8);
44
define('SPECIALNET_IFSUB', 9);
45
define('SPECIALNET_IFNET', 10);
46
define('SPECIALNET_GROUP', 11);
47
define('SPECIALNET_VIPS', 12);
48
define('SPECIALNET_CHECKPERM', 98);
49
define('SPECIALNET_EXCLUDE', 99);
50

    
51
/* kill a process by pid file */
52
function killbypid($pidfile, $waitfor = 0) {
53
	return sigkillbypid($pidfile, "TERM", $waitfor);
54
}
55

    
56
function isvalidpid($pidfile) {
57
	$output = "";
58
	if (file_exists($pidfile)) {
59
		exec("/bin/pgrep -qnF {$pidfile} 2>/dev/null", $output, $retval);
60
		return (intval($retval) == 0);
61
	}
62
	return false;
63
}
64

    
65
function is_process_running($process) {
66
	$output = "";
67
	if (!empty($process)) {
68
		exec("/bin/pgrep -anx " . escapeshellarg($process), $output, $retval);
69
		return (intval($retval) == 0);
70
	}
71
	return false;
72
}
73

    
74
function isvalidproc($proc) {
75
	return is_process_running($proc);
76
}
77

    
78
/* sigkill a process by pid file, and wait for it to terminate or remove the .pid file for $waitfor seconds */
79
/* return 1 for success and 0 for a failure */
80
function sigkillbypid($pidfile, $sig, $waitfor = 0) {
81
	if (isvalidpid($pidfile)) {
82
		$result = mwexec("/bin/pkill " . escapeshellarg("-{$sig}") .
83
		    " -F {$pidfile}", true);
84
		$waitcounter = $waitfor * 10;
85
		while(isvalidpid($pidfile) && $waitcounter > 0) {
86
			$waitcounter = $waitcounter - 1;
87
			usleep(100000);
88
		}
89
		return $result;
90
	}
91

    
92
	return 0;
93
}
94

    
95
/* kill a process by name */
96
function sigkillbyname($procname, $sig) {
97
	if (isvalidproc($procname)) {
98
		return mwexec("/usr/bin/killall " . escapeshellarg("-{$sig}") . " " . escapeshellarg($procname), true);
99
	}
100
}
101

    
102
/* kill a process by name */
103
function killbyname($procname) {
104
	if (isvalidproc($procname)) {
105
		mwexec("/usr/bin/killall " . escapeshellarg($procname));
106
	}
107
}
108

    
109
function is_subsystem_dirty($subsystem = "") {
110
	global $g;
111

    
112
	if ($subsystem == "") {
113
		return false;
114
	}
115

    
116
	if (file_exists("{$g['varrun_path']}/{$subsystem}.dirty")) {
117
		return true;
118
	}
119

    
120
	return false;
121
}
122

    
123
function mark_subsystem_dirty($subsystem = "") {
124
	global $g;
125

    
126
	if (!file_put_contents("{$g['varrun_path']}/{$subsystem}.dirty", "DIRTY")) {
127
		log_error(sprintf(gettext("WARNING: Could not mark subsystem: %s dirty"), $subsystem));
128
	}
129
}
130

    
131
function clear_subsystem_dirty($subsystem = "") {
132
	global $g;
133

    
134
	@unlink("{$g['varrun_path']}/{$subsystem}.dirty");
135
}
136

    
137
function clear_filter_subsystems_dirty() {
138
	clear_subsystem_dirty('aliases');
139
	clear_subsystem_dirty('filter');
140
	clear_subsystem_dirty('natconf');
141
	clear_subsystem_dirty('shaper');
142
}
143

    
144
/* lock configuration file */
145
function lock($lock, $op = LOCK_SH) {
146
	global $g;
147
	if (!$lock) {
148
		die(gettext("WARNING: A name must be given as parameter to lock() function."));
149
	}
150
	if (!file_exists("{$g['tmp_path']}/{$lock}.lock")) {
151
		@touch("{$g['tmp_path']}/{$lock}.lock");
152
		@chmod("{$g['tmp_path']}/{$lock}.lock", 0666);
153
	}
154
	if ($fp = fopen("{$g['tmp_path']}/{$lock}.lock", "w")) {
155
		if (flock($fp, $op)) {
156
			return $fp;
157
		} else {
158
			fclose($fp);
159
		}
160
	}
161
}
162

    
163
function try_lock($lock, $timeout = 5) {
164
	global $g;
165
	if (!$lock) {
166
		die(gettext("WARNING: A name must be given as parameter to try_lock() function."));
167
	}
168
	if (!file_exists("{$g['tmp_path']}/{$lock}.lock")) {
169
		@touch("{$g['tmp_path']}/{$lock}.lock");
170
		@chmod("{$g['tmp_path']}/{$lock}.lock", 0666);
171
	}
172
	if ($fp = fopen("{$g['tmp_path']}/{$lock}.lock", "w")) {
173
		$trycounter = 0;
174
		while (!flock($fp, LOCK_EX | LOCK_NB)) {
175
			if ($trycounter >= $timeout) {
176
				fclose($fp);
177
				return NULL;
178
			}
179
			sleep(1);
180
			$trycounter++;
181
		}
182

    
183
		return $fp;
184
	}
185

    
186
	return NULL;
187
}
188

    
189
/* unlock configuration file */
190
function unlock($cfglckkey = 0)
191
{
192
	if (!is_resource($cfglckkey))
193
		return;
194

    
195
	flock($cfglckkey, LOCK_UN);
196
	fclose($cfglckkey);
197
}
198

    
199
/* unlock forcefully configuration file */
200
function unlock_force($lock) {
201
	global $g;
202

    
203
	@unlink("{$g['tmp_path']}/{$lock}.lock");
204
}
205

    
206
function send_event($cmd) {
207
	global $g;
208

    
209
	if (!isset($g['event_address'])) {
210
		$g['event_address'] = "unix:///var/run/check_reload_status";
211
	}
212

    
213
	$try = 0;
214
	while ($try < 3) {
215
		$fd = @fsockopen(g_get('event_address'));
216
		if ($fd) {
217
			fwrite($fd, $cmd);
218
			$resp = fread($fd, 4096);
219
			if ($resp != "OK\n") {
220
				log_error("send_event: sent {$cmd} got {$resp}");
221
			}
222
			fclose($fd);
223
			$try = 3;
224
		} else if (!is_process_running("check_reload_status")) {
225
			mwexec_bg("/usr/bin/nice -n20 /usr/local/sbin/check_reload_status");
226
		}
227
		$try++;
228
	}
229
}
230

    
231
function send_multiple_events($cmds) {
232
	global $g;
233

    
234
	if (!isset($g['event_address'])) {
235
		$g['event_address'] = "unix:///var/run/check_reload_status";
236
	}
237

    
238
	if (!is_array($cmds)) {
239
		return;
240
	}
241

    
242
	$try = 0;
243
	while ($try < 3) {
244
		$fd = @fsockopen(g_get('event_address'));
245
		if ($fd) {
246
			foreach ($cmds as $cmd) {
247
				fwrite($fd, $cmd);
248
				$resp = fread($fd, 4096);
249
				if ($resp != "OK\n") {
250
					log_error("send_event: sent {$cmd} got {$resp}");
251
				}
252
			}
253
			fclose($fd);
254
			$try = 3;
255
		} else if (!is_process_running("check_reload_status")) {
256
			mwexec_bg("/usr/bin/nice -n20 /usr/local/sbin/check_reload_status");
257
		}
258
		$try++;
259
	}
260
}
261

    
262
/**
263
 * Test if a kernel module exists on disk in well-known locations
264
 *
265
 * Locations:
266
 *   > /boot/kernel
267
 *   > /bood/modules
268
 *
269
 * @param string $module_name		The name of the kernel module
270
 * @param string ...$add_dirs		Additional directories to search
271
 *
272
 * @return bool
273
 */
274
function is_module_available(string $module_name, string ...$add_dirs): bool
275
{
276
	/* bail if module is already loaded */
277
	if (is_module_loaded($module_name)) {
278
		return (true);
279
	}
280

    
281
	/* ensure the $module_name ends with .ko */
282
	if (!str_ends_with($module_name, '.ko')) {
283
		$module_name .= '.ko';
284
	}
285

    
286
	/* build list of well-known locations */
287
	$mod_dirs = [
288
		'/boot/kernel',
289
		'/boot/modules',
290
		...$add_dirs
291
	];
292

    
293
	/* filter list of well-known locations and cast to bool */
294
	return ((bool) array_filter($mod_dirs, function($mod_dir) use ($module_name) {
295
		return (file_exists($mod_dir . '/' . $module_name));
296
	}));
297
}
298

    
299
/**
300
 * Test if a kernel module is loaded
301
 *
302
 * @param string $module_name	The name of the kernel module
303
 *
304
 * @param bool
305
 */
306
function is_module_loaded(string $module_name): bool
307
{
308
	/* sanitize input */
309
	$module_name = escapeshellarg(rtrim($module_name, '.ko'));
310

    
311
	$base_cmd = ['/sbin/kldstat', '-q'];
312

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

    
317
	/* bailout early if we found it */
318
	if ($rc === 0) {
319
		return (true);
320
	}
321

    
322
	/* last pass test by file name */
323
	$cmd = implode(' ', [...$base_cmd, '-n', $module_name]);
324
	exec($cmd, $out, $rc);
325

    
326
	return ($rc === 0);
327
}
328

    
329
/* validate non-negative numeric string, or equivalent numeric variable */
330
function is_numericint($arg) {
331
	return (((is_int($arg) && $arg >= 0) || (is_string($arg) && strlen($arg) > 0 && ctype_digit(strval($arg)))) ? true : false);
332
}
333

    
334
/* Generate the (human readable) ipv4 or ipv6 subnet address (i.e., netmask, or subnet start IP)
335
   given an (human readable) ipv4 or ipv6 host address and subnet bit count */
336
function gen_subnet($ipaddr, $bits) {
337
	if (($sn = gen_subnetv6($ipaddr, $bits)) == '') {
338
		$sn = gen_subnetv4($ipaddr, $bits);  // try to avoid rechecking IPv4/v6
339
	}
340
	return $sn;
341
}
342

    
343
/* same as gen_subnet() but accepts IPv4 only */
344
function gen_subnetv4($ipaddr, $bits) {
345
	if (is_ipaddrv4($ipaddr) && is_numericint($bits) && $bits <= 32) {
346
		if ($bits == 0) {
347
			return '0.0.0.0';  // avoids <<32
348
		}
349
		return long2ip(ip2long($ipaddr) & ((0xFFFFFFFF << (32 - $bits)) & 0xFFFFFFFF));
350
	}
351
	return "";
352
}
353

    
354
/* same as gen_subnet() but accepts IPv6 only */
355
function gen_subnetv6($ipaddr, $bits) {
356
	if (is_ipaddrv6($ipaddr) && is_numericint($bits) && $bits <= 128) {
357
		return text_to_compressed_ip6(Net_IPv6::getNetmask($ipaddr, $bits));
358
	}
359
	return "";
360
}
361

    
362
/* Generate the (human readable) ipv4 or ipv6 subnet end address (i.e., highest address, end IP, or IPv4 broadcast address)
363
   given an (human readable) ipv4 or ipv6 host address and subnet bit count. */
364
function gen_subnet_max($ipaddr, $bits) {
365
	if (($sn = gen_subnetv6_max($ipaddr, $bits)) == '') {
366
		$sn = gen_subnetv4_max($ipaddr, $bits);  // try to avoid rechecking IPv4/v6
367
	}
368
	return $sn;
369
}
370

    
371
/* same as gen_subnet_max() but validates IPv4 only */
372
function gen_subnetv4_max($ipaddr, $bits) {
373
	if (is_ipaddrv4($ipaddr) && is_numericint($bits) && $bits <= 32) {
374
		if ($bits == 32) {
375
			return $ipaddr;
376
		}
377
		return long2ip32(ip2long($ipaddr) | (~gen_subnet_mask_long($bits) & 0xFFFFFFFF));
378
	}
379
	return "";
380
}
381

    
382
/* same as gen_subnet_max() but validates IPv6 only */
383
function gen_subnetv6_max($ipaddr, $bits) {
384
	if (is_ipaddrv6($ipaddr) && is_numericint($bits) && $bits <= 128) {
385
		$endip_bin = substr(ip6_to_bin($ipaddr), 0, $bits) . str_repeat('1', 128 - $bits);
386
		return bin_to_compressed_ip6($endip_bin);
387
	}
388
	return "";
389
}
390

    
391
/* returns a subnet mask (long given a bit count) */
392
function gen_subnet_mask_long($bits) {
393
	$sm = 0;
394
	for ($i = 0; $i < $bits; $i++) {
395
		$sm >>= 1;
396
		$sm |= 0x80000000;
397
	}
398
	return $sm;
399
}
400

    
401
/* same as above but returns a string */
402
function gen_subnet_mask($bits) {
403
	return long2ip(gen_subnet_mask_long($bits));
404
}
405

    
406
/* Convert a prefix length to an IPv6 address-like mask notation. Very rare but at least ntp needs it. See #4463 */
407
function gen_subnet_mask_v6($bits) {
408
	/* Binary representation of the prefix length */
409
	$bin = str_repeat('1', $bits);
410
	/* Pad right with zeroes to reach the full address length */
411
	$bin = str_pad($bin, 128, '0', STR_PAD_RIGHT);
412
	/* Convert back to an IPv6 address style notation */
413
	return bin_to_ip6($bin);
414
}
415

    
416
/* Convert long int to IPv4 address
417
   Returns '' if not valid IPv4 (including if any bits >32 are non-zero) */
418
function long2ip32($ip) {
419
	return long2ip($ip & 0xFFFFFFFF);
420
}
421

    
422
/* Convert IPv4 address to long int, truncated to 32-bits to avoid sign extension on 64-bit platforms.
423
   Returns '' if not valid IPv4. */
424
function ip2long32($ip) {
425
	return (ip2long($ip) & 0xFFFFFFFF);
426
}
427

    
428
/* Convert IPv4 address to unsigned long int.
429
   Returns '' if not valid IPv4. */
430
function ip2ulong($ip) {
431
	return sprintf("%u", ip2long32($ip));
432
}
433

    
434
/*
435
 * Convert IPv6 address to binary
436
 *
437
 * Obtained from: pear-Net_IPv6
438
 */
439
function ip6_to_bin($ip) {
440
	$binstr = '';
441

    
442
	$ip = Net_IPv6::removeNetmaskSpec($ip);
443
	$ip = Net_IPv6::Uncompress($ip);
444

    
445
	$parts = explode(':', $ip);
446

    
447
	foreach ( $parts as $v ) {
448

    
449
		$str     = base_convert($v, 16, 2);
450
		$binstr .= str_pad($str, 16, '0', STR_PAD_LEFT);
451

    
452
	}
453

    
454
	return $binstr;
455
}
456

    
457
/*
458
 * Convert IPv6 binary to uncompressed address
459
 *
460
 * Obtained from: pear-Net_IPv6
461
 */
462
function bin_to_ip6($bin) {
463
	$ip = "";
464

    
465
	if (strlen($bin) < 128) {
466
		$bin = str_pad($bin, 128, '0', STR_PAD_LEFT);
467
	}
468

    
469
	$parts = str_split($bin, "16");
470

    
471
	foreach ( $parts as $v ) {
472
		$str = base_convert($v, 2, 16);
473
		$ip .= $str.":";
474
	}
475

    
476
	$ip = substr($ip, 0, -1);
477

    
478
	return $ip;
479
}
480

    
481
/*
482
 * Convert IPv6 binary to compressed address
483
 */
484
function bin_to_compressed_ip6($bin) {
485
	return text_to_compressed_ip6(bin_to_ip6($bin));
486
}
487

    
488
/*
489
 * Convert textual IPv6 address string to compressed address
490
 */
491
function text_to_compressed_ip6($text) {
492
	// Force re-compression by passing parameter 2 (force) true.
493
	// This ensures that supposedly-compressed formats are uncompressed
494
	// first then re-compressed into strictly correct form.
495
	// e.g. 2001:0:0:4:0:0:0:1
496
	// 2001::4:0:0:0:1 is a strictly-incorrect compression,
497
	// but maybe the user entered it like that.
498
	// The "force" parameter will ensure it is returned as:
499
	// 2001:0:0:4::1
500
	return Net_IPv6::compress($text, true);
501
}
502

    
503
/* Find out how many IPs are contained within a given IP range
504
 *  e.g. 192.168.0.0 to 192.168.0.255 returns 256
505
 */
506
function ip_range_size_v4($startip, $endip) {
507
	if (is_ipaddrv4($startip) && is_ipaddrv4($endip)) {
508
		// Operate as unsigned long because otherwise it wouldn't work
509
		//   when crossing over from 127.255.255.255 / 128.0.0.0 barrier
510
		return abs(ip2ulong($startip) - ip2ulong($endip)) + 1;
511
	}
512
	return -1;
513
}
514

    
515
/* Find the smallest possible subnet mask which can contain a given number of IPs
516
 *  e.g. 512 IPs can fit in a /23, but 513 IPs need a /22
517
 */
518
function find_smallest_cidr_v4($number) {
519
	$smallest = 1;
520
	for ($b=32; $b > 0; $b--) {
521
		$smallest = ($number <= pow(2, $b)) ? $b : $smallest;
522
	}
523
	return (32-$smallest);
524
}
525

    
526
/* Return the previous IP address before the given address */
527
function ip_before($ip, $offset = 1) {
528
	return long2ip32(ip2long($ip) - $offset);
529
}
530

    
531
/* Return the next IP address after the given address */
532
function ip_after($ip, $offset = 1) {
533
	return long2ip32(ip2long($ip) + $offset);
534
}
535

    
536
/* Return true if the first IP is 'before' the second */
537
function ip_less_than($ip1, $ip2) {
538
	// Compare as unsigned long because otherwise it wouldn't work when
539
	//   crossing over from 127.255.255.255 / 128.0.0.0 barrier
540
	return ip2ulong($ip1) < ip2ulong($ip2);
541
}
542

    
543
/* Return true if the first IP is 'after' the second */
544
function ip_greater_than($ip1, $ip2) {
545
	// Compare as unsigned long because otherwise it wouldn't work
546
	//   when crossing over from 127.255.255.255 / 128.0.0.0 barrier
547
	return ip2ulong($ip1) > ip2ulong($ip2);
548
}
549

    
550
/* compare two IP addresses */
551
function ipcmp($a, $b) {
552
	if (is_subnet($a)) {
553
		$a = explode('/', $a)[0];
554
	}
555
	if (is_subnet($b)) {
556
		$b = explode('/', $b)[0];
557
	}
558
	if (ip_less_than($a, $b)) {
559
		return -1;
560
	} else if (ip_greater_than($a, $b)) {
561
		return 1;
562
	} else {
563
		return 0;
564
	}
565
}
566

    
567
/* Convert a range of IPv4 addresses to an array of individual addresses. */
568
/* Note: IPv6 ranges are not yet supported here. */
569
function ip_range_to_address_array($startip, $endip, $max_size = 5000) {
570
	if (!is_ipaddrv4($startip) || !is_ipaddrv4($endip)) {
571
		return false;
572
	}
573

    
574
	if (ip_greater_than($startip, $endip)) {
575
		// Swap start and end so we can process sensibly.
576
		$temp = $startip;
577
		$startip = $endip;
578
		$endip = $temp;
579
	}
580

    
581
	if (ip_range_size_v4($startip, $endip) > $max_size) {
582
		return false;
583
	}
584

    
585
	// Container for IP addresses within this range.
586
	$rangeaddresses = array();
587
	$end_int = ip2ulong($endip);
588
	for ($ip_int = ip2ulong($startip); $ip_int <= $end_int; $ip_int++) {
589
		$rangeaddresses[] = long2ip($ip_int);
590
	}
591

    
592
	return $rangeaddresses;
593
}
594

    
595
/*
596
 * Convert an IPv4 or IPv6 IP range to an array of subnets which can contain the range.
597
 * Algorithm and embodying code PD'ed by Stilez - enjoy as you like :-)
598
 *
599
 * Documented on pfsense dev list 19-20 May 2013. Summary:
600
 *
601
 * The algorithm looks at patterns of 0's and 1's in the least significant bit(s), whether IPv4 or IPv6.
602
 * These are all that needs checking to identify a _guaranteed_ correct, minimal and optimal subnet array.
603
 *
604
 * As a result, string/binary pattern matching of the binary IP is very efficient. It uses just 2 pattern-matching rules
605
 * to chop off increasingly larger subnets at both ends that can't be part of larger subnets, until nothing's left.
606
 *
607
 * (a) If any range has EITHER low bit 1 (in startip) or 0 (in endip), that end-point is _always guaranteed_ to be optimally
608
 * represented by its own 'single IP' CIDR; the remaining range then shrinks by one IP up or down, causing the new end-point's
609
 * low bit to change from 1->0 (startip) or 0->1 (endip). Only one edge case needs checking: if a range contains exactly 2
610
 * adjacent IPs of this format, then the two IPs themselves are required to span it, and we're done.
611
 * 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
612
 * low bits can now be ignored.
613
 *
614
 * (b) If any range has BOTH startip and endip ending in some number of 0's and 1's respectively, these low bits can
615
 * *always* be ignored and "bit-shifted" for subnet spanning. So provided we remember the bits we've place-shifted, we can
616
 * _always_ right-shift and chop off those bits, leaving a smaller range that has EITHER startip ending in 1 or endip ending
617
 * in 0 (ie can now apply (a) again) or the entire range has vanished and we're done.
618
 * We then loop to redo (a) again on the remaining (place shifted) range until after a few loops, the remaining (place shifted)
619
 * range 'vanishes' by meeting the exit criteria of (a) or (b), and we're done.
620
 */
621
function ip_range_to_subnet_array($ip1, $ip2) {
622

    
623
	if (is_ipaddrv4($ip1) && is_ipaddrv4($ip2)) {
624
		$proto = 'ipv4';  // for clarity
625
		$bits = 32;
626
		$ip1bin = decbin(ip2long32($ip1));
627
		$ip2bin = decbin(ip2long32($ip2));
628
	} elseif (is_ipaddrv6($ip1) && is_ipaddrv6($ip2)) {
629
		$proto = 'ipv6';
630
		$bits = 128;
631
		$ip1bin = ip6_to_bin($ip1);
632
		$ip2bin = ip6_to_bin($ip2);
633
	} else {
634
		return array();
635
	}
636

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

    
641
	if ($ip1bin == $ip2bin) {
642
		return array($ip1 . '/' . $bits); // exit if ip1=ip2 (trivial case)
643
	}
644

    
645
	if ($ip1bin > $ip2bin) {
646
		list ($ip1bin, $ip2bin) = array($ip2bin, $ip1bin);  // swap if needed (ensures ip1 < ip2)
647
	}
648

    
649
	$rangesubnets = array();
650
	$netsize = 0;
651

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

    
656
		// 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)
657

    
658
		if (substr($ip1bin, -1, 1) == '1') {
659
			// the start ip must be in a separate one-IP cidr range
660
			$new_subnet_ip = substr($ip1bin, $netsize, $bits - $netsize) . str_repeat('0', $netsize);
661
			$rangesubnets[$new_subnet_ip] = $bits - $netsize;
662
			$n = strrpos($ip1bin, '0');  //can't be all 1's
663
			$ip1bin = ($n == 0 ? '' : substr($ip1bin, 0, $n)) . '1' . str_repeat('0', $bits - $n - 1);  // BINARY VERSION OF $ip1 += 1
664
		}
665

    
666
		// 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)
667

    
668
		if (substr($ip2bin, -1, 1) == '0') {
669
			// the end ip must be in a separate one-IP cidr range
670
			$new_subnet_ip = substr($ip2bin, $netsize, $bits - $netsize) . str_repeat('0', $netsize);
671
			$rangesubnets[$new_subnet_ip] = $bits - $netsize;
672
			$n = strrpos($ip2bin, '1');  //can't be all 0's
673
			$ip2bin = ($n == 0 ? '' : substr($ip2bin, 0, $n)) . '0' . str_repeat('1', $bits - $n - 1);  // BINARY VERSION OF $ip2 -= 1
674
			// already checked for the edge case where end = start+1 and start ends in 0x1, above, so it's safe
675
		}
676

    
677
		// this is the only edge case arising from increment/decrement.
678
		// 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)
679

    
680
		if ($ip2bin < $ip1bin) {
681
			continue;
682
		}
683

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

    
687
		$shift = $bits - max(strrpos($ip1bin, '0'), strrpos($ip2bin, '1'));  // num of low bits which are '0' in ip1 and '1' in ip2
688
		$ip1bin = str_repeat('0', $shift) . substr($ip1bin, 0, $bits - $shift);
689
		$ip2bin = str_repeat('0', $shift) . substr($ip2bin, 0, $bits - $shift);
690
		$netsize += $shift;
691
		if ($ip1bin == $ip2bin) {
692
			// we're done.
693
			$new_subnet_ip = substr($ip1bin, $netsize, $bits - $netsize) . str_repeat('0', $netsize);
694
			$rangesubnets[$new_subnet_ip] = $bits - $netsize;
695
			continue;
696
		}
697

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

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

    
703
	ksort($rangesubnets, SORT_STRING);
704
	$out = array();
705

    
706
	foreach ($rangesubnets as $ip => $netmask) {
707
		if ($proto == 'ipv4') {
708
			$i = str_split($ip, 8);
709
			$out[] = implode('.', array(bindec($i[0]), bindec($i[1]), bindec($i[2]), bindec($i[3]))) . '/' . $netmask;
710
		} else {
711
			$out[] = bin_to_compressed_ip6($ip) . '/' . $netmask;
712
		}
713
	}
714

    
715
	return $out;
716
}
717

    
718
/* returns true if $range is a valid pair of IPv4 or IPv6 addresses separated by a "-"
719
	false - if not a valid pair
720
	true (numeric 4 or 6) - if valid, gives type of addresses */
721
function is_iprange($range) {
722
	if (substr_count($range, '-') != 1) {
723
		return false;
724
	}
725
	list($ip1, $ip2) = explode ('-', $range);
726
	if (is_ipaddrv4($ip1) && is_ipaddrv4($ip2)) {
727
		return 4;
728
	}
729
	if (is_ipaddrv6($ip1) && is_ipaddrv6($ip2)) {
730
		return 6;
731
	}
732
	return false;
733
}
734

    
735
/* returns true if $ipaddr is a valid dotted IPv4 address or a IPv6
736
	false - not valid
737
	true (numeric 4 or 6) - if valid, gives type of address */
738
function is_ipaddr($ipaddr) {
739
	if (is_ipaddrv4($ipaddr)) {
740
		return 4;
741
	}
742
	if (is_ipaddrv6($ipaddr)) {
743
		return 6;
744
	}
745
	return false;
746
}
747

    
748
/* returns true if $ipaddr is a valid IPv6 address */
749
function is_ipaddrv6($ipaddr) {
750
	if (!is_string($ipaddr) || empty($ipaddr)) {
751
		return false;
752
	}
753
	/*
754
	 * While Net_IPv6::checkIPv6() considers IPv6/mask a valid IPv6,
755
	 * is_ipaddrv6() needs to be more strict to keep the compatibility
756
	 * with is_ipaddrv4().
757
	 */
758
	if (strstr($ipaddr, "/")) {
759
		return false;
760
	}
761
	if (strstr($ipaddr, "%") && is_linklocal($ipaddr)) {
762
		$tmpip = explode("%", $ipaddr);
763
		$ipaddr = $tmpip[0];
764
	}
765
	/*
766
	 * Net_IPv6::checkIPv6 does not reject multiple attempts at compression
767
	 * so we must check it beforehand.
768
	 * https://redmine.pfsense.org/issues/13069
769
	 */
770
	if (substr_count($ipaddr, '::') > 1) {
771
		return false;
772
	}
773
	return Net_IPv6::checkIPv6($ipaddr);
774
}
775

    
776
function is_ipaddrv6_v4map($ipaddr) {
777
	/* check RFC4291 par 2.2.2 format, ex: fd00::1.2.3.4
778
	 * see https://redmine.pfsense.org/issues/11446 */
779
	if (is_ipaddrv6($ipaddr) && preg_match('/^[0-9a-f:]{2,30}[0-9.]{7,15}$/i', $ipaddr)) {
780
		return true;
781
	}
782
	return false;
783
}
784

    
785
/* returns true if $ipaddr is a valid dotted IPv4 address */
786
function is_ipaddrv4($ipaddr) {
787
	if (!is_string($ipaddr) || empty($ipaddr) || ip2long($ipaddr) === FALSE) {
788
		return false;
789
	}
790
	return true;
791
}
792

    
793
function is_mcast($ipaddr) {
794
	if (is_mcastv4($ipaddr)) {
795
		return 4;
796
	}
797
	if (is_mcastv6($ipaddr)) {
798
		return 6;
799
	}
800
	return false;
801
}
802

    
803
function is_mcastv4($ipaddr) {
804
	if (!is_ipaddrv4($ipaddr) ||
805
	    !preg_match('/^2(?:2[4-9]|3\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d?|0)){3}$/', $ipaddr)) {
806
		return false;
807
	}
808
	return true;
809
}
810

    
811
function is_mcastv6($ipaddr) {
812
	if (!is_ipaddrv6($ipaddr) || !preg_match('/^ff.+$/', $ipaddr)) {
813
		return false;
814
	}
815
	return true;
816
}
817

    
818
/* returns 4 or 6 respectively (== TRUE) if $ipaddr is a valid IPv4 or IPv6 linklocal address
819
   returns '' if not a valid linklocal address */
820
function is_linklocal($ipaddr) {
821
	if (is_ipaddrv4($ipaddr)) {
822
		// input is IPv4
823
		// test if it's 169.254.x.x per rfc3927 2.1
824
		$ip4 = explode(".", $ipaddr);
825
		if ($ip4[0] == '169' && $ip4[1] == '254') {
826
			return 4;
827
		}
828
	} elseif (Net_IPv6::getAddressType($ipaddr) == NET_IPV6_LOCAL_LINK) {
829
		return 6;
830
	}
831
	return '';
832
}
833

    
834
/* returns scope of a linklocal address */
835
function get_ll_scope($addr) {
836
	if (!is_linklocal($addr) || !strstr($addr, "%")) {
837
		return "";
838
	}
839
	return explode("%", $addr)[1];
840
}
841

    
842
/* returns true if $ipaddr is a valid literal IPv6 address */
843
function is_literalipaddrv6($ipaddr) {
844
	if (substr($ipaddr,0,1) == '[' && substr($ipaddr,-1,1) == ']') {
845
		// if it's data wrapped in "[ ... ]" then test if middle part is valid IPv6
846
		return is_ipaddrv6(substr($ipaddr,1,-1));
847
	}
848
	return false;
849
}
850

    
851
/* returns true if $iport is a valid IPv4:port or [Literal IPv6]:port
852
	false - not valid
853
	true (numeric 4 or 6) - if valid, gives type of address */
854
function is_ipaddrwithport($ipport) {
855
	$c = strrpos($ipport, ":");
856
	if ($c === false) {
857
		return false;  // can't split at final colon if no colon exists
858
	}
859

    
860
	if (!is_port(substr($ipport, $c + 1))) {
861
		return false;  // no valid port after last colon
862
	}
863

    
864
	$ip = substr($ipport, 0, $c);  // else is text before last colon a valid IP
865
	if (is_literalipaddrv6($ip)) {
866
		return 6;
867
	} elseif (is_ipaddrv4($ip)) {
868
		return 4;
869
	} else {
870
		return false;
871
	}
872
}
873

    
874
function is_hostnamewithport($hostport) {
875
	$parts = explode(":", $hostport);
876
	// no need to validate with is_string(); if it's not a string then explode won't return 2 parts anyway
877
	if (count($parts) == 2) {
878
		return is_hostname($parts[0]) && is_port($parts[1]);
879
	}
880
	return false;
881
}
882

    
883
/* returns true if $ipaddr is a valid dotted IPv4 address or an alias thereof */
884
function is_ipaddroralias($ipaddr) {
885
	if (is_alias($ipaddr)) {
886
		foreach (config_get_path('aliases/alias', []) as $alias) {
887
			if ($alias['name'] == $ipaddr && !preg_match("/port/i", $alias['type'])) {
888
				return true;
889
			}
890
		}
891
		return false;
892
	} else {
893
		return is_ipaddr($ipaddr);
894
	}
895

    
896
}
897

    
898
/* returns true if $subnet is a valid IPv4 or IPv6 subnet in CIDR format
899
	false - if not a valid subnet
900
	true (numeric 4 or 6) - if valid, gives type of subnet */
901
function is_subnet($subnet) {
902
	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)) {
903
		if (is_ipaddrv4($parts[1]) && $parts[3] <= 32) {
904
			return 4;
905
		}
906
		if (is_ipaddrv6($parts[2]) && $parts[3] <= 128) {
907
			return 6;
908
		}
909
	}
910
	return false;
911
}
912

    
913
function is_v4($ip_or_subnet) {
914
	return is_ipaddrv4($ip_or_subnet) || is_subnetv4($ip_or_subnet);
915
}
916

    
917
function is_v6($ip_or_subnet) {
918
	return is_ipaddrv6($ip_or_subnet) || is_subnetv6($ip_or_subnet);
919
}
920

    
921
/**
922
 * Checks if an IPv6 address is a Global Unicast Address (GUA).
923
 * 
924
 * @param string $ip_or_subnet IPv6 address or subnet
925
 * 
926
 * @return bool
927
 */
928
function is_v6gua(string $ip_or_subnet): bool {
929
	if (is_subnet($ip_or_subnet) == 6) {
930
		list($ip_or_subnet) = explode('/', $ip_or_subnet);
931
	}
932
	if (!is_ipaddrv6($ip_or_subnet)) {
933
		return false;
934
	}
935
	if (ip_in_subnet($ip_or_subnet, '2000::/3')) {
936
		return true;
937
	} else {
938
		return false;
939
	}
940
}
941

    
942
/* same as is_subnet() but accepts IPv4 only */
943
function is_subnetv4($subnet) {
944
	return (is_subnet($subnet) == 4);
945
}
946

    
947
/* same as is_subnet() but accepts IPv6 only */
948
function is_subnetv6($subnet) {
949
	return (is_subnet($subnet) == 6);
950
}
951

    
952
/* returns true if $subnet is a valid subnet in CIDR format or an alias thereof */
953
function is_subnetoralias($subnet) {
954
	global $aliastable;
955

    
956
	if (isset($aliastable[$subnet]) && is_subnet($aliastable[$subnet])) {
957
		return true;
958
	} else {
959
		return is_subnet($subnet);
960
	}
961
}
962

    
963
/* Get number of addresses in an IPv4/IPv6 subnet (represented as a string)
964
   optional $exact=true forces error (0) to be returned if it can't be represented exactly
965
   Exact result not possible above PHP_MAX_INT which is about 2^31 addresses on x32 or 2^63 on x64
966
   Returns 0 for bad data or if cannot represent size as an INT when $exact is set. */
967
function subnet_size($subnet, $exact=false) {
968
	$parts = explode("/", $subnet);
969
	$iptype = is_ipaddr($parts[0]);
970
	if (count($parts) == 2 && $iptype) {
971
		return subnet_size_by_netmask($iptype, $parts[1], $exact);
972
	}
973
	return 0;
974
}
975

    
976
/* Get number of addresses in an IPv4/IPv6 subnet (represented numerically as IP type + bits)
977
   optional $exact=true forces error (0) to be returned if it can't be represented exactly
978
   Hard to think where we might need to count exactly a huge subnet but an overflow detection option is probably sensible
979
   Returns 0 for bad data or if cannot represent size as an INT when $exact is set. */
980
function subnet_size_by_netmask($iptype, $bits, $exact=false) {
981
	if (!is_numericint($bits)) {
982
		return 0;
983
	} elseif ($iptype == 4 && $bits <= 32) {
984
		$snsize = 32 - $bits;
985
	} elseif ($iptype == 6 && $bits <= 128) {
986
		$snsize = 128 - $bits;
987
	} else {
988
		return 0;
989
	}
990

    
991
	// 2**N returns an exact result as an INT if possible, and a float/double if not.
992
	// 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
993
	$result = 2 ** $snsize;
994

    
995
	if ($exact && !is_int($result)) {
996
		//exact required but can't represent result exactly as an INT
997
		return 0;
998
	} else {
999
		// result ok, will be an INT where possible (guaranteed up to 2^31 addresses on x32/x64) and a float for 'huge' subnets
1000
		return $result;
1001
	}
1002
}
1003

    
1004
/* function used by pfblockerng */
1005
function subnetv4_expand($subnet) {
1006
	$result = array();
1007
	list ($ip, $bits) = explode("/", $subnet);
1008
	$net = ip2long($ip);
1009
	$mask = (0xffffffff << (32 - $bits));
1010
	$net &= $mask;
1011
	$size = round(exp(log(2) * (32 - $bits)));
1012
	for ($i = 0; $i < $size; $i += 1) {
1013
		$result[] = long2ip($net | $i);
1014
	}
1015
	return $result;
1016
}
1017

    
1018
/* find out whether two IPv4/IPv6 CIDR subnets overlap.
1019
   Note: CIDR overlap implies one is identical or included so largest sn will be the same */
1020
function check_subnets_overlap($subnet1, $bits1, $subnet2, $bits2) {
1021
	if (is_ipaddrv4($subnet1)) {
1022
		return check_subnetsv4_overlap($subnet1, $bits1, $subnet2, $bits2);
1023
	} else {
1024
		return check_subnetsv6_overlap($subnet1, $bits1, $subnet2, $bits2);
1025
	}
1026
}
1027

    
1028
/* find out whether two IPv4 CIDR subnets overlap.
1029
   Note: CIDR overlap means sn1/sn2 are identical or one is included in other. So sn using largest $bits will be the same  */
1030
function check_subnetsv4_overlap($subnet1, $bits1, $subnet2, $bits2) {
1031
	$largest_sn = min($bits1, $bits2);
1032
	$subnetv4_start1 = gen_subnetv4($subnet1, $largest_sn);
1033
	$subnetv4_start2 = gen_subnetv4($subnet2, $largest_sn);
1034

    
1035
	if ($subnetv4_start1 == '' || $subnetv4_start2 == '') {
1036
		// One or both args is not a valid IPv4 subnet
1037
		//FIXME: needs to return "bad data" not true/false if bad. For now return false, best we can do until fixed
1038
		return false;
1039
	}
1040
	return ($subnetv4_start1 == $subnetv4_start2);
1041
}
1042

    
1043
/* find out whether two IPv6 CIDR subnets overlap.
1044
   Note: CIDR overlap means sn1/sn2 are identical or one is included in other. So sn using largest $bits will be the same  */
1045
function check_subnetsv6_overlap($subnet1, $bits1, $subnet2, $bits2) {
1046
	$largest_sn = min($bits1, $bits2);
1047
	$subnetv6_start1 = gen_subnetv6($subnet1, $largest_sn);
1048
	$subnetv6_start2 = gen_subnetv6($subnet2, $largest_sn);
1049

    
1050
	if ($subnetv6_start1 == '' || $subnetv6_start2 == '') {
1051
		// One or both args is not a valid IPv6 subnet
1052
		//FIXME: needs to return "bad data" not true/false if bad. For now return false, best we can do until fixed
1053
		return false;
1054
	}
1055
	return ($subnetv6_start1 == $subnetv6_start2);
1056
}
1057

    
1058
/* return all PTR zones for a IPv6 network */
1059
function get_v6_ptr_zones($subnet, $bits) {
1060
	$result = array();
1061

    
1062
	if (!is_ipaddrv6($subnet)) {
1063
		return $result;
1064
	}
1065

    
1066
	if (!is_numericint($bits) || $bits > 128) {
1067
		return $result;
1068
	}
1069

    
1070
	/*
1071
	 * Find a small nibble boundary subnet mask
1072
	 * e.g. a /29 will create 8 /32 PTR zones
1073
	 */
1074
	$small_sn = $bits;
1075
	while ($small_sn % 4 != 0) {
1076
		$small_sn++;
1077
	}
1078

    
1079
	/* Get network prefix */
1080
	$small_subnet = Net_IPv6::getNetmask($subnet, $bits);
1081

    
1082
	/*
1083
	 * While small network is part of bigger one, increase 4-bit in last
1084
	 * digit to get next small network
1085
	 */
1086
	while (Net_IPv6::isInNetmask($small_subnet, $subnet, $bits)) {
1087
		/* Get a pure hex value */
1088
		$unpacked = unpack('H*hex', inet_pton($small_subnet));
1089
		/* Create PTR record using $small_sn / 4 chars */
1090
		$result[] = implode('.', array_reverse(str_split(substr(
1091
		    $unpacked['hex'], 0, $small_sn / 4)))).'.ip6.arpa';
1092

    
1093
		/* Detect what part of IP should be increased */
1094
		$change_part = (int) ($small_sn / 16);
1095
		if ($small_sn % 16 == 0) {
1096
			$change_part--;
1097
		}
1098

    
1099
		/* Increase 1 to desired part */
1100
		$parts = explode(":", Net_IPv6::uncompress($small_subnet));
1101
		$parts[$change_part]++;
1102
		$small_subnet = implode(":", $parts);
1103
	}
1104

    
1105
	return $result;
1106
}
1107

    
1108
/* return true if $addr is in $subnet, false if not */
1109
function ip_in_subnet($addr, $subnet) {
1110
	if (is_ipaddrv6($addr) && is_subnetv6($subnet)) {
1111
		/* Normalize IPv6 prefix to its start address to avoid PHP errors
1112
		 * https://redmine.pfsense.org/issues/14256
1113
		 */
1114
		list($prefix, $length) = explode("/", $subnet);
1115
		$prefix = gen_subnetv6($prefix, $length);
1116
		$subnet = "{$prefix}/{$length}";
1117
		return (Net_IPv6::isInNetmask($addr, $subnet));
1118
	} else if (is_ipaddrv4($addr) && is_subnetv4($subnet)) {
1119
		list($ip, $mask) = explode('/', $subnet);
1120
		$mask = (0xffffffff << (32 - $mask)) & 0xffffffff;
1121
		return ((ip2long($addr) & $mask) == (ip2long($ip) & $mask));
1122
	}
1123
	return false;
1124
}
1125

    
1126
/* returns true if $hostname is just a valid hostname (top part without any of the domain part) */
1127
function is_unqualified_hostname($hostname) {
1128
	if (!is_string($hostname)) {
1129
		return false;
1130
	}
1131

    
1132
	if (preg_match('/^(?:[a-z0-9_]|[a-z0-9_][a-z0-9_\-]*[a-z0-9_])$/i', $hostname)) {
1133
		return true;
1134
	} else {
1135
		return false;
1136
	}
1137
}
1138

    
1139
/* returns true if $hostname is a valid hostname, with or without being a fully-qualified domain name. */
1140
function is_hostname($hostname, $allow_wildcard=false) {
1141
	if (!is_string($hostname)) {
1142
		return false;
1143
	}
1144

    
1145
	if (is_domain($hostname, $allow_wildcard)) {
1146
		if ((substr_count($hostname, ".") == 1) && ($hostname[strlen($hostname)-1] == ".")) {
1147
			/* Only a single dot at the end like "test." - hosts cannot be directly in the root domain. */
1148
			return false;
1149
		} else {
1150
			return true;
1151
		}
1152
	} else {
1153
		return false;
1154
	}
1155
}
1156

    
1157
/* returns true if $domain is a valid domain name */
1158
function is_domain($domain, $allow_wildcard=false, $trailing_dot=true) {
1159
	if (!is_string($domain)) {
1160
		return false;
1161
	}
1162
	if (!$trailing_dot && ($domain[strlen($domain)-1] == ".")) {
1163
		return false;
1164
	}
1165
	if ($allow_wildcard) {
1166
		$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';
1167
	} else {
1168
		$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';
1169
	}
1170

    
1171
	if (preg_match($domain_regex, $domain)) {
1172
		return true;
1173
	} else {
1174
		return false;
1175
	}
1176
}
1177

    
1178
/* returns true if $macaddr is a valid MAC address */
1179
function is_macaddr($macaddr, $partial=false) {
1180
	$values = explode(":", $macaddr);
1181

    
1182
	/* Verify if the MAC address has a proper amount of parts for either a partial or full match. */
1183
	if ($partial) {
1184
		if ((count($values) < 1) || (count($values) > 6)) {
1185
			return false;
1186
		}
1187
	} elseif (count($values) != 6) {
1188
		return false;
1189
	}
1190
	for ($i = 0; $i < count($values); $i++) {
1191
		if (ctype_xdigit($values[$i]) == false)
1192
			return false;
1193
		if (hexdec($values[$i]) < 0 || hexdec($values[$i]) > 255)
1194
			return false;
1195
	}
1196

    
1197
	return true;
1198
}
1199

    
1200
/*
1201
	If $return_message is true then
1202
		returns a text message about the reason that the name is invalid.
1203
		the text includes the type of "thing" that is being checked, passed in $object. (e.g. "alias", "gateway group", "schedule")
1204
	else
1205
		returns true if $name is a valid name for an alias
1206
		returns false if $name is not a valid name for an alias
1207

    
1208
	Aliases cannot be:
1209
		bad chars: anything except a-z 0-9 and underscore
1210
		bad names: empty string, pure numeric, pure underscore
1211
		reserved words: pre-defined service/protocol/port names which should not be ambiguous, and the words "port" and  "pass" */
1212

    
1213
function is_validaliasname($name, $return_message = false, $object = "alias") {
1214
	/* Array of reserved words */
1215
	$reserved = array("port", "pass");
1216

    
1217
	if (!is_string($name) || strlen($name) >= 32 || preg_match('/(^_*$|^\d*$|[^a-z0-9_])/i', $name)) {
1218
		if ($return_message) {
1219
			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, _');
1220
		} else {
1221
			return false;
1222
		}
1223
	}
1224
	if (in_array($name, $reserved, true)) {
1225
		if ($return_message) {
1226
			return sprintf(gettext('The %1$s name must not be either of the reserved words %2$s or %3$s.'), $object, "'port'", "'pass'");
1227
		} else {
1228
			return false;
1229
		}
1230
	}
1231
	if (getprotobyname($name)) {
1232
		if ($return_message) {
1233
			return sprintf(gettext('The %1$s name must not be an IP protocol name such as TCP, UDP, ICMP etc.'), $object);
1234
		} else {
1235
			return false;
1236
		}
1237
	}
1238
	if (getservbyname($name, "tcp") || getservbyname($name, "udp")) {
1239
		if ($return_message) {
1240
			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);
1241
		} else {
1242
			return false;
1243
		}
1244
	}
1245
	if ($return_message) {
1246
		return sprintf(gettext('The %1$s name is valid.'), $object);
1247
	} else {
1248
		return true;
1249
	}
1250
}
1251

    
1252
/* returns a text message indicating if the alias name is valid, or the reason it is not valid. */
1253
function invalidaliasnamemsg($name, $object = "alias") {
1254
	return is_validaliasname($name, true, $object);
1255
}
1256

    
1257
/*
1258
 * returns true if $range is a valid integer range between $min and $max
1259
 * range delimiter can be ':' or '-'
1260
 */
1261
function is_intrange($range, $min, $max) {
1262
	$values = preg_split("/[:-]/", $range);
1263

    
1264
	if (!is_array($values) || count($values) != 2) {
1265
		return false;
1266
	}
1267

    
1268
	if (!ctype_digit(strval($values[0])) || !ctype_digit(strval($values[1]))) {
1269
		return false;
1270
	}
1271

    
1272
	$values[0] = intval($values[0]);
1273
	$values[1] = intval($values[1]);
1274

    
1275
	if ($values[0] >= $values[1]) {
1276
		return false;
1277
	}
1278

    
1279
	if ($values[0] < $min || $values[1] > $max) {
1280
		return false;
1281
	}
1282

    
1283
	return true;
1284
}
1285

    
1286
/* returns true if $port is a valid TCP/UDP/SCTP port */
1287
function is_port($port) {
1288
	if (ctype_digit(strval($port)) && ((intval($port) >= 1) && (intval($port) <= 65535))) {
1289
		return true;
1290
	}
1291
	if (getservbyname($port, "tcp") || getservbyname($port, "udp") || getservbyname($port, "sctp")) {
1292
		return true;
1293
	}
1294
	return false;
1295
}
1296

    
1297
/* returns true if $port is in use */
1298
function is_port_in_use($port, $proto = "tcp", $ip_version = 4) {
1299
	$port_info = array();
1300
	exec("/usr/bin/netstat --libxo json -an " . escapeshellarg('-' . $ip_version) . " -p " . escapeshellarg($proto), $rawdata, $rc);
1301
	if ($rc == 0) {
1302
		$netstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
1303
		$netstatarr = $netstatarr['statistics']['socket'];
1304

    
1305
		foreach($netstatarr as $portstats){
1306
			array_push($port_info, $portstats['local']['port']);
1307
		}
1308
	}
1309

    
1310
	return in_array($port, $port_info);
1311
}
1312

    
1313
/* returns true if $portrange is a valid portrange ("<port>:<port>") */
1314
function is_portrange($portrange) {
1315
	$ports = explode(":", $portrange);
1316

    
1317
	return (count($ports) == 2 && is_port($ports[0]) && is_port($ports[1]));
1318
}
1319

    
1320
/* returns true if $port is a valid port number or range ("<port>:<port>") */
1321
function is_port_or_range($port) {
1322
	return (is_port($port) || is_portrange($port));
1323
}
1324

    
1325
/* returns true if $port is an alias that is a port type */
1326
function is_portalias($port) {
1327
	if (is_alias($port)) {
1328
		foreach (config_get_path('aliases/alias', []) as $alias) {
1329
			if ($alias['name'] == $port && preg_match("/port/i", $alias['type'])) {
1330
				return true;
1331
			}
1332
		}
1333
	}
1334
	return false;
1335
}
1336

    
1337
/* returns true if $port is a valid port number or an alias thereof */
1338
function is_port_or_alias($port) {
1339
	return (is_port($port) || is_portalias($port));
1340
}
1341

    
1342
/* returns true if $port is a valid port number or range ("<port>:<port>") or an alias thereof */
1343
function is_port_or_range_or_alias($port) {
1344
	return (is_port($port) || is_portrange($port) || is_portalias($port));
1345
}
1346

    
1347
/* create ranges of sequential port numbers (200:215) and remove duplicates */
1348
function group_ports($ports, $kflc = false) {
1349
	if (!is_array($ports) || empty($ports)) {
1350
		return;
1351
	}
1352

    
1353
	$uniq = array();
1354
	$comments = array();
1355
	foreach ($ports as $port) {
1356
		if (($kflc) && (strpos($port, '#') === 0)) {	// Keep Full Line Comments (lines beginning with #).
1357
			$comments[] = $port;
1358
		} else if (is_portrange($port)) {
1359
			list($begin, $end) = explode(":", $port);
1360
			if ($begin > $end) {
1361
				$aux = $begin;
1362
				$begin = $end;
1363
				$end = $aux;
1364
			}
1365
			for ($i = $begin; $i <= $end; $i++) {
1366
				if (!in_array($i, $uniq)) {
1367
					$uniq[] = $i;
1368
				}
1369
			}
1370
		} else if (is_port($port)) {
1371
			if (!in_array($port, $uniq)) {
1372
				$uniq[] = $port;
1373
			}
1374
		}
1375
	}
1376
	sort($uniq, SORT_NUMERIC);
1377

    
1378
	$result = array();
1379
	foreach ($uniq as $idx => $port) {
1380
		if ($idx == 0) {
1381
			$result[] = $port;
1382
			continue;
1383
		}
1384

    
1385
		$last = end($result);
1386
		if (is_portrange($last)) {
1387
			list($begin, $end) = explode(":", $last);
1388
		} else {
1389
			$begin = $end = $last;
1390
		}
1391

    
1392
		if ($port == ($end+1)) {
1393
			$end++;
1394
			$result[count($result)-1] = "{$begin}:{$end}";
1395
		} else {
1396
			$result[] = $port;
1397
		}
1398
	}
1399

    
1400
	return array_merge($comments, $result);
1401
}
1402

    
1403
/* returns true if $val is a valid shaper bandwidth value */
1404
function is_valid_shaperbw($val) {
1405
	return (preg_match("/^(\d+(?:\.\d+)?)([MKG]?b|%)$/", $val));
1406
}
1407

    
1408
/* returns true if $test is in the range between $start and $end */
1409
function is_inrange_v4($test, $start, $end) {
1410
	if (!is_ipaddrv4($test) || !is_ipaddrv4($start) || !is_ipaddrv4($end)) {
1411
		return false;
1412
	}
1413

    
1414
	if (ip2ulong($test) <= ip2ulong($end) &&
1415
	    ip2ulong($test) >= ip2ulong($start)) {
1416
		return true;
1417
	}
1418

    
1419
	return false;
1420
}
1421

    
1422
/* returns true if $test is in the range between $start and $end */
1423
function is_inrange_v6($test, $start, $end) {
1424
	if (!is_ipaddrv6($test) || !is_ipaddrv6($start) || !is_ipaddrv6($end)) {
1425
		return false;
1426
	}
1427

    
1428
	if (inet_pton($test) <= inet_pton($end) &&
1429
	    inet_pton($test) >= inet_pton($start)) {
1430
		return true;
1431
	}
1432

    
1433
	return false;
1434
}
1435

    
1436
/* returns true if $test is in the range between $start and $end */
1437
function is_inrange($test, $start, $end) {
1438
	return is_ipaddrv6($test) ? is_inrange_v6($test, $start, $end) : is_inrange_v4($test, $start, $end);
1439
}
1440

    
1441
/**
1442
 * Check if an ethertype is valid
1443
 *
1444
 * @param string $ethertype	The ethertype as hex string
1445
 *
1446
 * @return bool
1447
 */
1448
function is_ethertype(string $ethertype): bool {
1449
	$ethertype = strtolower($ethertype);
1450

    
1451
	if (!str_starts_with($ethertype, '0x')) {
1452
		return (false);
1453
	}
1454

    
1455
	$ethertype = substr($ethertype, 2);
1456

    
1457
	if (!ctype_xdigit($ethertype)) {
1458
		return (false);
1459
	}
1460

    
1461
	return (filter_var(hexdec($ethertype), FILTER_VALIDATE_INT, ['options' => ['min_range' => 0x1, 'max_range' => 0xffff]]));
1462
}
1463

    
1464
function build_vip_list($fif, $family = "all") {
1465
	$list = array('address' => gettext('Interface Address'));
1466

    
1467
	$viplist = get_configured_vip_list($family);
1468
	foreach ($viplist as $vip => $address) {
1469
		if ($fif == get_configured_vip_interface($vip)) {
1470
			$list[$vip] = "$address";
1471
			if (get_vip_descr($address)) {
1472
				$list[$vip] .= " (". get_vip_descr($address) .")";
1473
			}
1474
		} else {
1475
			/**
1476
			 * additional check necessary: Alias-on-CARP VIPs return a "_vip<id>" string
1477
			 * that needs to be resolved via an additional check to see if that VIP should
1478
			 * be displayed here or skipped, as the parent isn't configured on this interface
1479
			 */
1480

    
1481
			$parentif = get_configured_vip_interface($vip);
1482
			if (str_starts_with($parentif, "_vip")) {
1483
				if ($fif == get_configured_vip_interface($parentif)) {
1484
					$list[$vip] = "$address";
1485
					if (get_vip_descr($address)) {
1486
						$list[$vip] .= " (". get_vip_descr($address) .")";
1487
					}
1488
				}
1489
			}
1490
		}
1491
	}
1492

    
1493
	return($list);
1494
}
1495

    
1496
function get_configured_vip_list($family = 'all', $type = VIP_ALL) {
1497
	global $config;
1498

    
1499
	$list = array();
1500
	if (!array_key_exists('virtualip', $config) ||
1501
		!is_array($config['virtualip']) ||
1502
	    !is_array($config['virtualip']['vip']) ||
1503
	    empty($config['virtualip']['vip'])) {
1504
		return ($list);
1505
	}
1506

    
1507
	$viparr = &$config['virtualip']['vip'];
1508
	foreach ($viparr as $vip) {
1509

    
1510
		if ($type == VIP_CARP) {
1511
			if ($vip['mode'] != "carp")
1512
				continue;
1513
		} elseif ($type == VIP_IPALIAS) {
1514
			if ($vip['mode'] != "ipalias")
1515
				continue;
1516
		} else {
1517
			if ($vip['mode'] != "carp" && $vip['mode'] != "ipalias")
1518
				continue;
1519
		}
1520

    
1521
		if ($family == 'all' ||
1522
		    ($family == 'inet' && is_ipaddrv4($vip['subnet'])) ||
1523
		    ($family == 'inet6' && is_ipaddrv6($vip['subnet']))) {
1524
			$list["_vip{$vip['uniqid']}"] = $vip['subnet'];
1525
		}
1526
	}
1527
	return ($list);
1528
}
1529

    
1530
function get_configured_vip($vipinterface = '') {
1531

    
1532
	return (get_configured_vip_detail($vipinterface, 'all', 'vip'));
1533
}
1534

    
1535
function get_configured_vip_interface($vipinterface = '') {
1536

    
1537
	return (get_configured_vip_detail($vipinterface, 'all', 'iface'));
1538
}
1539

    
1540
function get_configured_vip_ipv4($vipinterface = '') {
1541

    
1542
	return (get_configured_vip_detail($vipinterface, 'inet', 'ip'));
1543
}
1544

    
1545
function get_configured_vip_ipv6($vipinterface = '') {
1546

    
1547
	return (get_configured_vip_detail($vipinterface, 'inet6', 'ip'));
1548
}
1549

    
1550
function get_configured_vip_subnetv4($vipinterface = '') {
1551

    
1552
	return (get_configured_vip_detail($vipinterface, 'inet', 'subnet'));
1553
}
1554

    
1555
function get_configured_vip_subnetv6($vipinterface = '') {
1556

    
1557
	return (get_configured_vip_detail($vipinterface, 'inet6', 'subnet'));
1558
}
1559

    
1560
function get_configured_vip_detail($vipinterface = '', $family = 'inet', $what = 'ip') {
1561
	global $config;
1562

    
1563
	if (empty($vipinterface) ||
1564
	    !is_array($config['virtualip']) ||
1565
	    !is_array($config['virtualip']['vip']) ||
1566
	    empty($config['virtualip']['vip'])) {
1567
		return (NULL);
1568
	}
1569

    
1570
	$viparr = &$config['virtualip']['vip'];
1571
	foreach ($viparr as $vip) {
1572
		if ($vip['mode'] != "carp" && $vip['mode'] != "ipalias") {
1573
			continue;
1574
		}
1575

    
1576
		if ($vipinterface != "_vip{$vip['uniqid']}") {
1577
			continue;
1578
		}
1579

    
1580
		switch ($what) {
1581
			case 'subnet':
1582
				if ($family == 'inet' && is_ipaddrv4($vip['subnet']))
1583
					return ($vip['subnet_bits']);
1584
				else if ($family == 'inet6' && is_ipaddrv6($vip['subnet']))
1585
					return ($vip['subnet_bits']);
1586
				break;
1587
			case 'iface':
1588
				return ($vip['interface']);
1589
				break;
1590
			case 'vip':
1591
				return ($vip);
1592
				break;
1593
			case 'ip':
1594
			default:
1595
				if ($family == 'inet' && is_ipaddrv4($vip['subnet'])) {
1596
					return ($vip['subnet']);
1597
				} else if ($family == 'inet6' && is_ipaddrv6($vip['subnet'])) {
1598
					return ($vip['subnet']);
1599
				}
1600
				break;
1601
		}
1602
		break;
1603
	}
1604

    
1605
	return (NULL);
1606
}
1607

    
1608
/* comparison function for sorting by the order in which interfaces are normally created */
1609
function compare_interface_friendly_names($a, $b) {
1610
	if ($a == $b) {
1611
		return 0;
1612
	} else if ($a == 'wan') {
1613
		return -1;
1614
	} else if ($b == 'wan') {
1615
		return 1;
1616
	} else if ($a == 'lan') {
1617
		return -1;
1618
	} else if ($b == 'lan') {
1619
		return 1;
1620
	}
1621

    
1622
	return strnatcmp($a, $b);
1623
}
1624

    
1625
/**
1626
 * Get the configured interfaces list
1627
 *
1628
 * @param bool $with_disabled Include disabled interfaces
1629
 *
1630
 * @return array
1631
 */
1632
function get_configured_interface_list(bool $with_disabled = false) : array
1633
{
1634
	$iflist = [];
1635
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1636
		if ($with_disabled || isset($if_detail['enable'])) {
1637
			$iflist[$if] = $if;
1638
		}
1639
	}
1640

    
1641
	return ($iflist);
1642
}
1643

    
1644
/**
1645
 * Return the configured (and real) interfaces list.
1646
 *
1647
 * @param bool $with_disabled Include disabled interfaces
1648
 *
1649
 * @return array
1650
 */
1651
function get_configured_interface_list_by_realif(bool $with_disabled = false) : array
1652
{
1653
	$iflist = [];
1654
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1655
		if ($with_disabled || isset($if_detail['enable'])) {
1656
			$tmpif = get_real_interface($if);
1657
			if (empty($tmpif)) {
1658
				continue;
1659
			}
1660
			$iflist[$tmpif] = $if;
1661
		}
1662
	}
1663

    
1664
	return ($iflist);
1665
}
1666

    
1667
/**
1668
 * Return the configured interfaces list with their description.
1669
 *
1670
 * @param bool $with_disabled Include disabled interfaces
1671
 *
1672
 * @return array
1673
 */
1674
function get_configured_interface_with_descr(bool $with_disabled = false) : array
1675
{
1676
	global $user_settings;
1677

    
1678
	$iflist = [];
1679
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1680
		if ($with_disabled || isset($if_detail['enable'])) {
1681
			$iflist[$if] = strtoupper(array_get_path($if_detail, 'descr', $if));
1682
		}
1683
	}
1684

    
1685
	if (is_array($user_settings)
1686
	    && array_get_path($user_settings, 'webgui/interfacessort')) {
1687
		asort($iflist);
1688
	}
1689

    
1690
	return ($iflist);
1691
}
1692

    
1693
/*
1694
 *   get_configured_ip_addresses() - Return a list of all configured
1695
 *   IPv4 addresses.
1696
 *
1697
 */
1698
function get_configured_ip_addresses() {
1699
	global $config;
1700

    
1701
	if (!function_exists('get_interface_ip')) {
1702
		require_once("interfaces.inc");
1703
	}
1704
	$ip_array = array();
1705
	$interfaces = get_configured_interface_list();
1706
	if (is_array($interfaces)) {
1707
		foreach ($interfaces as $int) {
1708
			$ipaddr = get_interface_ip($int);
1709
			$ip_array[$int] = $ipaddr;
1710
		}
1711
	}
1712
	$interfaces = get_configured_vip_list('inet');
1713
	if (is_array($interfaces)) {
1714
		foreach ($interfaces as $int => $ipaddr) {
1715
			$ip_array[$int] = $ipaddr;
1716
		}
1717
	}
1718

    
1719
	/* pppoe server */
1720
	if (is_array($config['pppoes']) && is_array($config['pppoes']['pppoe'])) {
1721
		foreach ($config['pppoes']['pppoe'] as $pppoe) {
1722
			if ($pppoe['mode'] == "server") {
1723
				if (is_ipaddr($pppoe['localip'])) {
1724
					$int = "poes". $pppoe['pppoeid'];
1725
					$ip_array[$int] = $pppoe['localip'];
1726
				}
1727
			}
1728
		}
1729
	}
1730

    
1731
	return $ip_array;
1732
}
1733

    
1734
/*
1735
 *   get_configured_ipv6_addresses() - Return a list of all configured
1736
 *   IPv6 addresses.
1737
 *
1738
 */
1739
function get_configured_ipv6_addresses($linklocal_fallback = false) {
1740
	require_once("interfaces.inc");
1741
	$ipv6_array = array();
1742
	$interfaces = get_configured_interface_list();
1743
	if (is_array($interfaces)) {
1744
		foreach ($interfaces as $int) {
1745
			$ipaddrv6 = text_to_compressed_ip6(get_interface_ipv6($int, false, $linklocal_fallback));
1746
			$ipv6_array[$int] = $ipaddrv6;
1747
		}
1748
	}
1749
	$interfaces = get_configured_vip_list('inet6');
1750
	if (is_array($interfaces)) {
1751
		foreach ($interfaces as $int => $ipaddrv6) {
1752
			$ipv6_array[$int] = text_to_compressed_ip6($ipaddrv6);
1753
		}
1754
	}
1755
	return $ipv6_array;
1756
}
1757

    
1758
/*
1759
 *   get_interface_list() - Return a list of all physical interfaces
1760
 *   along with MAC and status.
1761
 *
1762
 *   $mode = "active" - use ifconfig -lu
1763
 *           "media"  - use ifconfig to check physical connection
1764
 *			status (much slower)
1765
 */
1766
function get_interface_list($mode = "active", $keyby = "physical", $vfaces = false) {
1767
	global $config;
1768
	$upints = array();
1769
	/* get a list of virtual interface types */
1770
	if (!$vfaces) {
1771
		$vfaces = array(
1772
				'bridge',
1773
				'ppp',
1774
				'pppoe',
1775
				'poes',
1776
				'pptp',
1777
				'l2tp',
1778
				'sl',
1779
				'gif',
1780
				'gre',
1781
				'faith',
1782
				'lo',
1783
				'ng',
1784
				'_vlan',
1785
				'_wlan',
1786
				'pflog',
1787
				'plip',
1788
				'pfsync',
1789
				'enc',
1790
				'tun',
1791
				'lagg',
1792
				'vip'
1793
		);
1794
	} else {
1795
		$vfaces = array(
1796
				'bridge',
1797
				'poes',
1798
				'sl',
1799
				'faith',
1800
				'lo',
1801
				'ng',
1802
				'_vlan',
1803
				'_wlan',
1804
				'pflog',
1805
				'plip',
1806
				'pfsync',
1807
				'enc',
1808
				'tun',
1809
				'lagg',
1810
				'vip',
1811
				'l2tps'
1812
		);
1813
	}
1814
	switch ($mode) {
1815
		case "active":
1816
			$upints = pfSense_interface_listget(IFF_UP);
1817
			break;
1818
		case "media":
1819
			$intlist = pfSense_interface_listget();
1820
			$ifconfig = [];
1821
			exec("/sbin/ifconfig -a", $ifconfig);
1822
			$ifstatus = preg_grep('/status:/', $ifconfig);
1823
			foreach ($ifstatus as $status) {
1824
				$int = array_shift($intlist);
1825
				if (stristr($status, "active")) {
1826
					$upints[] = $int;
1827
				}
1828
			}
1829
			break;
1830
		default:
1831
			$upints = pfSense_interface_listget();
1832
			break;
1833
	}
1834
	/* build interface list with netstat */
1835
	$linkinfo = [];
1836
	exec("/usr/bin/netstat -inW -f link | awk '{ print $1, $4 }'", $linkinfo);
1837
	array_shift($linkinfo);
1838
	/* build ip address list with netstat */
1839
	$ipinfo = [];
1840
	exec("/usr/bin/netstat -inW -f inet | awk '{ print $1, $4 }'", $ipinfo);
1841
	array_shift($ipinfo);
1842
	foreach ($linkinfo as $link) {
1843
		$friendly = "";
1844
		$alink = explode(" ", $link);
1845
		$ifname = rtrim(trim($alink[0]), '*');
1846
		/* trim out all numbers before checking for vfaces */
1847
		if (!in_array(array_shift(preg_split('/(\d-)*\d$/', $ifname)), $vfaces) &&
1848
		    interface_is_vlan($ifname) == NULL &&
1849
		    interface_is_qinq($ifname) == NULL &&
1850
		    !stristr($ifname, "_wlan") &&
1851
		    !stristr($ifname, "_stf")) {
1852
			$toput = array(
1853
					"mac" => trim($alink[1]),
1854
					"up" => in_array($ifname, $upints)
1855
				);
1856
			foreach ($ipinfo as $ip) {
1857
				$aip = explode(" ", $ip);
1858
				if ($aip[0] == $ifname) {
1859
					$toput['ipaddr'] = $aip[1];
1860
				}
1861
			}
1862
			if (is_array($config['interfaces'])) {
1863
				foreach ($config['interfaces'] as $name => $int) {
1864
					if ($int['if'] == $ifname) {
1865
						$friendly = $name;
1866
					}
1867
				}
1868
			}
1869
			switch ($keyby) {
1870
			case "physical":
1871
				if ($friendly != "") {
1872
					$toput['friendly'] = $friendly;
1873
				}
1874
				$dmesg_arr = array();
1875
				exec("/sbin/dmesg |grep $ifname | head -n1", $dmesg_arr);
1876
				preg_match_all("/<(.*?)>/i", $dmesg_arr[0], $dmesg);
1877
				$toput['dmesg'] = $dmesg[1][0];
1878
				$iflist[$ifname] = $toput;
1879
				break;
1880
			case "ppp":
1881

    
1882
			case "friendly":
1883
				if ($friendly != "") {
1884
					$toput['if'] = $ifname;
1885
					$iflist[$friendly] = $toput;
1886
				}
1887
				break;
1888
			}
1889
		}
1890
	}
1891
	return $iflist;
1892
}
1893

    
1894
function get_lagg_interface_list() {
1895
	global $config;
1896

    
1897
	$plist = array();
1898
	if (isset($config['laggs']['lagg']) && is_array($config['laggs']['lagg'])) {
1899
		foreach ($config['laggs']['lagg'] as $lagg) {
1900
			$lagg['mac'] = get_interface_mac($lagg['laggif']);
1901
			$lagg['islagg'] = true;
1902
			$plist[$lagg['laggif']] = $lagg;
1903
		}
1904
	}
1905

    
1906
	return ($plist);
1907
}
1908

    
1909
/****f* util/log_error
1910
* NAME
1911
*   log_error  - Sends a string to syslog.
1912
* INPUTS
1913
*   $error     - string containing the syslog message.
1914
* RESULT
1915
*   null
1916
******/
1917
function log_error($error) {
1918
	global $g;
1919
	$page = $_SERVER['SCRIPT_NAME'];
1920
	if (empty($page)) {
1921
		$files = get_included_files();
1922
		$page = basename($files[0]);
1923
	}
1924
	syslog(LOG_ERR, "$page: $error");
1925
	if (g_get('debug')) {
1926
		syslog(LOG_WARNING, var_export(debug_backtrace()));
1927
	}
1928
	return;
1929
}
1930

    
1931
/****f* util/log_auth
1932
* NAME
1933
*   log_auth   - Sends a string to syslog as LOG_AUTH facility
1934
* INPUTS
1935
*   $error     - string containing the syslog message.
1936
* RESULT
1937
*   null
1938
******/
1939
function log_auth($error) {
1940
	global $g;
1941
	$page = $_SERVER['SCRIPT_NAME'];
1942
	$level = config_path_enabled('system/webgui', 'quietlogin') ? LOG_NOTICE|LOG_AUTH : LOG_AUTH;
1943
	syslog($level, "{$page}: {$error}");
1944
	if (g_get('debug')) {
1945
		syslog(LOG_WARNING, var_export(debug_backtrace()));
1946
	}
1947
	return;
1948
}
1949

    
1950
/****f* util/exec_command
1951
 * NAME
1952
 *   exec_command - Execute a command and return a string of the result.
1953
 * INPUTS
1954
 *   $command   - String of the command to be executed.
1955
 * RESULT
1956
 *   String containing the command's result.
1957
 * NOTES
1958
 *   This function returns the command's stdout and stderr.
1959
 ******/
1960
function exec_command($command) {
1961
	$output = array();
1962
	exec($command . ' 2>&1', $output);
1963
	return(implode("\n", $output));
1964
}
1965

    
1966
/* wrapper for exec()
1967
   Executes in background or foreground.
1968
   For background execution, returns PID of background process to allow calling code control */
1969
function mwexec($command, $nologentry = false, $clearsigmask = false, $background = false) {
1970
	global $g;
1971
	$retval = 0;
1972

    
1973
	if (g_get('debug')) {
1974
		if (!$_SERVER['REMOTE_ADDR']) {
1975
			echo "mwexec(): $command" . ($background ? " [BG]":"") . "\n";
1976
		}
1977
	}
1978
	if ($clearsigmask) {
1979
		$oldset = array();
1980
		pcntl_sigprocmask(SIG_SETMASK, array(), $oldset);
1981
	}
1982

    
1983
	if ($background) {
1984
		// start background process and return PID
1985
		$retval = exec("/usr/bin/nohup $command > /dev/null 2>&1 & echo $!");
1986
	} else {
1987
		// run in foreground, and (optionally) log if nonzero return
1988
		$outputarray = array();
1989
		exec("$command 2>&1", $outputarray, $retval);
1990
		if (($retval <> 0) && (!$nologentry || isset($config['system']['developerspew']))) {
1991
			log_error(sprintf(gettext("The command '%1\$s' returned exit code '%2\$d', the output was '%3\$s' "), $command, $retval, implode(" ", $outputarray)));
1992
		}
1993
	}
1994

    
1995
	if ($clearsigmask) {
1996
		pcntl_sigprocmask(SIG_SETMASK, $oldset);
1997
	}
1998

    
1999
	return $retval;
2000
}
2001

    
2002
/* wrapper for exec() in background */
2003
function mwexec_bg($command, $clearsigmask = false) {
2004
	return mwexec($command, false, $clearsigmask, true);
2005
}
2006

    
2007
/*
2008
 * Unlink a file, or pattern-match of a file, if it exists
2009
 *
2010
 * If the file/path contains glob() compatible wildcards, all matching files
2011
 * will be unlinked.
2012
 * Any warning/errors are suppressed (e.g. no matching files to delete)
2013
 * If there are matching file(s) and they were all unlinked OK, then return
2014
 * true.  Otherwise return false (the requested file(s) did not exist, or
2015
 * could not be deleted), this allows the caller to know if they were the one
2016
 * to successfully delete the file(s).
2017
 */
2018
function unlink_if_exists($fn) {
2019
	$to_do = glob($fn);
2020
	if (is_array($to_do) && count($to_do) > 0) {
2021
		// Returns an array of boolean indicating if each unlink worked
2022
		$results = @array_map("unlink", $to_do);
2023
		// If there is no false in the array, then all went well
2024
		$result = !in_array(false, $results, true);
2025
	} else {
2026
		$result = @unlink($fn);
2027
	}
2028
	return $result;
2029
}
2030

    
2031
/* make a global alias table (for faster lookups) */
2032
function alias_make_table() {
2033
	global $aliastable;
2034

    
2035
	$aliastable = array();
2036

    
2037
	foreach (config_get_path('aliases/alias', []) as $alias) {
2038
		if (!is_array($alias) || empty($alias)) {
2039
			continue;
2040
		}
2041
		if ($alias['name']) {
2042
			$aliastable[$alias['name']] = $alias['address'];
2043
		}
2044
	}
2045
}
2046

    
2047
/* check if an alias exists */
2048
function is_alias($name) {
2049
	global $aliastable;
2050

    
2051
	return isset($aliastable[$name]);
2052
}
2053

    
2054
function alias_get_type($name) {
2055

    
2056
	foreach (config_get_path('aliases/alias', []) as $alias) {
2057
		if ($name == $alias['name']) {
2058
			return $alias['type'];
2059
		}
2060
	}
2061

    
2062
	return "";
2063
}
2064

    
2065
/* expand a url alias, if necessary */
2066
function alias_expand($name) {
2067
	global $aliastable;
2068
	$urltable_prefix = "/var/db/aliastables/";
2069
	$urltable_filename = $urltable_prefix . $name . ".txt";
2070

    
2071
	if (isset($aliastable[$name])) {
2072
		// alias names cannot be strictly numeric. redmine #4289
2073
		if (is_numericint($name)) {
2074
			return null;
2075
		}
2076
		/*
2077
		 * make sure if it's a url alias, it actually exists.
2078
		 * redmine #5845, #13068
2079
		 */
2080
		foreach (config_get_path('aliases/alias', []) as $alias) {
2081
			if ($alias['name'] == $name) {
2082
				if (in_array($alias['type'], ['url', 'url_ports', 'urltable', "urltable_ports"])) {
2083
					if (is_URL($alias['url']) &&
2084
					    file_exists($urltable_filename) &&
2085
					    !empty(trim(file_get_contents($urltable_filename)))) {
2086
						return "\${$name}";
2087
					} else {
2088
						return null;
2089
					}
2090
				}
2091
			}
2092
		}
2093
		return "\${$name}";
2094
	} else if (is_ipaddr($name) || is_subnet($name) ||
2095
	    is_port_or_range($name)) {
2096
		return "{$name}";
2097
	} else {
2098
		return null;
2099
	}
2100
}
2101

    
2102
function alias_expand_urltable($name) {
2103
	$urltable_prefix = "/var/db/aliastables/";
2104
	$urltable_filename = $urltable_prefix . $name . ".txt";
2105

    
2106
	foreach (config_get_path('aliases/alias', []) as $alias) {
2107
		if (!preg_match("/urltable/i", $alias['type']) ||
2108
		    ($alias['name'] != $name)) {
2109
			continue;
2110
		}
2111

    
2112
		if (is_URL($alias["url"]) && file_exists($urltable_filename)) {
2113
			if (!filesize($urltable_filename)) {
2114
				// file exists, but is empty, try to sync
2115
				send_event("service sync alias {$name}");
2116
			}
2117
			return $urltable_filename;
2118
		} else {
2119
			send_event("service sync alias {$name}");
2120
			break;
2121
		}
2122
	}
2123
	return null;
2124
}
2125

    
2126
/* obtain MAC address given an IP address by looking at the ARP/NDP table */
2127
function arp_get_mac_by_ip($ip, $do_ping = true) {
2128
	unset($macaddr);
2129
	$retval = 1;
2130
	switch (is_ipaddr($ip)) {
2131
		case 4:
2132
			if ($do_ping === true) {
2133
				mwexec("/sbin/ping -c 1 -t 1 " . escapeshellarg($ip), true);
2134
			}
2135
			$macaddr = exec("/usr/sbin/arp -n " . escapeshellarg($ip) . " | /usr/bin/awk '{print $4}'", $output, $retval);
2136
			break;
2137
		case 6:
2138
			if ($do_ping === true) {
2139
				mwexec("/sbin/ping6 -c 1 -X 1 " . escapeshellarg($ip), true);
2140
			}
2141
			$macaddr = exec("/usr/sbin/ndp -n " . escapeshellarg($ip) . " | /usr/bin/awk '{print $2}'", $output, $retval);
2142
			break;
2143
	}
2144
	if ($retval == 0 && is_macaddr($macaddr)) {
2145
		return $macaddr;
2146
	} else {
2147
		return false;
2148
	}
2149
}
2150

    
2151
/* return a fieldname that is safe for xml usage */
2152
function xml_safe_fieldname($fieldname) {
2153
	$replace = array(
2154
	    '/', '-', ' ', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
2155
	    '_', '+', '=', '{', '}', '[', ']', '|', '/', '<', '>', '?',
2156
	    ':', ',', '.', '\'', '\\'
2157
	);
2158
	return strtolower(str_replace($replace, "", $fieldname));
2159
}
2160

    
2161
function mac_format($clientmac) {
2162
	global $config, $cpzone;
2163

    
2164
	$mac = explode(":", $clientmac);
2165
	$mac_format = $cpzone ? $config['captiveportal'][$cpzone]['radmac_format'] : false;
2166

    
2167
	switch ($mac_format) {
2168
		case 'singledash':
2169
			return "$mac[0]$mac[1]$mac[2]-$mac[3]$mac[4]$mac[5]";
2170

    
2171
		case 'ietf':
2172
			return "$mac[0]-$mac[1]-$mac[2]-$mac[3]-$mac[4]-$mac[5]";
2173

    
2174
		case 'cisco':
2175
			return "$mac[0]$mac[1].$mac[2]$mac[3].$mac[4]$mac[5]";
2176

    
2177
		case 'unformatted':
2178
			return "$mac[0]$mac[1]$mac[2]$mac[3]$mac[4]$mac[5]";
2179

    
2180
		default:
2181
			return $clientmac;
2182
	}
2183
}
2184

    
2185
function resolve_retry($hostname, $protocol = 'inet') {
2186
	$retries = 10;
2187
	$numrecords = 1;
2188
	$recresult = array();
2189

    
2190
	switch ($protocol) {
2191
		case 'any':
2192
			$checkproto = 'is_ipaddr';
2193
			$dnsproto = DNS_ANY;
2194
			$dnstype = array('A', 'AAAA');
2195
			break;
2196
		case 'inet6':
2197
			$checkproto = 'is_ipaddrv6';
2198
			$dnsproto = DNS_AAAA;
2199
			$dnstype = array('AAAA');
2200
			break;
2201
		case 'inet':
2202
		default:
2203
			$checkproto = 'is_ipaddrv4';
2204
			$dnsproto = DNS_A;
2205
			$dnstype = array('A');
2206
			break;
2207
	}
2208

    
2209
	for ($i = 0; $i < $retries; $i++) {
2210
		if ($checkproto($hostname)) {
2211
			return $hostname;
2212
		}
2213

    
2214
		$dnsresult = @dns_get_record($hostname, $dnsproto);
2215

    
2216
		if (!empty($dnsresult)) {
2217
			foreach ($dnsresult as $ip) {
2218
				if (is_array($ip)) {
2219
					if (in_array($ip['type'], $dnstype)) {
2220
						if ($checkproto($ip['ip'])) {
2221
							$recresult[] = $ip['ip'];
2222
						}
2223

    
2224
						if ($checkproto($ip['ipv6'])) {
2225
							$recresult[] = $ip['ipv6'];
2226
						}
2227
					}
2228
				}
2229
			}
2230
		}
2231

    
2232
		// Return on success
2233
		if (!empty($recresult)) {
2234
			if ($numrecords == 1) {
2235
				return $recresult[0];
2236
			} else {
2237
				return array_slice($recresult, 0, $numrecords);
2238
			}
2239
		}
2240

    
2241
		usleep(100000);
2242
	}
2243

    
2244
	return false;
2245
}
2246

    
2247
function format_bytes($bytes) {
2248
	if ($bytes >= 1099511627776) {
2249
		return sprintf("%.2f TiB", $bytes/1099511627776);
2250
	} else if ($bytes >= 1073741824) {
2251
		return sprintf("%.2f GiB", $bytes/1073741824);
2252
	} else if ($bytes >= 1048576) {
2253
		return sprintf("%.2f MiB", $bytes/1048576);
2254
	} else if ($bytes >= 1024) {
2255
		return sprintf("%.0f KiB", $bytes/1024);
2256
	} else {
2257
		return sprintf("%d B", $bytes);
2258
	}
2259
}
2260

    
2261
function format_number(int $num, int $precision = 3): string
2262
{
2263
    $units = ['', 'K', 'M', 'G', 'T'];
2264
    for ($i = 0; $num >= 1000; $i++) {
2265
        $num /= 1000;
2266
    }
2267
    return (round($num, $precision) . $units[$i]);
2268
}
2269

    
2270
function unformat_number($formated_num) {
2271
	$num = strtoupper($formated_num);
2272

    
2273
	if ( strpos($num,"T") !== false ) {
2274
		$num = str_replace("T","",$num) * 1000 * 1000 * 1000 * 1000;
2275
	} else if ( strpos($num,"G") !== false ) {
2276
		$num = str_replace("G","",$num) * 1000 * 1000 * 1000;
2277
	} else if ( strpos($num,"M") !== false ) {
2278
		$num = str_replace("M","",$num) * 1000 * 1000;
2279
	} else if ( strpos($num,"K") !== false ) {
2280
		$num = str_replace("K","",$num) * 1000;
2281
	}
2282

    
2283
	return $num;
2284
}
2285

    
2286
function update_filter_reload_status($text, $new=false) {
2287
	global $g;
2288

    
2289
	if ($new) {
2290
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL);
2291
	} else {
2292
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL, FILE_APPEND);
2293
	}
2294
}
2295

    
2296
/****** util/return_dir_as_array
2297
 * NAME
2298
 *   return_dir_as_array - Return a directory's contents as an array.
2299
 * INPUTS
2300
 *   $dir          - string containing the path to the desired directory.
2301
 *   $filter_regex - string containing a regular expression to filter file names. Default empty.
2302
 * RESULT
2303
 *   $dir_array - array containing the directory's contents. This array will be empty if the path specified is invalid.
2304
 ******/
2305
function return_dir_as_array($dir, $filter_regex = '') {
2306
	$dir_array = array();
2307
	if (is_dir($dir)) {
2308
		if ($dh = opendir($dir)) {
2309
			while (($file = readdir($dh)) !== false) {
2310
				if (($file == ".") || ($file == "..")) {
2311
					continue;
2312
				}
2313

    
2314
				if (empty($filter_regex) || preg_match($filter_regex, $file)) {
2315
					array_push($dir_array, $file);
2316
				}
2317
			}
2318
			closedir($dh);
2319
		}
2320
	}
2321
	return $dir_array;
2322
}
2323

    
2324
function run_plugins($directory) {
2325
	/* process packager manager custom rules */
2326
	$files = return_dir_as_array($directory);
2327
	if (is_array($files)) {
2328
		foreach ($files as $file) {
2329
			if (stristr($file, ".sh") == true) {
2330
				mwexec($directory . $file . " start");
2331
			} else if (!is_dir($directory . "/" . $file) && stristr($file, ".inc")) {
2332
				require_once($directory . "/" . $file);
2333
			}
2334
		}
2335
	}
2336
}
2337

    
2338
/*
2339
 *    safe_mkdir($path, $mode = 0755)
2340
 *    create directory if it doesn't already exist and isn't a file!
2341
 */
2342
function safe_mkdir($path, $mode = 0755) {
2343
	if (!is_file($path) && !is_dir($path)) {
2344
		return @mkdir($path, $mode, true);
2345
	} else {
2346
		return false;
2347
	}
2348
}
2349

    
2350
/*
2351
 * get_sysctl($names)
2352
 * Get values of sysctl OID's listed in $names (accepts an array or a single
2353
 * name) and return an array of key/value pairs set for those that exist
2354
 */
2355
function get_sysctl($names) {
2356
	if (empty($names)) {
2357
		return array();
2358
	}
2359

    
2360
	if (is_array($names)) {
2361
		$name_list = array();
2362
		foreach ($names as $name) {
2363
			$name_list[] = escapeshellarg($name);
2364
		}
2365
	} else {
2366
		$name_list = array(escapeshellarg($names));
2367
	}
2368

    
2369
	exec("/sbin/sysctl -iq " . implode(" ", $name_list), $output);
2370
	$values = array();
2371
	foreach ($output as $line) {
2372
		$line = explode(": ", $line, 2);
2373
		if (count($line) == 2) {
2374
			$values[$line[0]] = $line[1];
2375
		}
2376
	}
2377

    
2378
	return $values;
2379
}
2380

    
2381
/*
2382
 * get_single_sysctl($name)
2383
 * Wrapper for get_sysctl() to simplify read of a single sysctl value
2384
 * return the value for sysctl $name or empty string if it doesn't exist
2385
 */
2386
function get_single_sysctl($name) {
2387
	if (empty($name)) {
2388
		return "";
2389
	}
2390

    
2391
	$value = get_sysctl($name);
2392
	if (empty($value) || !isset($value[$name])) {
2393
		return "";
2394
	}
2395

    
2396
	return $value[$name];
2397
}
2398

    
2399
/*
2400
 * set_sysctl($value_list)
2401
 * Set sysctl OID's listed as key/value pairs and return
2402
 * an array with keys set for those that succeeded
2403
 */
2404
function set_sysctl($values) {
2405
	if (empty($values)) {
2406
		return array();
2407
	}
2408

    
2409
	$value_list = array();
2410
	foreach ($values as $key => $value) {
2411
		$value_list[] = escapeshellarg($key) . "=" . escapeshellarg($value);
2412
	}
2413

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

    
2416
	/* Retry individually if failed (one or more read-only) */
2417
	if ($success <> 0 && count($value_list) > 1) {
2418
		foreach ($value_list as $value) {
2419
			exec("/sbin/sysctl -iq " . $value, $output);
2420
		}
2421
	}
2422

    
2423
	$ret = array();
2424
	foreach ($output as $line) {
2425
		$line = explode(": ", $line, 2);
2426
		if (count($line) == 2) {
2427
			$ret[$line[0]] = true;
2428
		}
2429
	}
2430

    
2431
	return $ret;
2432
}
2433

    
2434
/*
2435
 * set_single_sysctl($name, $value)
2436
 * Wrapper to set_sysctl() to make it simple to set only one sysctl
2437
 * returns boolean meaning if it succeeded
2438
 */
2439
function set_single_sysctl($name, $value) {
2440
	if (empty($name)) {
2441
		return false;
2442
	}
2443

    
2444
	$result = set_sysctl(array($name => $value));
2445

    
2446
	if (!isset($result[$name]) || $result[$name] != $value) {
2447
		return false;
2448
	}
2449

    
2450
	return true;
2451
}
2452

    
2453
/*
2454
 *     get_memory()
2455
 *     returns an array listing the amount of
2456
 *     memory installed in the hardware
2457
 *     [0] net memory available for the OS (FreeBSD) after some is taken by BIOS, video or whatever - e.g. 235 MBytes
2458
 *     [1] real (actual) memory of the system, should be the size of the RAM card/s - e.g. 256 MBytes
2459
 */
2460
function get_memory() {
2461
	$physmem = get_single_sysctl("hw.physmem");
2462
	$realmem = get_single_sysctl("hw.realmem");
2463
	/* Ensure $physmem has a sane value */
2464
	if (!is_numericint($physmem) &&
2465
	    is_numericint($realmem)) {
2466
		$physmem = $realmem;
2467
	}
2468
	/* Ensure $realmem has a sane value */
2469
	if (!is_numericint($realmem) &&
2470
	    is_numericint($physmem)) {
2471
		$realmem = $physmem;
2472
	}
2473
	/* If both are invalid, something deeper is wrong */
2474
	if (!is_numericint($physmem) &&
2475
	    is_numericint($realmem)) {
2476
		/* Try checking by pages instead */
2477
		$membypages = (int) get_single_sysctl("vm.stats.vm.v_page_count") * (int) get_single_sysctl("vm.stats.vm.v_page_size");
2478
		if (is_numericint($membypages)) {
2479
			$physmem = $membypages;
2480
			$realmem = $membypages;
2481
		} else {
2482
			/* Everything failed, return zeroes */
2483
			$physmem = 0;
2484
			$realmem = 0;
2485
		}
2486
	}
2487
	/* convert from bytes to megabytes */
2488
	return array(((int) $physmem/1048576), ((int) $realmem/1048576));
2489
}
2490

    
2491
function get_php_default_memory($ARCH) {
2492
	if ($ARCH == "amd64") {
2493
		return 512;
2494
	} else {
2495
		return 128;
2496
	}
2497
}
2498

    
2499
function get_php_max_memory() {
2500
	return intval(get_memory()[0] - 1024);
2501
}
2502

    
2503
function mute_kernel_msgs() {
2504
	if (config_path_enabled('system','enableserial')) {
2505
		return;
2506
	}
2507
	exec("/sbin/conscontrol mute on");
2508
}
2509

    
2510
function unmute_kernel_msgs() {
2511
	exec("/sbin/conscontrol mute off");
2512
}
2513

    
2514
function start_devd() {
2515
	global $g;
2516

    
2517
	/* Generate hints for the kernel loader. */
2518
	$module_paths = explode(";", get_single_sysctl("kern.module_path"));
2519
	foreach ($module_paths as $path) {
2520
		if (!is_dir($path) ||
2521
		    (($files = scandir($path)) == false)) {
2522
			continue;
2523
		}
2524
		$found = false;
2525
		foreach ($files as $file) {
2526
			if (strlen($file) > 3 &&
2527
			    strcasecmp(substr($file, -3), ".ko") == 0) {
2528
				$found = true;
2529
				break;
2530
			}
2531
		}
2532
		if ($found == false) {
2533
			continue;
2534
		}
2535
		$_gb = exec("/usr/sbin/kldxref $path");
2536
		unset($_gb);
2537
	}
2538

    
2539
	/* Use the undocumented -q options of devd to quiet its log spamming */
2540
	$_gb = exec("/sbin/devd -q -f /etc/{$g['product_name']}-devd.conf");
2541
	sleep(1);
2542
	unset($_gb);
2543
}
2544

    
2545
function is_interface_vlan_mismatch() {
2546
	foreach (config_get_path('vlans/vlan', []) as $vlan) {
2547
		if (substr($vlan['if'], 0, 4) == "lagg") {
2548
			return false;
2549
		}
2550
		if (does_interface_exist($vlan['if']) == false) {
2551
			return true;
2552
		}
2553
	}
2554

    
2555
	return false;
2556
}
2557

    
2558
function is_interface_mismatch() {
2559
	global $config, $g;
2560

    
2561
	$do_assign = false;
2562
	$i = 0;
2563
	$missing_interfaces = array();
2564
	if (is_array($config['interfaces'])) {
2565
		foreach ($config['interfaces'] as $ifcfg) {
2566
			if (interface_is_vlan($ifcfg['if']) != NULL ||
2567
			    interface_is_qinq($ifcfg['if']) != NULL ||
2568
			    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'])) {
2569
				// Do not check these interfaces.
2570
				$i++;
2571
				continue;
2572
			} else if (does_interface_exist($ifcfg['if']) == false) {
2573
				$missing_interfaces[] = $ifcfg['if'];
2574
				$do_assign = true;
2575
			} else {
2576
				$i++;
2577
			}
2578
		}
2579
	}
2580

    
2581
	/* VLAN/QinQ-only interface mismatch detection
2582
	 * see https://redmine.pfsense.org/issues/12170 */
2583
	init_config_arr(array('vlans', 'vlan'));
2584
	foreach ($config['vlans']['vlan'] as $vlan) {
2585
		if (!does_interface_exist($vlan['if']) &&
2586
		    !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'])) {
2587
			$missing_interfaces[] = $vlan['if'];
2588
			$do_assign = true;
2589
		}
2590
	}
2591
	init_config_arr(array('qinqs', 'qinqentry'));
2592
	foreach ($config['qinqs']['qinqentry'] as $qinq) {
2593
		if (!does_interface_exist($qinq['if']) &&
2594
		    !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'])) {
2595
			$missing_interfaces[] = $qinq['if'];
2596
			$do_assign = true;
2597
		}
2598
	}
2599

    
2600
	if (file_exists("{$g['tmp_path']}/assign_complete")) {
2601
		$do_assign = false;
2602
	}
2603

    
2604
	if (!empty($missing_interfaces) && $do_assign) {
2605
		file_put_contents("{$g['tmp_path']}/missing_interfaces", implode(' ', $missing_interfaces));
2606
	} else {
2607
		@unlink("{$g['tmp_path']}/missing_interfaces");
2608
	}
2609

    
2610
	return $do_assign;
2611
}
2612

    
2613
/* sync carp entries to other firewalls */
2614
function carp_sync_client() {
2615
	send_event("filter sync");
2616
}
2617

    
2618
/****f* util/isAjax
2619
 * NAME
2620
 *   isAjax - reports if the request is driven from prototype
2621
 * INPUTS
2622
 *   none
2623
 * RESULT
2624
 *   true/false
2625
 ******/
2626
function isAjax() {
2627
	return isset ($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
2628
}
2629

    
2630
/****f* util/timeout
2631
 * NAME
2632
 *   timeout - console input with timeout countdown. Note: erases 2 char of screen for timer. Leave space.
2633
 * INPUTS
2634
 *   optional, seconds to wait before timeout. Default 9 seconds.
2635
 * RESULT
2636
 *   returns 1 char of user input or null if no input.
2637
 ******/
2638
function timeout($timer = 9) {
2639
	while (!isset($key)) {
2640
		if ($timer >= 9) {
2641
			echo chr(8) . chr(8) . ($timer == 9 ? chr(32) : null) . "{$timer}";
2642
		} else {
2643
			echo chr(8). "{$timer}";
2644
		}
2645
		`/bin/stty -icanon min 0 time 25`;
2646
		$key = trim(`KEY=\`dd count=1 2>/dev/null\`; echo \$KEY`);
2647
		`/bin/stty icanon`;
2648
		if ($key == '') {
2649
			unset($key);
2650
		}
2651
		$timer--;
2652
		if ($timer == 0) {
2653
			break;
2654
		}
2655
	}
2656
	return $key;
2657
}
2658

    
2659
/****f* util/msort
2660
 * NAME
2661
 *   msort - sort array
2662
 * INPUTS
2663
 *   $array to be sorted, field to sort by, direction of sort
2664
 * RESULT
2665
 *   returns newly sorted array
2666
 ******/
2667
function msort($array, $id = "id", $sort_ascending = true) {
2668
	$temp_array = array();
2669
	if (!is_array($array)) {
2670
		return $temp_array;
2671
	}
2672
	while (count($array)>0) {
2673
		$lowest_id = 0;
2674
		$index = 0;
2675
		foreach ($array as $item) {
2676
			if (isset($item[$id])) {
2677
				if ($array[$lowest_id][$id]) {
2678
					if (strtolower($item[$id]) < strtolower($array[$lowest_id][$id])) {
2679
						$lowest_id = $index;
2680
					}
2681
				}
2682
			}
2683
			$index++;
2684
		}
2685
		$temp_array[] = $array[$lowest_id];
2686
		$array = array_merge(array_slice($array, 0, $lowest_id), array_slice($array, $lowest_id + 1));
2687
	}
2688
	if ($sort_ascending) {
2689
		return $temp_array;
2690
	} else {
2691
		return array_reverse($temp_array);
2692
	}
2693
}
2694

    
2695
/****f* util/is_URL
2696
 * NAME
2697
 *   is_URL
2698
 * INPUTS
2699
 *   $url: string to check
2700
 *   $httponly: Only allow HTTP or HTTPS scheme
2701
 * RESULT
2702
 *   Returns true if item is a URL
2703
 ******/
2704
function is_URL($url, $httponly = false) {
2705
	if (filter_var($url, FILTER_VALIDATE_URL)) {
2706
		if ($httponly) {
2707
			return in_array(strtolower(parse_url($url, PHP_URL_SCHEME)), ['http', 'https']);
2708
		}
2709
		return true;
2710
	}
2711
	return false;
2712
}
2713

    
2714
function is_file_included($file = "") {
2715
	$files = get_included_files();
2716
	if (in_array($file, $files)) {
2717
		return true;
2718
	}
2719

    
2720
	return false;
2721
}
2722

    
2723
/*
2724
 * Replace a value on a deep associative array using regex
2725
 */
2726
function array_replace_values_recursive($data, $match, $replace) {
2727
	if (empty($data)) {
2728
		return $data;
2729
	}
2730

    
2731
	if (is_string($data)) {
2732
		$data = preg_replace("/{$match}/", $replace, $data);
2733
	} else if (is_array($data)) {
2734
		foreach ($data as $k => $v) {
2735
			$data[$k] = array_replace_values_recursive($v, $match, $replace);
2736
		}
2737
	}
2738

    
2739
	return $data;
2740
}
2741

    
2742
/*
2743
	This function was borrowed from a comment on PHP.net at the following URL:
2744
	https://www.php.net/manual/en/function.array-merge-recursive.php#73843
2745
 */
2746
function array_merge_recursive_unique($array0, $array1) {
2747

    
2748
	$arrays = func_get_args();
2749
	$remains = $arrays;
2750

    
2751
	// We walk through each arrays and put value in the results (without
2752
	// considering previous value).
2753
	$result = array();
2754

    
2755
	// loop available array
2756
	foreach ($arrays as $array) {
2757

    
2758
		// The first remaining array is $array. We are processing it. So
2759
		// we remove it from remaining arrays.
2760
		array_shift($remains);
2761

    
2762
		// We don't care non array param, like array_merge since PHP 5.0.
2763
		if (is_array($array)) {
2764
			// Loop values
2765
			foreach ($array as $key => $value) {
2766
				if (is_array($value)) {
2767
					// we gather all remaining arrays that have such key available
2768
					$args = array();
2769
					foreach ($remains as $remain) {
2770
						if (array_key_exists($key, $remain)) {
2771
							array_push($args, $remain[$key]);
2772
						}
2773
					}
2774

    
2775
					if (count($args) > 2) {
2776
						// put the recursion
2777
						$result[$key] = call_user_func_array(__FUNCTION__, $args);
2778
					} else {
2779
						foreach ($value as $vkey => $vval) {
2780
							if (!is_array($result[$key])) {
2781
								$result[$key] = array();
2782
							}
2783
							$result[$key][$vkey] = $vval;
2784
						}
2785
					}
2786
				} else {
2787
					// simply put the value
2788
					$result[$key] = $value;
2789
				}
2790
			}
2791
		}
2792
	}
2793
	return $result;
2794
}
2795

    
2796

    
2797
/*
2798
 * converts a string like "a,b,c,d"
2799
 * into an array like array("a" => "b", "c" => "d")
2800
 */
2801
function explode_assoc($delimiter, $string) {
2802
	$array = explode($delimiter, $string);
2803
	$result = array();
2804
	$numkeys = floor(count($array) / 2);
2805
	for ($i = 0; $i < $numkeys; $i += 1) {
2806
		$result[$array[$i * 2]] = $array[$i * 2 + 1];
2807
	}
2808
	return $result;
2809
}
2810

    
2811
/*
2812
 * Given a string of text with some delimiter, look for occurrences
2813
 * of some string and replace all of those.
2814
 * $text - the text string (e.g. "abc,defg,x123,ipv4,xyz")
2815
 * $delimiter - the delimiter (e.g. ",")
2816
 * $element - the element to match (e.g. "defg")
2817
 * $replacement - the string to replace it with (e.g. "42")
2818
 * Returns the resulting delimited string (e.g. "abc,42,x123,ipv4,xyz")
2819
 */
2820
function replace_element_in_list($text, $delimiter, $element, $replacement) {
2821
	$textArray = explode($delimiter, $text);
2822
	while (($entry = array_search($element, $textArray)) !== false) {
2823
		$textArray[$entry] = $replacement;
2824
	}
2825
	return implode(',', $textArray);
2826
}
2827

    
2828
/* Return system's route table */
2829
function route_table() {
2830
	exec("/usr/bin/netstat --libxo json -nWr", $rawdata, $rc);
2831

    
2832
	if ($rc != 0) {
2833
		return array();
2834
	}
2835

    
2836
	$netstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
2837
	$netstatarr = $netstatarr['statistics']['route-information']
2838
	    ['route-table']['rt-family'];
2839

    
2840
	$result = array();
2841
	$result['inet'] = array();
2842
	$result['inet6'] = array();
2843
	foreach ($netstatarr as $item) {
2844
		if ($item['address-family'] == 'Internet') {
2845
			$result['inet'] = $item['rt-entry'];
2846
		} else if ($item['address-family'] == 'Internet6') {
2847
			$result['inet6'] = $item['rt-entry'];
2848
		}
2849
	}
2850
	unset($netstatarr);
2851

    
2852
	return $result;
2853
}
2854

    
2855
/* check if route is static (not BGP/OSPF) */
2856
function is_static_route($target, $ipprotocol = '') {
2857
	if (is_v4($target) || (($target == 'default') && ($ipprotocol == 'inet'))) {
2858
		$inet = '4';
2859
	} elseif (is_v6($target) || (($target == 'default') && ($ipprotocol == 'inet6'))) {
2860
		$inet = '6';
2861
	} else {
2862
		return false;
2863
	}
2864

    
2865
	if (exec("/sbin/route -n{$inet} get " . escapeshellarg($target) . " 2>/dev/null | egrep 'flags: <.*STATIC.*>'")) {
2866
		return true;
2867
	}
2868

    
2869
	return false;
2870
}
2871

    
2872
/* Get static route for specific destination */
2873
function route_get($target, $ipprotocol = '', $useroute = false) {
2874
	global $config;
2875

    
2876
	if (!empty($ipprotocol)) {
2877
		$family = $ipprotocol;
2878
	} else if (is_v4($target)) {
2879
		$family = 'inet';
2880
	} else if (is_v6($target)) {
2881
		$family = 'inet6';
2882
	}
2883

    
2884
	if (empty($family)) {
2885
		return array();
2886
	}
2887

    
2888
	if ($useroute) {
2889
		if ($family == 'inet') {
2890
			$inet = '4';
2891
		} else {
2892
			$inet = '6';
2893
		}
2894
		$interface = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/interface:/{print $2}'");
2895
		if (empty($interface)) {
2896
			return array();
2897
		} elseif ($interface == 'lo0') {
2898
			// interface assigned IP address
2899
			foreach (array_keys($config['interfaces']) as $intf) {
2900
				if ((($inet == '4') && (get_interface_ip($intf) == $target)) ||
2901
				    (($inet == '6') && (get_interface_ipv6($intf) == $target))) {
2902
					$interface = convert_friendly_interface_to_real_interface_name($intf);
2903
					$gateway = $interface;
2904
					break;
2905
				}
2906
			}
2907
		} else {
2908
			$gateway = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/gateway:/{print $2}'");
2909
			if (!$gateway) {
2910
				// non-local gateway
2911
				$gateway = get_interface_mac($interface);
2912
			}
2913
		}
2914
		$result[] = array('gateway' => $gateway, 'interface-name' => $interface);
2915
	} else {
2916
		$rtable = route_table();
2917
		if (empty($rtable)) {
2918
			return array();
2919
		}
2920

    
2921
		$result = array();
2922
		foreach ($rtable[$family] as $item) {
2923
			if ($item['destination'] == $target ||
2924
			    ip_in_subnet($target, $item['destination'])) {
2925
				$result[] = $item;
2926
			}
2927
		}
2928
	}
2929

    
2930
	return $result;
2931
}
2932

    
2933
/* Get default route */
2934
function route_get_default($ipprotocol) {
2935
	if (empty($ipprotocol) || ($ipprotocol != 'inet' &&
2936
	    $ipprotocol != 'inet6')) {
2937
		return '';
2938
	}
2939

    
2940
	$route = route_get('default', $ipprotocol, true);
2941

    
2942
	if (empty($route)) {
2943
		return '';
2944
	}
2945

    
2946
	if (!isset($route[0]['gateway'])) {
2947
		return '';
2948
	}
2949

    
2950
	return $route[0]['gateway'];
2951
}
2952

    
2953
/* Delete a static route */
2954
function route_del($target, $ipprotocol = '') {
2955
	global $config;
2956

    
2957
	if (empty($target)) {
2958
		return;
2959
	}
2960

    
2961
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
2962
	    $ipprotocol != 'inet6') {
2963
		return false;
2964
	}
2965

    
2966
	$route = route_get($target, $ipprotocol, true);
2967

    
2968
	if (empty($route)) {
2969
		return;
2970
	}
2971

    
2972
	$target_prefix = '';
2973
	if (is_subnet($target)) {
2974
		$target_prefix = '-net';
2975
	} else if (is_ipaddr($target)) {
2976
		$target_prefix = '-host';
2977
	}
2978

    
2979
	if (!empty($ipprotocol)) {
2980
		$target_prefix .= " -{$ipprotocol}";
2981
	} else if (is_v6($target)) {
2982
		$target_prefix .= ' -inet6';
2983
	} else if (is_v4($target)) {
2984
		$target_prefix .= ' -inet';
2985
	}
2986

    
2987
	foreach ($route as $item) {
2988
		if (substr($item['gateway'], 0, 5) == 'link#') {
2989
			continue;
2990
		}
2991

    
2992
		if (is_macaddr($item['gateway'])) {
2993
			$gw = '-iface ' . $item['interface-name'];
2994
		} else {
2995
			$gw = $item['gateway'];
2996
		}
2997

    
2998
		exec(escapeshellcmd("/sbin/route del {$target_prefix} " .
2999
		    "{$target} {$gw}"), $output, $rc);
3000

    
3001
		if (isset($config['system']['route-debug'])) {
3002
			log_error("ROUTING debug: " . microtime() .
3003
			    " - DEL RC={$rc} - {$target} - gw: " . $gw);
3004
			file_put_contents("/dev/console", "\n[" . getmypid() .
3005
			    "] ROUTE DEL: {$target_prefix} {$target} {$gw} " .
3006
			    "result: {$rc}");
3007
		}
3008
	}
3009
}
3010

    
3011
/*
3012
 * Add static route.  If it already exists, remove it and re-add
3013
 *
3014
 * $target - IP, subnet or 'default'
3015
 * $gw     - gateway address
3016
 * $iface  - Network interface
3017
 * $args   - Extra arguments for /sbin/route
3018
 * $ipprotocol - 'inet' or 'inet6'.  Mandatory when $target == 'default'
3019
 *
3020
 */
3021
function route_add_or_change($target, $gw, $iface = '', $args = '',
3022
    $ipprotocol = '') {
3023
	global $config;
3024

    
3025
	if (empty($target) || (empty($gw) && empty($iface))) {
3026
		return false;
3027
	}
3028

    
3029
	if ($target == 'default' && empty($ipprotocol)) {
3030
		return false;
3031
	}
3032

    
3033
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
3034
	    $ipprotocol != 'inet6') {
3035
		return false;
3036
	}
3037

    
3038
	/* use '-host' for IPv6 /128 routes, see https://redmine.pfsense.org/issues/11594 */
3039
	if (is_subnetv4($target) || (is_subnetv6($target) && (subnet_size($target) > 1))) {
3040
		$target_prefix = '-net';
3041
	} else if (is_ipaddr($target)) {
3042
		$target_prefix = '-host';
3043
	}
3044

    
3045
	if (!empty($ipprotocol)) {
3046
		$target_prefix .= " -{$ipprotocol}";
3047
	} else if (is_v6($target)) {
3048
		$target_prefix .= ' -inet6';
3049
	} else if (is_v4($target)) {
3050
		$target_prefix .= ' -inet';
3051
	}
3052

    
3053
	/* If there is another route to the same target, remove it */
3054
	route_del($target, $ipprotocol);
3055

    
3056
	$params = '';
3057
	if (!empty($iface) && does_interface_exist($iface)) {
3058
		$params .= " -iface {$iface}";
3059
	}
3060
	if (is_ipaddr($gw)) {
3061
		/* set correct linklocal gateway address,
3062
		 * see https://redmine.pfsense.org/issues/11713
3063
		 * and https://redmine.pfsense.org/issues/11806 */
3064
		if (is_ipaddrv6($gw) && is_linklocal($gw) && empty(get_ll_scope($gw))) {
3065
			$routeget = route_get($gw, 'inet6', true);
3066
			$gw .= "%" . $routeget[0]['interface-name'];
3067
		}
3068
		$params .= " " . $gw;
3069
	}
3070

    
3071
	if (empty($params)) {
3072
		log_error("route_add_or_change: Invalid gateway {$gw} and/or " .
3073
		    "network interface {$iface}");
3074
		return false;
3075
	}
3076

    
3077
	exec(escapeshellcmd("/sbin/route add {$target_prefix} " .
3078
	    "{$target} {$args} {$params}"), $output, $rc);
3079

    
3080
	if (isset($config['system']['route-debug'])) {
3081
		log_error("ROUTING debug: " . microtime() .
3082
		    " - ADD RC={$rc} - {$target} {$args}");
3083
		file_put_contents("/dev/console", "\n[" . getmypid() .
3084
		    "] ROUTE ADD: {$target_prefix} {$target} {$args} " .
3085
		    "{$params} result: {$rc}");
3086
	}
3087

    
3088
	return ($rc == 0);
3089
}
3090

    
3091
function set_ipv6routes_mtu($interface, $mtu) {
3092
	global $config;
3093

    
3094
	$ipv6mturoutes = array();
3095
	$if = convert_real_interface_to_friendly_interface_name($interface);
3096
	if (!$config['interfaces'][$if]['ipaddrv6']) {
3097
		return;
3098
	}
3099
	$a_gateways = return_gateways_array();
3100
	$a_staticroutes = get_staticroutes(false, false, true);
3101
	foreach ($a_gateways as $gate) {
3102
		foreach ($a_staticroutes as $sroute) {
3103
			if (($gate['interface'] == $interface) &&
3104
			    ($sroute['gateway'] == $gate['name']) &&
3105
			    (is_ipaddrv6($gate['gateway']))) {
3106
				$tgt = $sroute['network'];
3107
				$gateway = $gate['gateway'];
3108
				$ipv6mturoutes[$tgt] = $gateway;
3109
			}
3110
		}
3111
		if (($gate['interface'] == $interface) &&
3112
		    $gate['isdefaultgw'] && is_ipaddrv6($gate['gateway'])) {
3113
			$tgt = "default";
3114
			$gateway = $gate['gateway'];
3115
			$ipv6mturoutes[$tgt] = $gateway;
3116
		}
3117
	}
3118
	foreach ($ipv6mturoutes as $tgt => $gateway) {
3119
		mwexec("/sbin/route change -6 -mtu " . escapeshellarg($mtu) .
3120
		    " " . escapeshellarg($tgt) . " " .
3121
		    escapeshellarg($gateway));
3122
	}
3123
}
3124

    
3125
function alias_to_subnets_recursive($name, $returnhostnames = false) {
3126
	global $aliastable;
3127
	$result = array();
3128
	if (!isset($aliastable[$name])) {
3129
		return $result;
3130
	}
3131
	$subnets = preg_split('/\s+/', $aliastable[$name]);
3132
	foreach ($subnets as $net) {
3133
		if (is_alias($net)) {
3134
			$sub = alias_to_subnets_recursive($net, $returnhostnames);
3135
			$result = array_merge($result, $sub);
3136
			continue;
3137
		} elseif (!is_subnet($net)) {
3138
			if (is_ipaddrv4($net)) {
3139
				$net .= "/32";
3140
			} else if (is_ipaddrv6($net)) {
3141
				$net .= "/128";
3142
			} else if ($returnhostnames === false || !is_fqdn($net)) {
3143
				continue;
3144
			}
3145
		}
3146
		$result[] = $net;
3147
	}
3148
	return $result;
3149
}
3150

    
3151
function get_staticroutes($returnsubnetsonly = false, $returnhostnames = false, $returnenabledroutesonly = false) {
3152
	global $config;
3153

    
3154
	/* Bail if there are no routes, but return an array always so callers don't have to check. */
3155
	init_config_arr(array('staticroutes', 'route'));
3156
	if (empty($config['staticroutes']['route'])) {
3157
		return array();
3158
	}
3159

    
3160
	$allstaticroutes = array();
3161
	$allsubnets = array();
3162
	/* Loop through routes and expand aliases as we find them. */
3163
	foreach ($config['staticroutes']['route'] as $route) {
3164
		if ($returnenabledroutesonly && isset($route['disabled'])) {
3165
			continue;
3166
		}
3167

    
3168
		if (is_alias($route['network'])) {
3169
			foreach (alias_to_subnets_recursive($route['network'], $returnhostnames) as $net) {
3170
				$temproute = $route;
3171
				$temproute['network'] = $net;
3172
				$allstaticroutes[] = $temproute;
3173
				$allsubnets[] = $net;
3174
			}
3175
		} elseif (is_subnet($route['network'])) {
3176
			$allstaticroutes[] = $route;
3177
			$allsubnets[] = $route['network'];
3178
		}
3179
	}
3180
	if ($returnsubnetsonly) {
3181
		return $allsubnets;
3182
	} else {
3183
		return $allstaticroutes;
3184
	}
3185
}
3186

    
3187
/****f* util/get_alias_list
3188
 * NAME
3189
 *   get_alias_list - Provide a list of aliases.
3190
 * INPUTS
3191
 *   $type          - Optional, can be a string or array specifying what type(s) of aliases you need.
3192
 * RESULT
3193
 *   Array containing list of aliases.
3194
 *   If $type is unspecified, all aliases are returned.
3195
 *   If $type is a string, all aliases of the type specified in $type are returned.
3196
 *   If $type is an array, all aliases of any type specified in any element of $type are returned.
3197
 */
3198
function get_alias_list($type = null) {
3199
	$result = array();
3200
	foreach (config_get_path('aliases/alias', []) as $alias) {
3201
		if ($type === null) {
3202
			$result[] = $alias['name'];
3203
		} else if (is_array($type)) {
3204
			if (in_array($alias['type'], $type)) {
3205
				$result[] = $alias['name'];
3206
			}
3207
		} else if ($type === $alias['type']) {
3208
			$result[] = $alias['name'];
3209
		}
3210
	}
3211
	return $result;
3212
}
3213

    
3214
/* Returns the current alias contents sorted by name in case insensitive and
3215
 * natural order.
3216
 * https://redmine.pfsense.org/issues/14015 */
3217
function get_sorted_aliases() {
3218
	$aliases = config_get_path('aliases/alias', []);
3219
	uasort($aliases, function ($a, $b) { return strnatcasecmp($a['name'], $b['name']); });
3220
	return $aliases;
3221
}
3222

    
3223
/* returns an array consisting of every element of $haystack that is not equal to $needle. */
3224
function array_exclude($needle, $haystack) {
3225
	$result = array();
3226
	if (is_array($haystack)) {
3227
		foreach ($haystack as $thing) {
3228
			if ($needle !== $thing) {
3229
				$result[] = $thing;
3230
			}
3231
		}
3232
	}
3233
	return $result;
3234
}
3235

    
3236
/* Define what is preferred, IPv4 or IPv6 */
3237
function prefer_ipv4_or_ipv6() {
3238
	if (config_path_enabled('system', 'prefer_ipv4')) {
3239
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv4");
3240
	} else {
3241
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv6");
3242
	}
3243
}
3244

    
3245
/* Redirect to page passing parameters via POST */
3246
function post_redirect($page, $params) {
3247
	if (!is_array($params)) {
3248
		return;
3249
	}
3250

    
3251
	print "<html><body><form action=\"{$page}\" name=\"formredir\" method=\"post\">\n";
3252
	foreach ($params as $key => $value) {
3253
		print "<input type=\"hidden\" name=\"{$key}\" value=\"{$value}\" />\n";
3254
	}
3255
	print "</form>\n";
3256
	print "<script type=\"text/javascript\">\n";
3257
	print "//<![CDATA[\n";
3258
	print "document.formredir.submit();\n";
3259
	print "//]]>\n";
3260
	print "</script>\n";
3261
	print "</body></html>\n";
3262
}
3263

    
3264
/* Locate disks that can be queried for S.M.A.R.T. data. */
3265
function get_smart_drive_list() {
3266
	/* SMART supports some disks directly, and some controllers directly,
3267
	 * See https://redmine.pfsense.org/issues/9042 */
3268
	$supported_disk_types = array("ad", "da", "ada");
3269
	$supported_controller_types = array("nvme");
3270
	$disk_list = explode(" ", get_single_sysctl("kern.disks"));
3271
	foreach ($disk_list as $id => $disk) {
3272
		// We only want certain kinds of disks for S.M.A.R.T.
3273
		// 1 is a match, 0 is no match, False is any problem processing the regex
3274
		if (preg_match("/^(" . implode("|", $supported_disk_types) . ").*[0-9]{1,2}$/", $disk) !== 1) {
3275
			unset($disk_list[$id]);
3276
			continue;
3277
		}
3278
	}
3279
	foreach ($supported_controller_types as $controller) {
3280
		$devices = glob("/dev/{$controller}*");
3281
		if (!is_array($devices)) {
3282
			continue;
3283
		}
3284
		foreach ($devices as $device) {
3285
			$disk_list[] = basename($device);
3286
		}
3287
	}
3288
	sort($disk_list);
3289
	return $disk_list;
3290
}
3291

    
3292
// Validate a network address
3293
//	$addr: the address to validate
3294
//	$type: IPV4|IPV6|IPV4V6
3295
//	$label: the label used by the GUI to display this value. Required to compose an error message
3296
//	$err_msg: pointer to the callers error message array so that error messages can be added to it here
3297
//	$alias: are aliases permitted for this address?
3298
// Returns:
3299
//	IPV4 - if $addr is a valid IPv4 address
3300
//	IPV6 - if $addr is a valid IPv6 address
3301
//	ALIAS - if $alias=true and $addr is an alias
3302
//	false - otherwise
3303

    
3304
function validateipaddr(&$addr, $type, $label, &$err_msg, $alias=false) {
3305
	switch ($type) {
3306
		case IPV4:
3307
			if (is_ipaddrv4($addr)) {
3308
				return IPV4;
3309
			} else if ($alias) {
3310
				if (is_alias($addr)) {
3311
					return ALIAS;
3312
				} else {
3313
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address or alias."), $label);
3314
					return false;
3315
				}
3316
			} else {
3317
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address."), $label);
3318
				return false;
3319
			}
3320
		break;
3321
		case IPV6:
3322
			if (is_ipaddrv6($addr)) {
3323
				$addr = strtolower($addr);
3324
				return IPV6;
3325
			} else if ($alias) {
3326
				if (is_alias($addr)) {
3327
					return ALIAS;
3328
				} else {
3329
					$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address or alias."), $label);
3330
					return false;
3331
				}
3332
			} else {
3333
				$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address."), $label);
3334
				return false;
3335
			}
3336
		break;
3337
		case IPV4V6:
3338
			if (is_ipaddrv6($addr)) {
3339
				$addr = strtolower($addr);
3340
				return IPV6;
3341
			} else if (is_ipaddrv4($addr)) {
3342
				return IPV4;
3343
			} else if ($alias) {
3344
				if (is_alias($addr)) {
3345
					return ALIAS;
3346
				} else {
3347
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address or alias."), $label);
3348
					return false;
3349
				}
3350
			} else {
3351
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address."), $label);
3352
				return false;
3353
			}
3354
		break;
3355
	}
3356

    
3357
	return false;
3358
}
3359

    
3360
/* From DUID configuration inputs, format a string that looks (more) like the expected raw DUID format:
3361
 * 1) For DUIDs entered as a known DUID type, convert to a hexstring and prepend the DUID number, after having done the following:
3362
 *     a) For DUID-LLT and DUID-EN, convert the time/enterprise ID input to hex and append the link-layer address/identifier input.
3363
 *     b) For DUID-LLT and DUID-LL, prepend a hardware type of 1.
3364
 *     c) For DUID-UUID, remove any "-".
3365
 * 2) Replace any remaining "-" with ":".
3366
 * 3) If any components are input with just a single char (hex digit hopefully), put a "0" in front.
3367
 * 4) The first two components should be a 16-bit integer (little- or big-endian, depending on the current machine type) that
3368
 *    is equal to the number of other components. If not, prepend this as "nn:00" (all pfSense builds are little-endian).
3369
 *    This is convenience, because the DUID reported by dhcp6c in logs does not include this count, which corresponds to the
3370
 *    option-len field of DHCPv6's OPTION_CLIENTID option.
3371
 *
3372
 * The final result should be closer to:
3373
 *
3374
 * "nn:00:00:0n:nn:nn:nn:..."
3375
 *
3376
 * This function does not validate the input. is_duid() will do validation.
3377
 */
3378
function format_duid($duidtype, $duidpt1, $duidpt2=null) {
3379
	if ($duidpt2)
3380
		$duidpt1 = implode(':', str_split(str_pad(dechex($duidpt1), 8, '0', STR_PAD_LEFT), 2)) . ':' . $duidpt2;
3381

    
3382
	/* Make hexstrings */
3383
	if ($duidtype) {
3384
		switch ($duidtype) {
3385
		/* Add a hardware type to DUID-LLT and DUID-LL; assume Ethernet */
3386
		case 1:
3387
		case 3:
3388
			$duidpt1 = '00:01:' . $duidpt1;
3389
			break;
3390
		/* Remove '-' from given UUID and insert ':' every 2 characters */
3391
		case 4:
3392
			$duidpt1 = implode(':', str_split(str_replace('-', '', $duidpt1), 2));
3393
			break;
3394
		default:
3395
		}
3396
		$duidpt1 = '00:0' . $duidtype . ':' . $duidpt1;
3397
	}
3398

    
3399
	$values = explode(':', strtolower(str_replace('-', ':', $duidpt1)));
3400

    
3401
	if (hexdec($values[0]) != count($values) - 2)
3402
		array_unshift($values, dechex(count($values)), '00');
3403

    
3404
	array_walk($values, function(&$value) {
3405
		$value = str_pad($value, 2, '0', STR_PAD_LEFT);
3406
	});
3407

    
3408
	return implode(":", $values);
3409
}
3410

    
3411
/* Returns true if $dhcp6duid is a valid DUID entry.
3412
 * Parse the entry to check for valid length according to known DUID types.
3413
 */
3414
function is_duid($dhcp6duid) {
3415
	$values = explode(":", $dhcp6duid);
3416
	if (hexdec($values[0]) == count($values) - 2) {
3417
		switch (hexdec($values[2] . $values[3])) {
3418
		case 0:
3419
			return false;
3420
			break;
3421
		case 1:
3422
			if (count($values) != 16 || strlen($dhcp6duid) != 47)
3423
				return false;
3424
			break;
3425
		case 3:
3426
			if (count($values) != 12 || strlen($dhcp6duid) != 35)
3427
				return false;
3428
			break;
3429
		case 4:
3430
			if (count($values) != 20 || strlen($dhcp6duid) != 59)
3431
				return false;
3432
			break;
3433
		/* DUID is up to 128 octets; allow 2 octets for type code, 2 more for option-len */
3434
		default:
3435
			if (count($values) > 132 || strlen($dhcp6duid) != count($values) * 3 - 1)
3436
				return false;
3437
		}
3438
	} else
3439
		return false;
3440

    
3441
	for ($i = 0; $i < count($values); $i++) {
3442
		if (ctype_xdigit($values[$i]) == false)
3443
			return false;
3444
		if (hexdec($values[$i]) < 0 || hexdec($values[$i]) > 255)
3445
			return false;
3446
	}
3447

    
3448
	return true;
3449
}
3450

    
3451
/* Write the DHCP6 DUID file */
3452
function write_dhcp6_duid($duidstring) {
3453
	// Create the hex array from the dhcp6duid config entry and write to file
3454
	global $g;
3455

    
3456
	if(!is_duid($duidstring)) {
3457
		log_error(gettext("Error: attempting to write DUID file - Invalid DUID detected"));
3458
		return false;
3459
	}
3460
	$temp = str_replace(":","",$duidstring);
3461
	$duid_binstring = pack("H*",$temp);
3462
	if ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "wb")) {
3463
		fwrite($fd, $duid_binstring);
3464
		fclose($fd);
3465
		return true;
3466
	}
3467
	log_error(gettext("Error: attempting to write DUID file - File write error"));
3468
	return false;
3469
}
3470

    
3471
/* returns duid string from 'vardb_path']}/dhcp6c_duid' */
3472
function get_duid_from_file() {
3473
	global $g;
3474

    
3475
	$duid_ASCII = "";
3476
	$count = 0;
3477

    
3478
	if (file_exists("{$g['vardb_path']}/dhcp6c_duid") &&
3479
	    ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "r"))) {
3480
		$fsize = filesize("{$g['vardb_path']}/dhcp6c_duid");
3481
		if ($fsize <= 132) {
3482
			$buffer = fread($fd, $fsize);
3483
			while($count < $fsize) {
3484
				$duid_ASCII .= bin2hex($buffer[$count]);
3485
				$count++;
3486
				if($count < $fsize) {
3487
					$duid_ASCII .= ":";
3488
				}
3489
			}
3490
		}
3491
		fclose($fd);
3492
	}
3493
	//if no file or error with read then the string returns blanked DUID string
3494
	if(!is_duid($duid_ASCII)) {
3495
		return "--:--:--:--:--:--:--:--:--:--:--:--:--:--:--:--";
3496
	}
3497
	return($duid_ASCII);
3498
}
3499

    
3500
/* Replaces the Mac OS 9 and earlier (\r) and DOS/Windows (\r\n) newlines with the Unix equivalent (\n). */
3501
function unixnewlines($text) {
3502
	return preg_replace('/\r\n?/', "\n", $text);
3503
}
3504

    
3505
function array_remove_duplicate($array, $field) {
3506
	$cmp = array();
3507
	foreach ($array as $sub) {
3508
		if (isset($sub[$field])) {
3509
			$cmp[] = $sub[$field];
3510
		}
3511
	}
3512
	$unique = array_unique(array_reverse($cmp, true));
3513
	foreach (array_keys($unique) as $k) {
3514
		$new[] = $array[$k];
3515
	}
3516
	return $new;
3517
}
3518

    
3519
/**
3520
 * Return a value specified by a path of keys in a nested array, if it exists.
3521
 * @param $arr array value to search
3522
 * @param $path string path with '/' separators
3523
 * @param $default mixed value to return if the path is not found
3524
 * @returns mixed value at path or $default if the path does not exist or if the
3525
 *          path keys an empty string and $default is non-null
3526
 */
3527
function array_get_path(array &$arr, string $path, $default = null) {
3528
	$vpath = explode('/', $path);
3529
	$el = $arr;
3530
	foreach ($vpath as $key) {
3531
		if (mb_strlen($key) == 0) {
3532
			continue;
3533
		}
3534
		if (is_array($el) && array_key_exists($key, $el)) {
3535
			$el = $el[$key];
3536
		} else {
3537
			return ($default);
3538
		}
3539
	}
3540

    
3541
	if (($default !== null) && ($el === '')) {
3542
		return ($default);
3543
	}
3544

    
3545
	return ($el);
3546
}
3547

    
3548
/*
3549
 * Initialize an arbitrary array multiple levels deep only if unset
3550
 * @param $arr top of array
3551
 * @param $path string path with '/' separators
3552
 */
3553
function array_init_path(mixed &$arr, ?string $path)
3554
{
3555
	if (!is_array($arr)) {
3556
		$arr = [];
3557
	}
3558
	if (is_null($path)) {
3559
		return;
3560
	}
3561
	$tmp = &$arr;
3562
	foreach (explode('/', $path) as $key) {
3563
		if (!is_array($tmp[$key])) {
3564
			$tmp[$key] = [];
3565
		}
3566
		$tmp = &$tmp[$key];
3567
	}
3568
}
3569

    
3570
/**
3571
 * Set a value by path in a nested array, creating arrays for intermediary keys
3572
 * as necessary. If the path cannot be reached because an intermediary exists
3573
 * but is not empty or an array, return $default.
3574
 * @param $arr array value to search
3575
 * @param $path string path with '/' separators
3576
 * @param $value mixed
3577
 * @param $default mixed value to return if the path is not found
3578
 * @returns mixed $val or $default if the path prefix does not exist
3579
 */
3580
function array_set_path(array &$arr, string $path, $value, $default = null) {
3581
	$vpath = explode('/', $path);
3582
	$vkey = null;
3583
	do {
3584
		$vkey = array_pop($vpath);
3585
	} while (mb_strlen($vkey) == 0);
3586
	if ($vkey == null) {
3587
		return ($default);
3588
	}
3589
	$el =& $arr;
3590
	foreach ($vpath as $key) {
3591
		if (mb_strlen($key) == 0) {
3592
			continue;
3593
		}
3594
		if (array_key_exists($key, $el) && !empty($el[$key])) {
3595
			if (!is_array($el[$key])) {
3596
					return ($default);
3597
			}
3598
		} else {
3599
				$el[$key] = [];
3600
		}
3601
		$el =& $el[$key];
3602
	}
3603
	$el[$vkey] = $value;
3604
	return ($value);
3605
}
3606

    
3607
/**
3608
 * Determine whether a path in a nested array has a non-null value keyed by
3609
 * $enable_key.
3610
 * @param $arr array value to search
3611
 * @param $path string path with '/' separators
3612
 * @param $enable_key string an optional alternative key value for the enable key
3613
 * @returns bool true if $enable_key exists in the array at $path, and has a
3614
 * non-null value, otherwise false
3615
 */
3616
function array_path_enabled(array &$arr, string $path, $enable_key = "enable") {
3617
	$el = array_get_path($arr, $path, []);
3618
	if (is_array($el) && isset($el[$enable_key])) {
3619
		return (true);
3620
	}
3621
	return (false);
3622
}
3623

    
3624
/**
3625
 * Remove a key from the nested array by path.
3626
 * @param $arr array value to search
3627
 * @param $path string path with '/' separators
3628
 * @returns array copy of the removed value or null
3629
 */
3630
function array_del_path(array &$arr, string $path) {
3631
	$vpath = explode('/', $path);
3632
	$vkey = array_pop($vpath);
3633
	$el =& $arr;
3634
	foreach($vpath as $key) {
3635
		if (mb_strlen($key) == 0) {
3636
			continue;
3637
		}
3638
		if (is_array($el) && array_key_exists($key, $el)) {
3639
			$el =& $el[$key];
3640
		} else {
3641
			return null;
3642
		}
3643
	}
3644

    
3645
	if (!(is_array($el) && array_key_exists($vkey, $el))) {
3646
		return null;
3647
	}
3648

    
3649
	$ret = $el[$vkey];
3650
	unset($el[$vkey]);
3651
	return ($ret);
3652
}
3653

    
3654

    
3655
function dhcpd_date_adjust_gmt($dt) {
3656
	init_config_arr(array('dhcpd'));
3657

    
3658
	foreach (config_get_path('dhcpd', []) as $dhcpditem) {
3659
		if (empty($dhcpditem)) {
3660
			continue;
3661
		}
3662
		if ($dhcpditem['dhcpleaseinlocaltime'] == "yes") {
3663
			$ts = strtotime($dt . " GMT");
3664
			if ($ts !== false) {
3665
				return strftime("%Y/%m/%d %H:%M:%S", $ts);
3666
			}
3667
		}
3668
	}
3669

    
3670
	/*
3671
	 * If we did not need to convert to local time or the conversion
3672
	 * failed, just return the input.
3673
	 */
3674
	return $dt;
3675
}
3676

    
3677
global $supported_image_types;
3678
$supported_image_types = array(
3679
	IMAGETYPE_JPEG,
3680
	IMAGETYPE_PNG,
3681
	IMAGETYPE_GIF,
3682
	IMAGETYPE_WEBP
3683
);
3684

    
3685
function is_supported_image($image_filename) {
3686
	global $supported_image_types;
3687
	$img_info = getimagesize($image_filename);
3688

    
3689
	/* If it's not an image, or it isn't in the supported list, return false */
3690
	if (($img_info === false) ||
3691
	    !in_array($img_info[2], array_keys($supported_image_types))) {
3692
		return false;
3693
	} else {
3694
		return $img_info[2];
3695
	}
3696
}
3697

    
3698
function get_lagg_ports ($laggport) {
3699
	$laggp = array();
3700
	foreach ($laggport as $lgp) {
3701
		list($lpname, $lpinfo) = explode(" ", $lgp);
3702
		preg_match('~<(.+)>~', $lpinfo, $lgportmode);
3703
		if ($lgportmode[1]) {
3704
			$laggp[] = $lpname . " (" . $lgportmode[1] . ")";
3705
		} else {
3706
			$laggp[] = $lpname;
3707
		}
3708
	}
3709
	if ($laggp) {
3710
		return implode(", ", $laggp);
3711
	} else {
3712
		return false;
3713
	}
3714
}
3715

    
3716
function cisco_to_cidr($addr) {
3717
	if (!is_ipaddr($addr)) {
3718
		throw new Exception('Value is not in dotted quad notation.');
3719
	}
3720

    
3721
	$mask = decbin(~ip2long($addr));
3722
	$mask = substr($mask, -32);
3723
	$k = 0;
3724
	for ($i = 0; $i <= 32; $i++) {
3725
		$k += intval($mask[$i]);
3726
	}
3727
	return $k;
3728
}
3729

    
3730
function cisco_extract_index($prule) {
3731
	$index = explode("#", $prule);
3732
	if (is_numeric($index[1])) {
3733
		return intval($index[1]);
3734
	} else {
3735
		syslog(LOG_WARNING, "Error parsing rule {$prule}: Could not extract index");
3736
	}
3737
	return -1;;
3738
}
3739

    
3740
function parse_cisco_acl_rule($rule, $devname, $dir, $proto) {
3741
	$rule_orig = $rule;
3742
	$rule = explode(" ", $rule);
3743
	$tmprule = "";
3744
	$index = 0;
3745

    
3746
	if ($rule[$index] == "permit") {
3747
		$startrule = "pass {$dir} quick on {$devname} ";
3748
	} else if ($rule[$index] == "deny") {
3749
		$startrule = "block {$dir} quick on {$devname} ";
3750
	} else {
3751
		return;
3752
	}
3753

    
3754
	$index++;
3755

    
3756
	switch ($rule[$index]) {
3757
		case "ip":
3758
			break;
3759
		case "icmp":
3760
			$icmp = ($proto == "inet") ? "icmp" : "ipv6-icmp";
3761
			$tmprule .= "proto {$icmp} ";
3762
			break;
3763
		case "tcp":
3764
		case "udp":
3765
			$tmprule .= "proto {$rule[$index]} ";
3766
			break;
3767
		default:
3768
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid protocol.");
3769
			return;
3770
	}
3771
	$index++;
3772

    
3773
	/* Source */
3774
	if (trim($rule[$index]) == "host") {
3775
		$index++;
3776
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
3777
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
3778
			if ($GLOBALS['attributes']['framed_ip']) {
3779
				$tmprule .= "from {$GLOBALS['attributes']['framed_ip']} ";
3780
			} else {
3781
				$tmprule .= "from {$rule[$index]} ";
3782
			}
3783
			$index++;
3784
		} else {
3785
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source host '{$rule[$index]}'.");
3786
			return;
3787
		}
3788
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
3789
		$tmprule .= "from {$rule[$index]} ";
3790
		$index++;
3791
	} elseif (trim($rule[$index]) == "any") {
3792
		$tmprule .= "from any ";
3793
		$index++;
3794
	} else {
3795
		$network = $rule[$index];
3796
		$netmask = $rule[++$index];
3797

    
3798
		if (is_ipaddrv4($network) && ($proto == "inet")) {
3799
			try {
3800
				$netmask = cisco_to_cidr($netmask);
3801
			} catch(Exception $e) {
3802
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source netmask '$netmask' (" . $e->getMessage() . ").");
3803
				return;
3804
			}
3805
			$tmprule .= "from {$network}/{$netmask} ";
3806
		} else {
3807
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source network '$network'.");
3808
			return;
3809
		}
3810

    
3811
		$index++;
3812
	}
3813

    
3814
	/* Source Operator */
3815
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
3816
		switch(trim($rule[$index])) {
3817
			case "lt":
3818
				$operator = "<";
3819
				break;
3820
			case "gt":
3821
				$operator = ">";
3822
				break;
3823
			case "eq":
3824
				$operator = "=";
3825
				break;
3826
			case "neq":
3827
				$operator = "!=";
3828
				break;
3829
		}
3830

    
3831
		$port = $rule[++$index];
3832
		if (is_port($port)) {
3833
			$tmprule .= "port {$operator} {$port} ";
3834
		} else {
3835
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source port: '$port' not a numeric value between 0 and 65535.");
3836
			return;
3837
		}
3838
		$index++;
3839
	} else if (trim($rule[$index]) == "range") {
3840
		$port = array($rule[++$index], $rule[++$index]);
3841
		if (is_port($port[0]) && is_port($port[1])) {
3842
			$tmprule .= "port {$port[0]}:{$port[1]} ";
3843
		} else {
3844
			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.");
3845
			return;
3846
		}
3847
		$index++;
3848
	}
3849

    
3850
	/* Destination */
3851
	if (trim($rule[$index]) == "host") {
3852
		$index++;
3853
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
3854
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
3855
			$tmprule .= "to {$rule[$index]} ";
3856
			$index++;
3857
		} else {
3858
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination host '{$rule[$index]}'.");
3859
			return;
3860
		}
3861
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
3862
		$tmprule .= "to {$rule[$index]} ";
3863
		$index++;
3864
	} elseif (trim($rule[$index]) == "any") {
3865
		$tmprule .= "to any ";
3866
		$index++;
3867
	} else {
3868
		$network = $rule[$index];
3869
		$netmask = $rule[++$index];
3870

    
3871
		if (is_ipaddrv4($network) && ($proto == "inet")) {
3872
			try {
3873
				$netmask = cisco_to_cidr($netmask);
3874
			} catch(Exception $e) {
3875
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination netmask '$netmask' (" . $e->getMessage() . ").");
3876
				return;
3877
			}
3878
			$tmprule .= "to {$network}/{$netmask} ";
3879
		} else {
3880
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination network '$network'.");
3881
			return;
3882
		}
3883

    
3884
		$index++;
3885
	}
3886

    
3887
	/* Destination Operator */
3888
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
3889
		switch(trim($rule[$index])) {
3890
			case "lt":
3891
				$operator = "<";
3892
				break;
3893
			case "gt":
3894
				$operator = ">";
3895
				break;
3896
			case "eq":
3897
				$operator = "=";
3898
				break;
3899
			case "neq":
3900
				$operator = "!=";
3901
				break;
3902
		}
3903

    
3904
		$port = $rule[++$index];
3905
		if (is_port($port)) {
3906
			$tmprule .= "port {$operator} {$port} ";
3907
		} else {
3908
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination port: '$port' not a numeric value between 0 and 65535.");
3909
			return;
3910
		}
3911
		$index++;
3912
	} else if (trim($rule[$index]) == "range") {
3913
		$port = array($rule[++$index], $rule[++$index]);
3914
		if (is_port($port[0]) && is_port($port[1])) {
3915
			$tmprule .= "port {$port[0]}:{$port[1]} ";
3916
		} else {
3917
			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.");
3918
			return;
3919
		}
3920
		$index++;
3921
	}
3922

    
3923
	$tmprule = $startrule . $proto . " " . $tmprule;
3924
	return $tmprule;
3925
}
3926

    
3927
function parse_cisco_acl($attribs, $dev) {
3928
	global $attributes;
3929

    
3930
	if (!is_array($attribs)) {
3931
		return "";
3932
	}
3933
	$finalrules = "";
3934
	if (is_array($attribs['ciscoavpair'])) {
3935
		$inrules = array('inet' => array(), 'inet6' => array());
3936
		$outrules = array('inet' => array(), 'inet6' => array());
3937
		foreach ($attribs['ciscoavpair'] as $avrules) {
3938
			$rule = explode("=", $avrules);
3939
			$dir = "";
3940
			if (strstr($rule[0], "inacl")) {
3941
				$dir = "in";
3942
			} else if (strstr($rule[0], "outacl")) {
3943
				$dir = "out";
3944
			} else if (strstr($rule[0], "dns-servers")) {
3945
				$attributes['dns-servers'] = explode(" ", $rule[1]);
3946
				continue;
3947
			} else if (strstr($rule[0], "route")) {
3948
				if (!is_array($attributes['routes'])) {
3949
					$attributes['routes'] = array();
3950
				}
3951
				$attributes['routes'][] = $rule[1];
3952
				continue;
3953
			}
3954
			$rindex = cisco_extract_index($rule[0]);
3955
			if ($rindex < 0) {
3956
				continue;
3957
			}
3958

    
3959
			if (strstr($rule[0], "ipv6")) {
3960
				$proto = "inet6";
3961
			} else {
3962
				$proto = "inet";
3963
			}
3964

    
3965
			$tmprule = parse_cisco_acl_rule($rule[1], $dev, $dir, $proto);
3966
			if (!empty($tmprule)) {
3967
				if ($dir == "in") {
3968
					$inrules[$proto][$rindex] = $tmprule;
3969
				} else if ($dir == "out") {
3970
					$outrules[$proto][$rindex] = $tmprule;
3971
				}
3972
			}
3973
		}
3974

    
3975

    
3976
		$state = "";
3977
		foreach (array('inet', 'inet6') as $ip) {
3978
			if (!empty($outrules[$ip])) {
3979
				$state = "no state";
3980
			}
3981
			ksort($inrules[$ip], SORT_NUMERIC);
3982
			foreach ($inrules[$ip] as $inrule) {
3983
				$finalrules .= "{$inrule} {$state}\n";
3984
			}
3985
			if (!empty($outrules[$ip])) {
3986
				ksort($outrules[$ip], SORT_NUMERIC);
3987
				foreach ($outrules[$ip] as $outrule) {
3988
					$finalrules .= "{$outrule} {$state}\n";
3989
				}
3990
			}
3991
		}
3992
	}
3993
	return $finalrules;
3994
}
3995

    
3996
function alias_idn_to_utf8($alias) {
3997
	if (is_alias($alias)) {
3998
		return $alias;
3999
	} else {
4000
		return idn_to_utf8($alias);
4001
	}
4002
}
4003

    
4004
function alias_idn_to_ascii($alias) {
4005
	if (is_alias($alias)) {
4006
		return $alias;
4007
	} else {
4008
		return idn_to_ascii($alias);
4009
	}
4010
}
4011

    
4012
// These functions were in guiconfig.inc but have been moved here so non GUI processes can use them
4013
function address_to_pconfig($adr, &$padr, &$pmask, &$pnot, &$pbeginport, &$pendport) {
4014
	if (isset($adr['any'])) {
4015
		$padr = "any";
4016
	} else if ($adr['network']) {
4017
		$padr = $adr['network'];
4018
	} else if ($adr['address']) {
4019
		list($padr, $pmask) = explode("/", $adr['address']);
4020
		if (!$pmask) {
4021
			if (is_ipaddrv6($padr)) {
4022
				$pmask = 128;
4023
			} else {
4024
				$pmask = 32;
4025
			}
4026
		}
4027
	}
4028

    
4029
	if (isset($adr['not'])) {
4030
		$pnot = 1;
4031
	} else {
4032
		$pnot = 0;
4033
	}
4034

    
4035
	if ($adr['port']) {
4036
		list($pbeginport, $pendport) = explode("-", $adr['port']);
4037
		if (!$pendport) {
4038
			$pendport = $pbeginport;
4039
		}
4040
	} else if (!is_alias($pbeginport) && !is_alias($pendport)) {
4041
		$pbeginport = "any";
4042
		$pendport = "any";
4043
	}
4044
}
4045

    
4046
function pconfig_to_address(&$adr, $padr, $pmask, $pnot = false, $pbeginport = 0, $pendport = 0, $addmask = false) {
4047
	$adr = array();
4048

    
4049
	if ($padr == "any") {
4050
		$adr['any'] = true;
4051
	} else if (get_specialnet($padr)) {
4052
		if ($addmask) {
4053
			$padr .= "/" . $pmask;
4054
		}
4055
		$adr['network'] = $padr;
4056
	} else {
4057
		$adr['address'] = $padr;
4058
		if (is_ipaddrv6($padr)) {
4059
			if ($pmask != 128) {
4060
				$adr['address'] .= "/" . $pmask;
4061
			}
4062
		} else {
4063
			if ($pmask != 32) {
4064
				$adr['address'] .= "/" . $pmask;
4065
			}
4066
		}
4067
	}
4068

    
4069
	if ($pnot) {
4070
		$adr['not'] = true;
4071
	} else {
4072
		unset($adr['not']);
4073
	}
4074

    
4075
	if (($pbeginport != 0) && ($pbeginport != "any")) {
4076
		if ($pbeginport != $pendport) {
4077
			$adr['port'] = $pbeginport . "-" . $pendport;
4078
		} else {
4079
			$adr['port'] = $pbeginport;
4080
		}
4081
	}
4082

    
4083
	/*
4084
	 * If the port is still unset, then it must not be numeric, but could
4085
	 * be an alias or a well-known/registered service.
4086
	 * See https://redmine.pfsense.org/issues/8410
4087
	 */
4088
	if (!isset($adr['port']) && is_port_or_alias($pbeginport)) {
4089
		$adr['port'] = $pbeginport;
4090
	}
4091
}
4092

    
4093
function get_specialnet(?string $net = '', array $flags = [], ?string $if = ''):bool|array {
4094
	if ($net === null || $if === null) {
4095
		return false;
4096
	}
4097
	$checkpermission = false;
4098
	$allnetflags = [SPECIALNET_ANY, SPECIALNET_SELF, SPECIALNET_CLIENTS,
4099
	                SPECIALNET_IFADDR, SPECIALNET_IFSUB, SPECIALNET_IFNET,
4100
	                SPECIALNET_GROUP, SPECIALNET_VIPS];
4101

    
4102
	if (empty($flags)) {
4103
		$flags = $allnetflags;
4104
	} else {
4105
		if (in_array(SPECIALNET_CHECKPERM, $flags)) {
4106
			$checkpermission = true;
4107
		}
4108
		if (in_array(SPECIALNET_EXCLUDE, $flags)) {
4109
			$flags = array_diff($allnetflags, $flags);
4110
		}
4111
	}
4112
	$specialnet = [];
4113
	if (in_array(SPECIALNET_IFADDR, $flags) || in_array(SPECIALNET_IFSUB, $flags) || in_array(SPECIALNET_IFNET, $flags)) {
4114
		$ifmacros = get_configured_interface_with_descr();
4115
	}
4116
	foreach ($flags as $include) {
4117
		switch ($include) {
4118
			case SPECIALNET_NONE:
4119
				$specialnet = array_merge($specialnet, ['none' => gettext('None')]);
4120
				break;
4121
			case SPECIALNET_ANY:
4122
				$specialnet = array_merge($specialnet, ['any' => gettext('Any')]);
4123
				break;
4124
			case SPECIALNET_COMPAT_ADDR:
4125
			case SPECIALNET_ADDR:
4126
				if ($include == SPECIALNET_COMPAT_ADDR) {
4127
					$ifname = 'single';
4128
				} else {
4129
					$ifname = 'address';
4130
				}
4131
				$specialnet = array_merge($specialnet, [$ifname => gettext('Address')]);
4132
				break;
4133
			case SPECIALNET_COMPAT_ADDRAL:
4134
			case SPECIALNET_ADDRAL:
4135
				if ($include == SPECIALNET_COMPAT_ADDRAL) {
4136
					$ifname = 'single';
4137
				} else {
4138
					$ifname = 'address';
4139
				}
4140
				$specialnet = array_merge($specialnet, [$ifname => gettext('Address or Alias')]);
4141
				break;
4142
			case SPECIALNET_NET:
4143
			case SPECIALNET_NETAL:
4144
				if ($include == SPECIALNET_NET) {
4145
					$ifdescr = 'Network';
4146
				} else {
4147
					$ifdescr = 'Network or Alias';
4148
				}
4149
				$specialnet = array_merge($specialnet, ['network' => gettext($ifdescr)]);
4150
				break;
4151
			case SPECIALNET_SELF:
4152
				$specialnet = array_merge($specialnet, ['(self)' => gettext('This Firewall (self)')]);
4153
				break;
4154
			case SPECIALNET_CLIENTS:
4155
				if ($checkpermission) {
4156
					$ifallowed = [];
4157
					if (have_ruleint_access("pptp")) {
4158
						$ifallowed['pptp'] = gettext('PPTP clients');
4159
					}
4160
					if (have_ruleint_access("pppoe")) {
4161
						$ifallowed['pppoe'] = gettext('PPPoE clients');
4162
					}
4163
					if (have_ruleint_access("l2tp")) {
4164
						$ifallowed['l2tp'] = gettext('L2TP clients');
4165
					}
4166
					$specialnet = array_merge($specialnet, $ifallowed);
4167
				} else {
4168
					$specialnet = array_merge($specialnet, [
4169
					                          'pptp' => gettext('PPTP clients'),
4170
					                          'pppoe' => gettext('PPPoE clients'),
4171
					                          'l2tp' => gettext('L2TP clients')]);
4172
				}
4173
				break;
4174
			case SPECIALNET_IFADDR:
4175
			case SPECIALNET_IFSUB:
4176
			case SPECIALNET_IFNET:
4177
				if ($include == SPECIALNET_IFADDR) {
4178
					$ifname_suffix = 'ip';
4179
					$ifdescr_suffix = ' address';
4180
				} elseif ($include == SPECIALNET_IFSUB) {
4181
					$ifname_suffix = '';
4182
					$ifdescr_suffix = ' subnet';
4183
				} else {
4184
					$ifname_suffix = '';
4185
					$ifdescr_suffix = ' subnets';
4186
				}
4187
				foreach ($ifmacros as $ifname => $ifdescr) {
4188
					// Only add macros for allowed interfaces when checking permissions
4189
					if ($checkpermission && !have_ruleint_access($ifname)) {
4190
						continue;
4191
					}
4192
					// If an interface is specified, only add interface macros for that interface
4193
					if (!empty($if)) {
4194
						if ($if != $ifname) {
4195
							continue;
4196
						} else {
4197
							$specialnet[$ifname . $ifname_suffix] = $ifdescr . $ifdescr_suffix;
4198
							break;
4199
						}
4200
					} else {
4201
						$specialnet[$ifname . $ifname_suffix] = $ifdescr . $ifdescr_suffix;
4202
					}
4203
				}
4204
				break;
4205
			case SPECIALNET_GROUP:
4206
				foreach (config_get_path('ifgroups/ifgroupentry', []) as $ifgen) {
4207
					$ifgenmembers = explode(' ', $ifgen['members']);
4208
					foreach ($ifgenmembers as $ifgmember) {
4209
						// Skip interface groups with members which are not allowed
4210
						if ($checkpermission && !have_ruleint_access($ifgmember)) {
4211
							continue 2;
4212
						}
4213
						// Skip interface groups which do not include the specified interface
4214
						if (!empty($if) && $if != $ifgmember &&
4215
						    $ifgmember == $ifgenmembers[array_key_last($ifgenmembers)]) {
4216
							continue 2;
4217
						}
4218
					}
4219
					$specialnet[$ifgen['ifname']] = $ifgen['ifname'] . ' networks';
4220
				}
4221
				break;
4222
			case SPECIALNET_VIPS:
4223
				$tmp_vips = [];
4224
				foreach (config_get_path('virtualip/vip', []) as $vip) {
4225
					// Only add VIPs for allowed interfaces when checking permissions
4226
					if ($checkpermission && !empty($vip['interface']) && !have_ruleint_access($vip['interface'])) {
4227
						continue;
4228
					}
4229
					$tmp_vips[$vip['subnet'] . '/' . $vip['subnet_bits']] = $vip;
4230
				}
4231
				asort($tmp_vips, SORT_NATURAL);
4232
				foreach ($tmp_vips as $vipsn => $sn) {
4233
					if (($sn['mode'] == "proxyarp" || $sn['mode'] == "other") && $sn['type'] == "network") {
4234
						$specialnet[$vipsn] = 'Subnet: ' . $sn['subnet'] . '/' . $sn['subnet_bits'] . ' (' . $sn['descr'] . ')';
4235
						// Skip expanding IPv4 VIPs with the option, as well as any IPv6 network
4236
						if (isset($sn['noexpand']) || strstr($sn['subnet'], ':' !== false)) {
4237
							continue;
4238
						}
4239
						$start = ip2long32(gen_subnet($sn['subnet'], $sn['subnet_bits']));
4240
						$end = ip2long32(gen_subnet_max($sn['subnet'], $sn['subnet_bits']));
4241
						$len = $end - $start;
4242
						for ($i = 0; $i <= $len; $i++) {
4243
							$snip = ip2long32($start+$i);
4244
			
4245
							$specialnet[$snip] = $snip . ' (' . $sn['descr'] . ')';
4246
						}
4247
					} else {
4248
						$specialnet[$sn['subnet']] = $sn['subnet'] . ' (' . $sn['descr'] . ')';
4249
					}
4250
				}
4251
				break;
4252
			default:
4253
				break;
4254
		}
4255
	}
4256

    
4257
	if (empty($net)) {
4258
		return $specialnet;
4259
	} elseif (array_key_exists($net, $specialnet)) {
4260
		return true;
4261
	} else {
4262
		return false;
4263
	}
4264
}
4265

    
4266
function is_interface_ipaddr($interface) {
4267
	if (!empty(config_get_path("interfaces/{$interface}/ipaddr"))) {
4268
		return true;
4269
	}
4270
	return false;
4271
}
4272

    
4273
function is_interface_ipaddrv6($interface) {
4274
	if (!empty(config_get_path("interfaces/{$interface}/ipaddrv6"))) {
4275
		return true;
4276
	}
4277
	return false;
4278
}
4279

    
4280
function escape_filter_regex($filtertext) {
4281
	/* If the caller (user) has not already put a backslash before a slash, to escape it in the regex, */
4282
	/* then this will do it. Take out any "\/" already there, then turn all ordinary "/" into "\/".    */
4283
	return str_replace('/', '\/', str_replace('\/', '/', $filtertext));
4284
}
4285

    
4286
/*
4287
 * Check if a given pattern has the same number of two different unescaped
4288
 * characters.
4289
 * For example, it can ensure a pattern has balanced sets of parentheses,
4290
 * braces, and brackets.
4291
 */
4292
function is_pattern_balanced_char($pattern, $open, $close) {
4293
	/* First remove escaped versions */
4294
	$pattern = str_replace('\\' . $open, '', $pattern);
4295
	$pattern = str_replace('\\' . $close, '', $pattern);
4296
	/* Check if the counts of both characters match in the target pattern */
4297
	return (substr_count($pattern, $open) == substr_count($pattern, $close));
4298
}
4299

    
4300
/*
4301
 * Check if a pattern contains balanced sets of parentheses, braces, and
4302
 * brackets.
4303
 */
4304
function is_pattern_balanced($pattern) {
4305
	if (is_pattern_balanced_char($pattern, '(', ')') &&
4306
	    is_pattern_balanced_char($pattern, '{', '}') &&
4307
	    is_pattern_balanced_char($pattern, '[', ']')) {
4308
		/* Balanced if all are true */
4309
		return true;
4310
	}
4311
	return false;
4312
}
4313

    
4314
function cleanup_regex_pattern($filtertext) {
4315
	/* Cleanup filter to prevent backreferences. */
4316
	$filtertext = escape_filter_regex($filtertext);
4317

    
4318
	/* Remove \<digit>+ backreferences
4319
	 * To match \ it must be escaped as \\\\ in PHP for preg_replace() */
4320
	$filtertext = preg_replace('/\\\\\\d+/', '', $filtertext);
4321

    
4322
	/* Check for unbalanced parentheses, braces, and brackets which
4323
	 * may be an error or attempt to circumvent protections.
4324
	 * Also discard any pattern that attempts problematic duplication
4325
	 * methods. */
4326
	if (!is_pattern_balanced($filtertext) ||
4327
	    (substr_count($filtertext, ')*') > 0) ||
4328
	    (substr_count($filtertext, ')+') > 0) ||
4329
	    (substr_count($filtertext, '{') > 0)) {
4330
		return '';
4331
	}
4332

    
4333
	return $filtertext;
4334
}
4335

    
4336
function ip6_to_asn1($addr) {
4337
	/* IPv6 MIB uses an OCTET STRING of length 16 to represent
4338
	 * 128-bit IPv6 address in network byte order.
4339
	 * see https://datatracker.ietf.org/doc/html/rfc2465#section-3
4340
	 * i.e. fc00:3::4 = 252.0.0.3.0.0.0.0.0.0.0.0.0.0.0.4
4341
	 */
4342

    
4343
	if (!is_ipaddrv6($addr)) {
4344
		return false;
4345
	}
4346
	$ipv6str = "";
4347
	$octstr = "";
4348
	foreach (explode(':', Net_IPv6::uncompress($addr)) as $v) {
4349
		$ipv6str .= str_pad($v, 4, '0', STR_PAD_LEFT);
4350
	}
4351
	foreach (str_split($ipv6str, 2) as $v) {
4352
		$octstr .= base_convert($v, 16, 10) . '.';
4353
	}
4354

    
4355
	return $octstr;
4356
}
4357

    
4358
function interfaces_interrupts() {
4359
	exec("/usr/bin/vmstat -i --libxo json", $rawdata, $rc);
4360
	$interrupts = array();
4361
	if ($rc == 0) {
4362
		$vnstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
4363
		$interruptarr = $vnstatarr['interrupt-statistics']['interrupt'];
4364

    
4365
		foreach ($interruptarr as $int){
4366
			preg_match("/irq\d+: ([a-z0-9]+)/", $int['name'], $matches);
4367
			$name = $matches[1];
4368
			if (array_key_exists($name, $interrupts)) {
4369
				/* interface with multiple queues */
4370
				$interrupts[$name]['total'] += $int['total'];
4371
				$interrupts[$name]['rate'] += $int['rate'];
4372
			} else {
4373
				$interrupts[$name]['total'] = $int['total'];
4374
				$interrupts[$name]['rate'] = $int['rate'];
4375
			}
4376
		}
4377
	}
4378

    
4379
	return $interrupts;
4380
}
4381

    
4382
function dummynet_load_module($max_qlimit) {
4383
	if (!is_module_loaded("dummynet.ko")) {
4384
		mute_kernel_msgs();
4385
		mwexec("/sbin/kldload dummynet");
4386
		unmute_kernel_msgs();
4387
	}
4388
	$sysctls = (array(
4389
			"net.inet.ip.dummynet.io_fast" => "1",
4390
			"net.inet.ip.dummynet.hash_size" => "256",
4391
			"net.inet.ip.dummynet.pipe_slot_limit" => $max_qlimit
4392
	));
4393
	init_config_arr(array('sysctl', 'item'));
4394
	foreach (config_get_path('sysctl/item', []) as $item) {
4395
		if (preg_match('/net\.inet\.ip\.dummynet\./', $item['tunable'])) {
4396
			$sysctls[$item['tunable']] = $item['value'];
4397
		}
4398
	}
4399
	set_sysctl($sysctls);
4400
}
4401

    
4402
function get_interface_vip_ips($interface) {
4403
	global $config;
4404
	$vipips = '';
4405

    
4406
	init_config_arr(array('virtualip', 'vip'));
4407
	foreach ($config['virtualip']['vip'] as $vip) {
4408
		if (($vip['interface'] == $interface) &&
4409
		    (($vip['mode'] == "carp") || ($vip['mode'] == "ipalias"))) {
4410
			$vipips .= $vip['subnet'] . ' ';
4411
		}
4412
	}
4413
	return $vipips;
4414
}
4415

    
4416
?>
(54-54/61)