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
/* same as is_subnet() but accepts IPv4 only */
922
function is_subnetv4($subnet) {
923
	return (is_subnet($subnet) == 4);
924
}
925

    
926
/* same as is_subnet() but accepts IPv6 only */
927
function is_subnetv6($subnet) {
928
	return (is_subnet($subnet) == 6);
929
}
930

    
931
/* returns true if $subnet is a valid subnet in CIDR format or an alias thereof */
932
function is_subnetoralias($subnet) {
933
	global $aliastable;
934

    
935
	if (isset($aliastable[$subnet]) && is_subnet($aliastable[$subnet])) {
936
		return true;
937
	} else {
938
		return is_subnet($subnet);
939
	}
940
}
941

    
942
/* Get number of addresses in an IPv4/IPv6 subnet (represented as a string)
943
   optional $exact=true forces error (0) to be returned if it can't be represented exactly
944
   Exact result not possible above PHP_MAX_INT which is about 2^31 addresses on x32 or 2^63 on x64
945
   Returns 0 for bad data or if cannot represent size as an INT when $exact is set. */
946
function subnet_size($subnet, $exact=false) {
947
	$parts = explode("/", $subnet);
948
	$iptype = is_ipaddr($parts[0]);
949
	if (count($parts) == 2 && $iptype) {
950
		return subnet_size_by_netmask($iptype, $parts[1], $exact);
951
	}
952
	return 0;
953
}
954

    
955
/* Get number of addresses in an IPv4/IPv6 subnet (represented numerically as IP type + bits)
956
   optional $exact=true forces error (0) to be returned if it can't be represented exactly
957
   Hard to think where we might need to count exactly a huge subnet but an overflow detection option is probably sensible
958
   Returns 0 for bad data or if cannot represent size as an INT when $exact is set. */
959
function subnet_size_by_netmask($iptype, $bits, $exact=false) {
960
	if (!is_numericint($bits)) {
961
		return 0;
962
	} elseif ($iptype == 4 && $bits <= 32) {
963
		$snsize = 32 - $bits;
964
	} elseif ($iptype == 6 && $bits <= 128) {
965
		$snsize = 128 - $bits;
966
	} else {
967
		return 0;
968
	}
969

    
970
	// 2**N returns an exact result as an INT if possible, and a float/double if not.
971
	// 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
972
	$result = 2 ** $snsize;
973

    
974
	if ($exact && !is_int($result)) {
975
		//exact required but can't represent result exactly as an INT
976
		return 0;
977
	} else {
978
		// result ok, will be an INT where possible (guaranteed up to 2^31 addresses on x32/x64) and a float for 'huge' subnets
979
		return $result;
980
	}
981
}
982

    
983
/* function used by pfblockerng */
984
function subnetv4_expand($subnet) {
985
	$result = array();
986
	list ($ip, $bits) = explode("/", $subnet);
987
	$net = ip2long($ip);
988
	$mask = (0xffffffff << (32 - $bits));
989
	$net &= $mask;
990
	$size = round(exp(log(2) * (32 - $bits)));
991
	for ($i = 0; $i < $size; $i += 1) {
992
		$result[] = long2ip($net | $i);
993
	}
994
	return $result;
995
}
996

    
997
/* find out whether two IPv4/IPv6 CIDR subnets overlap.
998
   Note: CIDR overlap implies one is identical or included so largest sn will be the same */
999
function check_subnets_overlap($subnet1, $bits1, $subnet2, $bits2) {
1000
	if (is_ipaddrv4($subnet1)) {
1001
		return check_subnetsv4_overlap($subnet1, $bits1, $subnet2, $bits2);
1002
	} else {
1003
		return check_subnetsv6_overlap($subnet1, $bits1, $subnet2, $bits2);
1004
	}
1005
}
1006

    
1007
/* find out whether two IPv4 CIDR subnets overlap.
1008
   Note: CIDR overlap means sn1/sn2 are identical or one is included in other. So sn using largest $bits will be the same  */
1009
function check_subnetsv4_overlap($subnet1, $bits1, $subnet2, $bits2) {
1010
	$largest_sn = min($bits1, $bits2);
1011
	$subnetv4_start1 = gen_subnetv4($subnet1, $largest_sn);
1012
	$subnetv4_start2 = gen_subnetv4($subnet2, $largest_sn);
1013

    
1014
	if ($subnetv4_start1 == '' || $subnetv4_start2 == '') {
1015
		// One or both args is not a valid IPv4 subnet
1016
		//FIXME: needs to return "bad data" not true/false if bad. For now return false, best we can do until fixed
1017
		return false;
1018
	}
1019
	return ($subnetv4_start1 == $subnetv4_start2);
1020
}
1021

    
1022
/* find out whether two IPv6 CIDR subnets overlap.
1023
   Note: CIDR overlap means sn1/sn2 are identical or one is included in other. So sn using largest $bits will be the same  */
1024
function check_subnetsv6_overlap($subnet1, $bits1, $subnet2, $bits2) {
1025
	$largest_sn = min($bits1, $bits2);
1026
	$subnetv6_start1 = gen_subnetv6($subnet1, $largest_sn);
1027
	$subnetv6_start2 = gen_subnetv6($subnet2, $largest_sn);
1028

    
1029
	if ($subnetv6_start1 == '' || $subnetv6_start2 == '') {
1030
		// One or both args is not a valid IPv6 subnet
1031
		//FIXME: needs to return "bad data" not true/false if bad. For now return false, best we can do until fixed
1032
		return false;
1033
	}
1034
	return ($subnetv6_start1 == $subnetv6_start2);
1035
}
1036

    
1037
/* return all PTR zones for a IPv6 network */
1038
function get_v6_ptr_zones($subnet, $bits) {
1039
	$result = array();
1040

    
1041
	if (!is_ipaddrv6($subnet)) {
1042
		return $result;
1043
	}
1044

    
1045
	if (!is_numericint($bits) || $bits > 128) {
1046
		return $result;
1047
	}
1048

    
1049
	/*
1050
	 * Find a small nibble boundary subnet mask
1051
	 * e.g. a /29 will create 8 /32 PTR zones
1052
	 */
1053
	$small_sn = $bits;
1054
	while ($small_sn % 4 != 0) {
1055
		$small_sn++;
1056
	}
1057

    
1058
	/* Get network prefix */
1059
	$small_subnet = Net_IPv6::getNetmask($subnet, $bits);
1060

    
1061
	/*
1062
	 * While small network is part of bigger one, increase 4-bit in last
1063
	 * digit to get next small network
1064
	 */
1065
	while (Net_IPv6::isInNetmask($small_subnet, $subnet, $bits)) {
1066
		/* Get a pure hex value */
1067
		$unpacked = unpack('H*hex', inet_pton($small_subnet));
1068
		/* Create PTR record using $small_sn / 4 chars */
1069
		$result[] = implode('.', array_reverse(str_split(substr(
1070
		    $unpacked['hex'], 0, $small_sn / 4)))).'.ip6.arpa';
1071

    
1072
		/* Detect what part of IP should be increased */
1073
		$change_part = (int) ($small_sn / 16);
1074
		if ($small_sn % 16 == 0) {
1075
			$change_part--;
1076
		}
1077

    
1078
		/* Increase 1 to desired part */
1079
		$parts = explode(":", Net_IPv6::uncompress($small_subnet));
1080
		$parts[$change_part]++;
1081
		$small_subnet = implode(":", $parts);
1082
	}
1083

    
1084
	return $result;
1085
}
1086

    
1087
/* return true if $addr is in $subnet, false if not */
1088
function ip_in_subnet($addr, $subnet) {
1089
	if (is_ipaddrv6($addr) && is_subnetv6($subnet)) {
1090
		/* Normalize IPv6 prefix to its start address to avoid PHP errors
1091
		 * https://redmine.pfsense.org/issues/14256
1092
		 */
1093
		list($prefix, $length) = explode("/", $subnet);
1094
		$prefix = gen_subnetv6($prefix, $length);
1095
		$subnet = "{$prefix}/{$length}";
1096
		return (Net_IPv6::isInNetmask($addr, $subnet));
1097
	} else if (is_ipaddrv4($addr) && is_subnetv4($subnet)) {
1098
		list($ip, $mask) = explode('/', $subnet);
1099
		$mask = (0xffffffff << (32 - $mask)) & 0xffffffff;
1100
		return ((ip2long($addr) & $mask) == (ip2long($ip) & $mask));
1101
	}
1102
	return false;
1103
}
1104

    
1105
/* returns true if $hostname is just a valid hostname (top part without any of the domain part) */
1106
function is_unqualified_hostname($hostname) {
1107
	if (!is_string($hostname)) {
1108
		return false;
1109
	}
1110

    
1111
	if (preg_match('/^(?:[a-z0-9_]|[a-z0-9_][a-z0-9_\-]*[a-z0-9_])$/i', $hostname)) {
1112
		return true;
1113
	} else {
1114
		return false;
1115
	}
1116
}
1117

    
1118
/* returns true if $hostname is a valid hostname, with or without being a fully-qualified domain name. */
1119
function is_hostname($hostname, $allow_wildcard=false) {
1120
	if (!is_string($hostname)) {
1121
		return false;
1122
	}
1123

    
1124
	if (is_domain($hostname, $allow_wildcard)) {
1125
		if ((substr_count($hostname, ".") == 1) && ($hostname[strlen($hostname)-1] == ".")) {
1126
			/* Only a single dot at the end like "test." - hosts cannot be directly in the root domain. */
1127
			return false;
1128
		} else {
1129
			return true;
1130
		}
1131
	} else {
1132
		return false;
1133
	}
1134
}
1135

    
1136
/* returns true if $domain is a valid domain name */
1137
function is_domain($domain, $allow_wildcard=false, $trailing_dot=true) {
1138
	if (!is_string($domain)) {
1139
		return false;
1140
	}
1141
	if (!$trailing_dot && ($domain[strlen($domain)-1] == ".")) {
1142
		return false;
1143
	}
1144
	if ($allow_wildcard) {
1145
		$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';
1146
	} else {
1147
		$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';
1148
	}
1149

    
1150
	if (preg_match($domain_regex, $domain)) {
1151
		return true;
1152
	} else {
1153
		return false;
1154
	}
1155
}
1156

    
1157
/* returns true if $macaddr is a valid MAC address */
1158
function is_macaddr($macaddr, $partial=false) {
1159
	$values = explode(":", $macaddr);
1160

    
1161
	/* Verify if the MAC address has a proper amount of parts for either a partial or full match. */
1162
	if ($partial) {
1163
		if ((count($values) < 1) || (count($values) > 6)) {
1164
			return false;
1165
		}
1166
	} elseif (count($values) != 6) {
1167
		return false;
1168
	}
1169
	for ($i = 0; $i < count($values); $i++) {
1170
		if (ctype_xdigit($values[$i]) == false)
1171
			return false;
1172
		if (hexdec($values[$i]) < 0 || hexdec($values[$i]) > 255)
1173
			return false;
1174
	}
1175

    
1176
	return true;
1177
}
1178

    
1179
/*
1180
	If $return_message is true then
1181
		returns a text message about the reason that the name is invalid.
1182
		the text includes the type of "thing" that is being checked, passed in $object. (e.g. "alias", "gateway group", "schedule")
1183
	else
1184
		returns true if $name is a valid name for an alias
1185
		returns false if $name is not a valid name for an alias
1186

    
1187
	Aliases cannot be:
1188
		bad chars: anything except a-z 0-9 and underscore
1189
		bad names: empty string, pure numeric, pure underscore
1190
		reserved words: pre-defined service/protocol/port names which should not be ambiguous, and the words "port" and  "pass" */
1191

    
1192
function is_validaliasname($name, $return_message = false, $object = "alias") {
1193
	/* Array of reserved words */
1194
	$reserved = array("port", "pass");
1195

    
1196
	if (!is_string($name) || strlen($name) >= 32 || preg_match('/(^_*$|^\d*$|[^a-z0-9_])/i', $name)) {
1197
		if ($return_message) {
1198
			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, _');
1199
		} else {
1200
			return false;
1201
		}
1202
	}
1203
	if (in_array($name, $reserved, true)) {
1204
		if ($return_message) {
1205
			return sprintf(gettext('The %1$s name must not be either of the reserved words %2$s or %3$s.'), $object, "'port'", "'pass'");
1206
		} else {
1207
			return false;
1208
		}
1209
	}
1210
	if (getprotobyname($name)) {
1211
		if ($return_message) {
1212
			return sprintf(gettext('The %1$s name must not be an IP protocol name such as TCP, UDP, ICMP etc.'), $object);
1213
		} else {
1214
			return false;
1215
		}
1216
	}
1217
	if (getservbyname($name, "tcp") || getservbyname($name, "udp")) {
1218
		if ($return_message) {
1219
			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);
1220
		} else {
1221
			return false;
1222
		}
1223
	}
1224
	if ($return_message) {
1225
		return sprintf(gettext('The %1$s name is valid.'), $object);
1226
	} else {
1227
		return true;
1228
	}
1229
}
1230

    
1231
/* returns a text message indicating if the alias name is valid, or the reason it is not valid. */
1232
function invalidaliasnamemsg($name, $object = "alias") {
1233
	return is_validaliasname($name, true, $object);
1234
}
1235

    
1236
/*
1237
 * returns true if $range is a valid integer range between $min and $max
1238
 * range delimiter can be ':' or '-'
1239
 */
1240
function is_intrange($range, $min, $max) {
1241
	$values = preg_split("/[:-]/", $range);
1242

    
1243
	if (!is_array($values) || count($values) != 2) {
1244
		return false;
1245
	}
1246

    
1247
	if (!ctype_digit(strval($values[0])) || !ctype_digit(strval($values[1]))) {
1248
		return false;
1249
	}
1250

    
1251
	$values[0] = intval($values[0]);
1252
	$values[1] = intval($values[1]);
1253

    
1254
	if ($values[0] >= $values[1]) {
1255
		return false;
1256
	}
1257

    
1258
	if ($values[0] < $min || $values[1] > $max) {
1259
		return false;
1260
	}
1261

    
1262
	return true;
1263
}
1264

    
1265
/* returns true if $port is a valid TCP/UDP/SCTP port */
1266
function is_port($port) {
1267
	if (ctype_digit(strval($port)) && ((intval($port) >= 1) && (intval($port) <= 65535))) {
1268
		return true;
1269
	}
1270
	if (getservbyname($port, "tcp") || getservbyname($port, "udp") || getservbyname($port, "sctp")) {
1271
		return true;
1272
	}
1273
	return false;
1274
}
1275

    
1276
/* returns true if $port is in use */
1277
function is_port_in_use($port, $proto = "tcp", $ip_version = 4) {
1278
	$port_info = array();
1279
	exec("/usr/bin/netstat --libxo json -an " . escapeshellarg('-' . $ip_version) . " -p " . escapeshellarg($proto), $rawdata, $rc);
1280
	if ($rc == 0) {
1281
		$netstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
1282
		$netstatarr = $netstatarr['statistics']['socket'];
1283

    
1284
		foreach($netstatarr as $portstats){
1285
			array_push($port_info, $portstats['local']['port']);
1286
		}
1287
	}
1288

    
1289
	return in_array($port, $port_info);
1290
}
1291

    
1292
/* returns true if $portrange is a valid portrange ("<port>:<port>") */
1293
function is_portrange($portrange) {
1294
	$ports = explode(":", $portrange);
1295

    
1296
	return (count($ports) == 2 && is_port($ports[0]) && is_port($ports[1]));
1297
}
1298

    
1299
/* returns true if $port is a valid port number or range ("<port>:<port>") */
1300
function is_port_or_range($port) {
1301
	return (is_port($port) || is_portrange($port));
1302
}
1303

    
1304
/* returns true if $port is an alias that is a port type */
1305
function is_portalias($port) {
1306
	if (is_alias($port)) {
1307
		foreach (config_get_path('aliases/alias', []) as $alias) {
1308
			if ($alias['name'] == $port && preg_match("/port/i", $alias['type'])) {
1309
				return true;
1310
			}
1311
		}
1312
	}
1313
	return false;
1314
}
1315

    
1316
/* returns true if $port is a valid port number or an alias thereof */
1317
function is_port_or_alias($port) {
1318
	return (is_port($port) || is_portalias($port));
1319
}
1320

    
1321
/* returns true if $port is a valid port number or range ("<port>:<port>") or an alias thereof */
1322
function is_port_or_range_or_alias($port) {
1323
	return (is_port($port) || is_portrange($port) || is_portalias($port));
1324
}
1325

    
1326
/* create ranges of sequential port numbers (200:215) and remove duplicates */
1327
function group_ports($ports, $kflc = false) {
1328
	if (!is_array($ports) || empty($ports)) {
1329
		return;
1330
	}
1331

    
1332
	$uniq = array();
1333
	$comments = array();
1334
	foreach ($ports as $port) {
1335
		if (($kflc) && (strpos($port, '#') === 0)) {	// Keep Full Line Comments (lines beginning with #).
1336
			$comments[] = $port;
1337
		} else if (is_portrange($port)) {
1338
			list($begin, $end) = explode(":", $port);
1339
			if ($begin > $end) {
1340
				$aux = $begin;
1341
				$begin = $end;
1342
				$end = $aux;
1343
			}
1344
			for ($i = $begin; $i <= $end; $i++) {
1345
				if (!in_array($i, $uniq)) {
1346
					$uniq[] = $i;
1347
				}
1348
			}
1349
		} else if (is_port($port)) {
1350
			if (!in_array($port, $uniq)) {
1351
				$uniq[] = $port;
1352
			}
1353
		}
1354
	}
1355
	sort($uniq, SORT_NUMERIC);
1356

    
1357
	$result = array();
1358
	foreach ($uniq as $idx => $port) {
1359
		if ($idx == 0) {
1360
			$result[] = $port;
1361
			continue;
1362
		}
1363

    
1364
		$last = end($result);
1365
		if (is_portrange($last)) {
1366
			list($begin, $end) = explode(":", $last);
1367
		} else {
1368
			$begin = $end = $last;
1369
		}
1370

    
1371
		if ($port == ($end+1)) {
1372
			$end++;
1373
			$result[count($result)-1] = "{$begin}:{$end}";
1374
		} else {
1375
			$result[] = $port;
1376
		}
1377
	}
1378

    
1379
	return array_merge($comments, $result);
1380
}
1381

    
1382
/* returns true if $val is a valid shaper bandwidth value */
1383
function is_valid_shaperbw($val) {
1384
	return (preg_match("/^(\d+(?:\.\d+)?)([MKG]?b|%)$/", $val));
1385
}
1386

    
1387
/* returns true if $test is in the range between $start and $end */
1388
function is_inrange_v4($test, $start, $end) {
1389
	if (!is_ipaddrv4($test) || !is_ipaddrv4($start) || !is_ipaddrv4($end)) {
1390
		return false;
1391
	}
1392

    
1393
	if (ip2ulong($test) <= ip2ulong($end) &&
1394
	    ip2ulong($test) >= ip2ulong($start)) {
1395
		return true;
1396
	}
1397

    
1398
	return false;
1399
}
1400

    
1401
/* returns true if $test is in the range between $start and $end */
1402
function is_inrange_v6($test, $start, $end) {
1403
	if (!is_ipaddrv6($test) || !is_ipaddrv6($start) || !is_ipaddrv6($end)) {
1404
		return false;
1405
	}
1406

    
1407
	if (inet_pton($test) <= inet_pton($end) &&
1408
	    inet_pton($test) >= inet_pton($start)) {
1409
		return true;
1410
	}
1411

    
1412
	return false;
1413
}
1414

    
1415
/* returns true if $test is in the range between $start and $end */
1416
function is_inrange($test, $start, $end) {
1417
	return is_ipaddrv6($test) ? is_inrange_v6($test, $start, $end) : is_inrange_v4($test, $start, $end);
1418
}
1419

    
1420
/**
1421
 * Check if an ethertype is valid
1422
 *
1423
 * @param string $ethertype	The ethertype as hex string
1424
 *
1425
 * @return bool
1426
 */
1427
function is_ethertype(string $ethertype): bool {
1428
	$ethertype = strtolower($ethertype);
1429

    
1430
	if (!str_starts_with($ethertype, '0x')) {
1431
		return (false);
1432
	}
1433

    
1434
	$ethertype = substr($ethertype, 2);
1435

    
1436
	if (!ctype_xdigit($ethertype)) {
1437
		return (false);
1438
	}
1439

    
1440
	return (filter_var(hexdec($ethertype), FILTER_VALIDATE_INT, ['options' => ['min_range' => 0x1, 'max_range' => 0xffff]]));
1441
}
1442

    
1443
function build_vip_list($fif, $family = "all") {
1444
	$list = array('address' => gettext('Interface Address'));
1445

    
1446
	$viplist = get_configured_vip_list($family);
1447
	foreach ($viplist as $vip => $address) {
1448
		if ($fif == get_configured_vip_interface($vip)) {
1449
			$list[$vip] = "$address";
1450
			if (get_vip_descr($address)) {
1451
				$list[$vip] .= " (". get_vip_descr($address) .")";
1452
			}
1453
		} else {
1454
			/**
1455
			 * additional check necessary: Alias-on-CARP VIPs return a "_vip<id>" string
1456
			 * that needs to be resolved via an additional check to see if that VIP should
1457
			 * be displayed here or skipped, as the parent isn't configured on this interface
1458
			 */
1459

    
1460
			$parentif = get_configured_vip_interface($vip);
1461
			if (str_starts_with($parentif, "_vip")) {
1462
				if ($fif == get_configured_vip_interface($parentif)) {
1463
					$list[$vip] = "$address";
1464
					if (get_vip_descr($address)) {
1465
						$list[$vip] .= " (". get_vip_descr($address) .")";
1466
					}
1467
				}
1468
			}
1469
		}
1470
	}
1471

    
1472
	return($list);
1473
}
1474

    
1475
function get_configured_vip_list($family = 'all', $type = VIP_ALL) {
1476
	global $config;
1477

    
1478
	$list = array();
1479
	if (!array_key_exists('virtualip', $config) ||
1480
		!is_array($config['virtualip']) ||
1481
	    !is_array($config['virtualip']['vip']) ||
1482
	    empty($config['virtualip']['vip'])) {
1483
		return ($list);
1484
	}
1485

    
1486
	$viparr = &$config['virtualip']['vip'];
1487
	foreach ($viparr as $vip) {
1488

    
1489
		if ($type == VIP_CARP) {
1490
			if ($vip['mode'] != "carp")
1491
				continue;
1492
		} elseif ($type == VIP_IPALIAS) {
1493
			if ($vip['mode'] != "ipalias")
1494
				continue;
1495
		} else {
1496
			if ($vip['mode'] != "carp" && $vip['mode'] != "ipalias")
1497
				continue;
1498
		}
1499

    
1500
		if ($family == 'all' ||
1501
		    ($family == 'inet' && is_ipaddrv4($vip['subnet'])) ||
1502
		    ($family == 'inet6' && is_ipaddrv6($vip['subnet']))) {
1503
			$list["_vip{$vip['uniqid']}"] = $vip['subnet'];
1504
		}
1505
	}
1506
	return ($list);
1507
}
1508

    
1509
function get_configured_vip($vipinterface = '') {
1510

    
1511
	return (get_configured_vip_detail($vipinterface, 'all', 'vip'));
1512
}
1513

    
1514
function get_configured_vip_interface($vipinterface = '') {
1515

    
1516
	return (get_configured_vip_detail($vipinterface, 'all', 'iface'));
1517
}
1518

    
1519
function get_configured_vip_ipv4($vipinterface = '') {
1520

    
1521
	return (get_configured_vip_detail($vipinterface, 'inet', 'ip'));
1522
}
1523

    
1524
function get_configured_vip_ipv6($vipinterface = '') {
1525

    
1526
	return (get_configured_vip_detail($vipinterface, 'inet6', 'ip'));
1527
}
1528

    
1529
function get_configured_vip_subnetv4($vipinterface = '') {
1530

    
1531
	return (get_configured_vip_detail($vipinterface, 'inet', 'subnet'));
1532
}
1533

    
1534
function get_configured_vip_subnetv6($vipinterface = '') {
1535

    
1536
	return (get_configured_vip_detail($vipinterface, 'inet6', 'subnet'));
1537
}
1538

    
1539
function get_configured_vip_detail($vipinterface = '', $family = 'inet', $what = 'ip') {
1540
	global $config;
1541

    
1542
	if (empty($vipinterface) ||
1543
	    !is_array($config['virtualip']) ||
1544
	    !is_array($config['virtualip']['vip']) ||
1545
	    empty($config['virtualip']['vip'])) {
1546
		return (NULL);
1547
	}
1548

    
1549
	$viparr = &$config['virtualip']['vip'];
1550
	foreach ($viparr as $vip) {
1551
		if ($vip['mode'] != "carp" && $vip['mode'] != "ipalias") {
1552
			continue;
1553
		}
1554

    
1555
		if ($vipinterface != "_vip{$vip['uniqid']}") {
1556
			continue;
1557
		}
1558

    
1559
		switch ($what) {
1560
			case 'subnet':
1561
				if ($family == 'inet' && is_ipaddrv4($vip['subnet']))
1562
					return ($vip['subnet_bits']);
1563
				else if ($family == 'inet6' && is_ipaddrv6($vip['subnet']))
1564
					return ($vip['subnet_bits']);
1565
				break;
1566
			case 'iface':
1567
				return ($vip['interface']);
1568
				break;
1569
			case 'vip':
1570
				return ($vip);
1571
				break;
1572
			case 'ip':
1573
			default:
1574
				if ($family == 'inet' && is_ipaddrv4($vip['subnet'])) {
1575
					return ($vip['subnet']);
1576
				} else if ($family == 'inet6' && is_ipaddrv6($vip['subnet'])) {
1577
					return ($vip['subnet']);
1578
				}
1579
				break;
1580
		}
1581
		break;
1582
	}
1583

    
1584
	return (NULL);
1585
}
1586

    
1587
/* comparison function for sorting by the order in which interfaces are normally created */
1588
function compare_interface_friendly_names($a, $b) {
1589
	if ($a == $b) {
1590
		return 0;
1591
	} else if ($a == 'wan') {
1592
		return -1;
1593
	} else if ($b == 'wan') {
1594
		return 1;
1595
	} else if ($a == 'lan') {
1596
		return -1;
1597
	} else if ($b == 'lan') {
1598
		return 1;
1599
	}
1600

    
1601
	return strnatcmp($a, $b);
1602
}
1603

    
1604
/**
1605
 * Get the configured interfaces list
1606
 *
1607
 * @param bool $with_disabled Include disabled interfaces
1608
 *
1609
 * @return array
1610
 */
1611
function get_configured_interface_list(bool $with_disabled = false) : array
1612
{
1613
	$iflist = [];
1614
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1615
		if ($with_disabled || isset($if_detail['enable'])) {
1616
			$iflist[$if] = $if;
1617
		}
1618
	}
1619

    
1620
	return ($iflist);
1621
}
1622

    
1623
/**
1624
 * Return the configured (and real) interfaces list.
1625
 *
1626
 * @param bool $with_disabled Include disabled interfaces
1627
 *
1628
 * @return array
1629
 */
1630
function get_configured_interface_list_by_realif(bool $with_disabled = false) : array
1631
{
1632
	$iflist = [];
1633
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1634
		if ($with_disabled || isset($if_detail['enable'])) {
1635
			$tmpif = get_real_interface($if);
1636
			if (empty($tmpif)) {
1637
				continue;
1638
			}
1639
			$iflist[$tmpif] = $if;
1640
		}
1641
	}
1642

    
1643
	return ($iflist);
1644
}
1645

    
1646
/**
1647
 * Return the configured interfaces list with their description.
1648
 *
1649
 * @param bool $with_disabled Include disabled interfaces
1650
 *
1651
 * @return array
1652
 */
1653
function get_configured_interface_with_descr(bool $with_disabled = false) : array
1654
{
1655
	global $user_settings;
1656

    
1657
	$iflist = [];
1658
	foreach (config_get_path('interfaces', []) as $if => $if_detail) {
1659
		if ($with_disabled || isset($if_detail['enable'])) {
1660
			$iflist[$if] = strtoupper(array_get_path($if_detail, 'descr', $if));
1661
		}
1662
	}
1663

    
1664
	if (is_array($user_settings)
1665
	    && array_get_path($user_settings, 'webgui/interfacessort')) {
1666
		asort($iflist);
1667
	}
1668

    
1669
	return ($iflist);
1670
}
1671

    
1672
/*
1673
 *   get_configured_ip_addresses() - Return a list of all configured
1674
 *   IPv4 addresses.
1675
 *
1676
 */
1677
function get_configured_ip_addresses() {
1678
	global $config;
1679

    
1680
	if (!function_exists('get_interface_ip')) {
1681
		require_once("interfaces.inc");
1682
	}
1683
	$ip_array = array();
1684
	$interfaces = get_configured_interface_list();
1685
	if (is_array($interfaces)) {
1686
		foreach ($interfaces as $int) {
1687
			$ipaddr = get_interface_ip($int);
1688
			$ip_array[$int] = $ipaddr;
1689
		}
1690
	}
1691
	$interfaces = get_configured_vip_list('inet');
1692
	if (is_array($interfaces)) {
1693
		foreach ($interfaces as $int => $ipaddr) {
1694
			$ip_array[$int] = $ipaddr;
1695
		}
1696
	}
1697

    
1698
	/* pppoe server */
1699
	if (is_array($config['pppoes']) && is_array($config['pppoes']['pppoe'])) {
1700
		foreach ($config['pppoes']['pppoe'] as $pppoe) {
1701
			if ($pppoe['mode'] == "server") {
1702
				if (is_ipaddr($pppoe['localip'])) {
1703
					$int = "poes". $pppoe['pppoeid'];
1704
					$ip_array[$int] = $pppoe['localip'];
1705
				}
1706
			}
1707
		}
1708
	}
1709

    
1710
	return $ip_array;
1711
}
1712

    
1713
/*
1714
 *   get_configured_ipv6_addresses() - Return a list of all configured
1715
 *   IPv6 addresses.
1716
 *
1717
 */
1718
function get_configured_ipv6_addresses($linklocal_fallback = false) {
1719
	require_once("interfaces.inc");
1720
	$ipv6_array = array();
1721
	$interfaces = get_configured_interface_list();
1722
	if (is_array($interfaces)) {
1723
		foreach ($interfaces as $int) {
1724
			$ipaddrv6 = text_to_compressed_ip6(get_interface_ipv6($int, false, $linklocal_fallback));
1725
			$ipv6_array[$int] = $ipaddrv6;
1726
		}
1727
	}
1728
	$interfaces = get_configured_vip_list('inet6');
1729
	if (is_array($interfaces)) {
1730
		foreach ($interfaces as $int => $ipaddrv6) {
1731
			$ipv6_array[$int] = text_to_compressed_ip6($ipaddrv6);
1732
		}
1733
	}
1734
	return $ipv6_array;
1735
}
1736

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

    
1861
			case "friendly":
1862
				if ($friendly != "") {
1863
					$toput['if'] = $ifname;
1864
					$iflist[$friendly] = $toput;
1865
				}
1866
				break;
1867
			}
1868
		}
1869
	}
1870
	return $iflist;
1871
}
1872

    
1873
function get_lagg_interface_list() {
1874
	global $config;
1875

    
1876
	$plist = array();
1877
	if (isset($config['laggs']['lagg']) && is_array($config['laggs']['lagg'])) {
1878
		foreach ($config['laggs']['lagg'] as $lagg) {
1879
			$lagg['mac'] = get_interface_mac($lagg['laggif']);
1880
			$lagg['islagg'] = true;
1881
			$plist[$lagg['laggif']] = $lagg;
1882
		}
1883
	}
1884

    
1885
	return ($plist);
1886
}
1887

    
1888
/****f* util/log_error
1889
* NAME
1890
*   log_error  - Sends a string to syslog.
1891
* INPUTS
1892
*   $error     - string containing the syslog message.
1893
* RESULT
1894
*   null
1895
******/
1896
function log_error($error) {
1897
	global $g;
1898
	$page = $_SERVER['SCRIPT_NAME'];
1899
	if (empty($page)) {
1900
		$files = get_included_files();
1901
		$page = basename($files[0]);
1902
	}
1903
	syslog(LOG_ERR, "$page: $error");
1904
	if (g_get('debug')) {
1905
		syslog(LOG_WARNING, var_export(debug_backtrace()));
1906
	}
1907
	return;
1908
}
1909

    
1910
/****f* util/log_auth
1911
* NAME
1912
*   log_auth   - Sends a string to syslog as LOG_AUTH facility
1913
* INPUTS
1914
*   $error     - string containing the syslog message.
1915
* RESULT
1916
*   null
1917
******/
1918
function log_auth($error) {
1919
	global $g;
1920
	$page = $_SERVER['SCRIPT_NAME'];
1921
	$level = config_path_enabled('system/webgui', 'quietlogin') ? LOG_NOTICE|LOG_AUTH : LOG_AUTH;
1922
	syslog($level, "{$page}: {$error}");
1923
	if (g_get('debug')) {
1924
		syslog(LOG_WARNING, var_export(debug_backtrace()));
1925
	}
1926
	return;
1927
}
1928

    
1929
/****f* util/exec_command
1930
 * NAME
1931
 *   exec_command - Execute a command and return a string of the result.
1932
 * INPUTS
1933
 *   $command   - String of the command to be executed.
1934
 * RESULT
1935
 *   String containing the command's result.
1936
 * NOTES
1937
 *   This function returns the command's stdout and stderr.
1938
 ******/
1939
function exec_command($command) {
1940
	$output = array();
1941
	exec($command . ' 2>&1', $output);
1942
	return(implode("\n", $output));
1943
}
1944

    
1945
/* wrapper for exec()
1946
   Executes in background or foreground.
1947
   For background execution, returns PID of background process to allow calling code control */
1948
function mwexec($command, $nologentry = false, $clearsigmask = false, $background = false) {
1949
	global $g;
1950
	$retval = 0;
1951

    
1952
	if (g_get('debug')) {
1953
		if (!$_SERVER['REMOTE_ADDR']) {
1954
			echo "mwexec(): $command" . ($background ? " [BG]":"") . "\n";
1955
		}
1956
	}
1957
	if ($clearsigmask) {
1958
		$oldset = array();
1959
		pcntl_sigprocmask(SIG_SETMASK, array(), $oldset);
1960
	}
1961

    
1962
	if ($background) {
1963
		// start background process and return PID
1964
		$retval = exec("/usr/bin/nohup $command > /dev/null 2>&1 & echo $!");
1965
	} else {
1966
		// run in foreground, and (optionally) log if nonzero return
1967
		$outputarray = array();
1968
		exec("$command 2>&1", $outputarray, $retval);
1969
		if (($retval <> 0) && (!$nologentry || isset($config['system']['developerspew']))) {
1970
			log_error(sprintf(gettext("The command '%1\$s' returned exit code '%2\$d', the output was '%3\$s' "), $command, $retval, implode(" ", $outputarray)));
1971
		}
1972
	}
1973

    
1974
	if ($clearsigmask) {
1975
		pcntl_sigprocmask(SIG_SETMASK, $oldset);
1976
	}
1977

    
1978
	return $retval;
1979
}
1980

    
1981
/* wrapper for exec() in background */
1982
function mwexec_bg($command, $clearsigmask = false) {
1983
	return mwexec($command, false, $clearsigmask, true);
1984
}
1985

    
1986
/*
1987
 * Unlink a file, or pattern-match of a file, if it exists
1988
 *
1989
 * If the file/path contains glob() compatible wildcards, all matching files
1990
 * will be unlinked.
1991
 * Any warning/errors are suppressed (e.g. no matching files to delete)
1992
 * If there are matching file(s) and they were all unlinked OK, then return
1993
 * true.  Otherwise return false (the requested file(s) did not exist, or
1994
 * could not be deleted), this allows the caller to know if they were the one
1995
 * to successfully delete the file(s).
1996
 */
1997
function unlink_if_exists($fn) {
1998
	$to_do = glob($fn);
1999
	if (is_array($to_do) && count($to_do) > 0) {
2000
		// Returns an array of boolean indicating if each unlink worked
2001
		$results = @array_map("unlink", $to_do);
2002
		// If there is no false in the array, then all went well
2003
		$result = !in_array(false, $results, true);
2004
	} else {
2005
		$result = @unlink($fn);
2006
	}
2007
	return $result;
2008
}
2009

    
2010
/* make a global alias table (for faster lookups) */
2011
function alias_make_table() {
2012
	global $aliastable;
2013

    
2014
	$aliastable = array();
2015

    
2016
	foreach (config_get_path('aliases/alias', []) as $alias) {
2017
		if (!is_array($alias) || empty($alias)) {
2018
			continue;
2019
		}
2020
		if ($alias['name']) {
2021
			$aliastable[$alias['name']] = $alias['address'];
2022
		}
2023
	}
2024
}
2025

    
2026
/* check if an alias exists */
2027
function is_alias($name) {
2028
	global $aliastable;
2029

    
2030
	return isset($aliastable[$name]);
2031
}
2032

    
2033
function alias_get_type($name) {
2034

    
2035
	foreach (config_get_path('aliases/alias', []) as $alias) {
2036
		if ($name == $alias['name']) {
2037
			return $alias['type'];
2038
		}
2039
	}
2040

    
2041
	return "";
2042
}
2043

    
2044
/* expand a url alias, if necessary */
2045
function alias_expand($name) {
2046
	global $aliastable;
2047
	$urltable_prefix = "/var/db/aliastables/";
2048
	$urltable_filename = $urltable_prefix . $name . ".txt";
2049

    
2050
	if (isset($aliastable[$name])) {
2051
		// alias names cannot be strictly numeric. redmine #4289
2052
		if (is_numericint($name)) {
2053
			return null;
2054
		}
2055
		/*
2056
		 * make sure if it's a url alias, it actually exists.
2057
		 * redmine #5845, #13068
2058
		 */
2059
		foreach (config_get_path('aliases/alias', []) as $alias) {
2060
			if ($alias['name'] == $name) {
2061
				if (in_array($alias['type'], ['url', 'url_ports', 'urltable', "urltable_ports"])) {
2062
					if (is_URL($alias['url']) &&
2063
					    file_exists($urltable_filename) &&
2064
					    !empty(trim(file_get_contents($urltable_filename)))) {
2065
						return "\${$name}";
2066
					} else {
2067
						return null;
2068
					}
2069
				}
2070
			}
2071
		}
2072
		return "\${$name}";
2073
	} else if (is_ipaddr($name) || is_subnet($name) ||
2074
	    is_port_or_range($name)) {
2075
		return "{$name}";
2076
	} else {
2077
		return null;
2078
	}
2079
}
2080

    
2081
function alias_expand_urltable($name) {
2082
	$urltable_prefix = "/var/db/aliastables/";
2083
	$urltable_filename = $urltable_prefix . $name . ".txt";
2084

    
2085
	foreach (config_get_path('aliases/alias', []) as $alias) {
2086
		if (!preg_match("/urltable/i", $alias['type']) ||
2087
		    ($alias['name'] != $name)) {
2088
			continue;
2089
		}
2090

    
2091
		if (is_URL($alias["url"]) && file_exists($urltable_filename)) {
2092
			if (!filesize($urltable_filename)) {
2093
				// file exists, but is empty, try to sync
2094
				send_event("service sync alias {$name}");
2095
			}
2096
			return $urltable_filename;
2097
		} else {
2098
			send_event("service sync alias {$name}");
2099
			break;
2100
		}
2101
	}
2102
	return null;
2103
}
2104

    
2105
/* obtain MAC address given an IP address by looking at the ARP/NDP table */
2106
function arp_get_mac_by_ip($ip, $do_ping = true) {
2107
	unset($macaddr);
2108
	$retval = 1;
2109
	switch (is_ipaddr($ip)) {
2110
		case 4:
2111
			if ($do_ping === true) {
2112
				mwexec("/sbin/ping -c 1 -t 1 " . escapeshellarg($ip), true);
2113
			}
2114
			$macaddr = exec("/usr/sbin/arp -n " . escapeshellarg($ip) . " | /usr/bin/awk '{print $4}'", $output, $retval);
2115
			break;
2116
		case 6:
2117
			if ($do_ping === true) {
2118
				mwexec("/sbin/ping6 -c 1 -X 1 " . escapeshellarg($ip), true);
2119
			}
2120
			$macaddr = exec("/usr/sbin/ndp -n " . escapeshellarg($ip) . " | /usr/bin/awk '{print $2}'", $output, $retval);
2121
			break;
2122
	}
2123
	if ($retval == 0 && is_macaddr($macaddr)) {
2124
		return $macaddr;
2125
	} else {
2126
		return false;
2127
	}
2128
}
2129

    
2130
/* return a fieldname that is safe for xml usage */
2131
function xml_safe_fieldname($fieldname) {
2132
	$replace = array(
2133
	    '/', '-', ' ', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
2134
	    '_', '+', '=', '{', '}', '[', ']', '|', '/', '<', '>', '?',
2135
	    ':', ',', '.', '\'', '\\'
2136
	);
2137
	return strtolower(str_replace($replace, "", $fieldname));
2138
}
2139

    
2140
function mac_format($clientmac) {
2141
	global $config, $cpzone;
2142

    
2143
	$mac = explode(":", $clientmac);
2144
	$mac_format = $cpzone ? $config['captiveportal'][$cpzone]['radmac_format'] : false;
2145

    
2146
	switch ($mac_format) {
2147
		case 'singledash':
2148
			return "$mac[0]$mac[1]$mac[2]-$mac[3]$mac[4]$mac[5]";
2149

    
2150
		case 'ietf':
2151
			return "$mac[0]-$mac[1]-$mac[2]-$mac[3]-$mac[4]-$mac[5]";
2152

    
2153
		case 'cisco':
2154
			return "$mac[0]$mac[1].$mac[2]$mac[3].$mac[4]$mac[5]";
2155

    
2156
		case 'unformatted':
2157
			return "$mac[0]$mac[1]$mac[2]$mac[3]$mac[4]$mac[5]";
2158

    
2159
		default:
2160
			return $clientmac;
2161
	}
2162
}
2163

    
2164
function resolve_retry($hostname, $protocol = 'inet') {
2165
	$retries = 10;
2166
	$numrecords = 1;
2167
	$recresult = array();
2168

    
2169
	switch ($protocol) {
2170
		case 'any':
2171
			$checkproto = 'is_ipaddr';
2172
			$dnsproto = DNS_ANY;
2173
			$dnstype = array('A', 'AAAA');
2174
			break;
2175
		case 'inet6':
2176
			$checkproto = 'is_ipaddrv6';
2177
			$dnsproto = DNS_AAAA;
2178
			$dnstype = array('AAAA');
2179
			break;
2180
		case 'inet':
2181
		default:
2182
			$checkproto = 'is_ipaddrv4';
2183
			$dnsproto = DNS_A;
2184
			$dnstype = array('A');
2185
			break;
2186
	}
2187

    
2188
	for ($i = 0; $i < $retries; $i++) {
2189
		if ($checkproto($hostname)) {
2190
			return $hostname;
2191
		}
2192

    
2193
		$dnsresult = @dns_get_record($hostname, $dnsproto);
2194

    
2195
		if (!empty($dnsresult)) {
2196
			foreach ($dnsresult as $ip) {
2197
				if (is_array($ip)) {
2198
					if (in_array($ip['type'], $dnstype)) {
2199
						if ($checkproto($ip['ip'])) {
2200
							$recresult[] = $ip['ip'];
2201
						}
2202

    
2203
						if ($checkproto($ip['ipv6'])) {
2204
							$recresult[] = $ip['ipv6'];
2205
						}
2206
					}
2207
				}
2208
			}
2209
		}
2210

    
2211
		// Return on success
2212
		if (!empty($recresult)) {
2213
			if ($numrecords == 1) {
2214
				return $recresult[0];
2215
			} else {
2216
				return array_slice($recresult, 0, $numrecords);
2217
			}
2218
		}
2219

    
2220
		usleep(100000);
2221
	}
2222

    
2223
	return false;
2224
}
2225

    
2226
function format_bytes($bytes) {
2227
	if ($bytes >= 1099511627776) {
2228
		return sprintf("%.2f TiB", $bytes/1099511627776);
2229
	} else if ($bytes >= 1073741824) {
2230
		return sprintf("%.2f GiB", $bytes/1073741824);
2231
	} else if ($bytes >= 1048576) {
2232
		return sprintf("%.2f MiB", $bytes/1048576);
2233
	} else if ($bytes >= 1024) {
2234
		return sprintf("%.0f KiB", $bytes/1024);
2235
	} else {
2236
		return sprintf("%d B", $bytes);
2237
	}
2238
}
2239

    
2240
function format_number(int $num, int $precision = 3): string
2241
{
2242
    $units = ['', 'K', 'M', 'G', 'T'];
2243
    for ($i = 0; $num >= 1000; $i++) {
2244
        $num /= 1000;
2245
    }
2246
    return (round($num, $precision) . $units[$i]);
2247
}
2248

    
2249
function unformat_number($formated_num) {
2250
	$num = strtoupper($formated_num);
2251

    
2252
	if ( strpos($num,"T") !== false ) {
2253
		$num = str_replace("T","",$num) * 1000 * 1000 * 1000 * 1000;
2254
	} else if ( strpos($num,"G") !== false ) {
2255
		$num = str_replace("G","",$num) * 1000 * 1000 * 1000;
2256
	} else if ( strpos($num,"M") !== false ) {
2257
		$num = str_replace("M","",$num) * 1000 * 1000;
2258
	} else if ( strpos($num,"K") !== false ) {
2259
		$num = str_replace("K","",$num) * 1000;
2260
	}
2261

    
2262
	return $num;
2263
}
2264

    
2265
function update_filter_reload_status($text, $new=false) {
2266
	global $g;
2267

    
2268
	if ($new) {
2269
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL);
2270
	} else {
2271
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL, FILE_APPEND);
2272
	}
2273
}
2274

    
2275
/****** util/return_dir_as_array
2276
 * NAME
2277
 *   return_dir_as_array - Return a directory's contents as an array.
2278
 * INPUTS
2279
 *   $dir          - string containing the path to the desired directory.
2280
 *   $filter_regex - string containing a regular expression to filter file names. Default empty.
2281
 * RESULT
2282
 *   $dir_array - array containing the directory's contents. This array will be empty if the path specified is invalid.
2283
 ******/
2284
function return_dir_as_array($dir, $filter_regex = '') {
2285
	$dir_array = array();
2286
	if (is_dir($dir)) {
2287
		if ($dh = opendir($dir)) {
2288
			while (($file = readdir($dh)) !== false) {
2289
				if (($file == ".") || ($file == "..")) {
2290
					continue;
2291
				}
2292

    
2293
				if (empty($filter_regex) || preg_match($filter_regex, $file)) {
2294
					array_push($dir_array, $file);
2295
				}
2296
			}
2297
			closedir($dh);
2298
		}
2299
	}
2300
	return $dir_array;
2301
}
2302

    
2303
function run_plugins($directory) {
2304
	/* process packager manager custom rules */
2305
	$files = return_dir_as_array($directory);
2306
	if (is_array($files)) {
2307
		foreach ($files as $file) {
2308
			if (stristr($file, ".sh") == true) {
2309
				mwexec($directory . $file . " start");
2310
			} else if (!is_dir($directory . "/" . $file) && stristr($file, ".inc")) {
2311
				require_once($directory . "/" . $file);
2312
			}
2313
		}
2314
	}
2315
}
2316

    
2317
/*
2318
 *    safe_mkdir($path, $mode = 0755)
2319
 *    create directory if it doesn't already exist and isn't a file!
2320
 */
2321
function safe_mkdir($path, $mode = 0755) {
2322
	if (!is_file($path) && !is_dir($path)) {
2323
		return @mkdir($path, $mode, true);
2324
	} else {
2325
		return false;
2326
	}
2327
}
2328

    
2329
/*
2330
 * get_sysctl($names)
2331
 * Get values of sysctl OID's listed in $names (accepts an array or a single
2332
 * name) and return an array of key/value pairs set for those that exist
2333
 */
2334
function get_sysctl($names) {
2335
	if (empty($names)) {
2336
		return array();
2337
	}
2338

    
2339
	if (is_array($names)) {
2340
		$name_list = array();
2341
		foreach ($names as $name) {
2342
			$name_list[] = escapeshellarg($name);
2343
		}
2344
	} else {
2345
		$name_list = array(escapeshellarg($names));
2346
	}
2347

    
2348
	exec("/sbin/sysctl -iq " . implode(" ", $name_list), $output);
2349
	$values = array();
2350
	foreach ($output as $line) {
2351
		$line = explode(": ", $line, 2);
2352
		if (count($line) == 2) {
2353
			$values[$line[0]] = $line[1];
2354
		}
2355
	}
2356

    
2357
	return $values;
2358
}
2359

    
2360
/*
2361
 * get_single_sysctl($name)
2362
 * Wrapper for get_sysctl() to simplify read of a single sysctl value
2363
 * return the value for sysctl $name or empty string if it doesn't exist
2364
 */
2365
function get_single_sysctl($name) {
2366
	if (empty($name)) {
2367
		return "";
2368
	}
2369

    
2370
	$value = get_sysctl($name);
2371
	if (empty($value) || !isset($value[$name])) {
2372
		return "";
2373
	}
2374

    
2375
	return $value[$name];
2376
}
2377

    
2378
/*
2379
 * set_sysctl($value_list)
2380
 * Set sysctl OID's listed as key/value pairs and return
2381
 * an array with keys set for those that succeeded
2382
 */
2383
function set_sysctl($values) {
2384
	if (empty($values)) {
2385
		return array();
2386
	}
2387

    
2388
	$value_list = array();
2389
	foreach ($values as $key => $value) {
2390
		$value_list[] = escapeshellarg($key) . "=" . escapeshellarg($value);
2391
	}
2392

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

    
2395
	/* Retry individually if failed (one or more read-only) */
2396
	if ($success <> 0 && count($value_list) > 1) {
2397
		foreach ($value_list as $value) {
2398
			exec("/sbin/sysctl -iq " . $value, $output);
2399
		}
2400
	}
2401

    
2402
	$ret = array();
2403
	foreach ($output as $line) {
2404
		$line = explode(": ", $line, 2);
2405
		if (count($line) == 2) {
2406
			$ret[$line[0]] = true;
2407
		}
2408
	}
2409

    
2410
	return $ret;
2411
}
2412

    
2413
/*
2414
 * set_single_sysctl($name, $value)
2415
 * Wrapper to set_sysctl() to make it simple to set only one sysctl
2416
 * returns boolean meaning if it succeeded
2417
 */
2418
function set_single_sysctl($name, $value) {
2419
	if (empty($name)) {
2420
		return false;
2421
	}
2422

    
2423
	$result = set_sysctl(array($name => $value));
2424

    
2425
	if (!isset($result[$name]) || $result[$name] != $value) {
2426
		return false;
2427
	}
2428

    
2429
	return true;
2430
}
2431

    
2432
/*
2433
 *     get_memory()
2434
 *     returns an array listing the amount of
2435
 *     memory installed in the hardware
2436
 *     [0] net memory available for the OS (FreeBSD) after some is taken by BIOS, video or whatever - e.g. 235 MBytes
2437
 *     [1] real (actual) memory of the system, should be the size of the RAM card/s - e.g. 256 MBytes
2438
 */
2439
function get_memory() {
2440
	$physmem = get_single_sysctl("hw.physmem");
2441
	$realmem = get_single_sysctl("hw.realmem");
2442
	/* Ensure $physmem has a sane value */
2443
	if (!is_numericint($physmem) &&
2444
	    is_numericint($realmem)) {
2445
		$physmem = $realmem;
2446
	}
2447
	/* Ensure $realmem has a sane value */
2448
	if (!is_numericint($realmem) &&
2449
	    is_numericint($physmem)) {
2450
		$realmem = $physmem;
2451
	}
2452
	/* If both are invalid, something deeper is wrong */
2453
	if (!is_numericint($physmem) &&
2454
	    is_numericint($realmem)) {
2455
		/* Try checking by pages instead */
2456
		$membypages = (int) get_single_sysctl("vm.stats.vm.v_page_count") * (int) get_single_sysctl("vm.stats.vm.v_page_size");
2457
		if (is_numericint($membypages)) {
2458
			$physmem = $membypages;
2459
			$realmem = $membypages;
2460
		} else {
2461
			/* Everything failed, return zeroes */
2462
			$physmem = 0;
2463
			$realmem = 0;
2464
		}
2465
	}
2466
	/* convert from bytes to megabytes */
2467
	return array(((int) $physmem/1048576), ((int) $realmem/1048576));
2468
}
2469

    
2470
function mute_kernel_msgs() {
2471
	if (config_path_enabled('system','enableserial')) {
2472
		return;
2473
	}
2474
	exec("/sbin/conscontrol mute on");
2475
}
2476

    
2477
function unmute_kernel_msgs() {
2478
	exec("/sbin/conscontrol mute off");
2479
}
2480

    
2481
function start_devd() {
2482
	global $g;
2483

    
2484
	/* Generate hints for the kernel loader. */
2485
	$module_paths = explode(";", get_single_sysctl("kern.module_path"));
2486
	foreach ($module_paths as $path) {
2487
		if (!is_dir($path) ||
2488
		    (($files = scandir($path)) == false)) {
2489
			continue;
2490
		}
2491
		$found = false;
2492
		foreach ($files as $file) {
2493
			if (strlen($file) > 3 &&
2494
			    strcasecmp(substr($file, -3), ".ko") == 0) {
2495
				$found = true;
2496
				break;
2497
			}
2498
		}
2499
		if ($found == false) {
2500
			continue;
2501
		}
2502
		$_gb = exec("/usr/sbin/kldxref $path");
2503
		unset($_gb);
2504
	}
2505

    
2506
	/* Use the undocumented -q options of devd to quiet its log spamming */
2507
	$_gb = exec("/sbin/devd -q -f /etc/{$g['product_name']}-devd.conf");
2508
	sleep(1);
2509
	unset($_gb);
2510
}
2511

    
2512
function is_interface_vlan_mismatch() {
2513
	foreach (config_get_path('vlans/vlan', []) as $vlan) {
2514
		if (substr($vlan['if'], 0, 4) == "lagg") {
2515
			return false;
2516
		}
2517
		if (does_interface_exist($vlan['if']) == false) {
2518
			return true;
2519
		}
2520
	}
2521

    
2522
	return false;
2523
}
2524

    
2525
function is_interface_mismatch() {
2526
	global $config, $g;
2527

    
2528
	$do_assign = false;
2529
	$i = 0;
2530
	$missing_interfaces = array();
2531
	if (is_array($config['interfaces'])) {
2532
		foreach ($config['interfaces'] as $ifcfg) {
2533
			if (interface_is_vlan($ifcfg['if']) != NULL ||
2534
			    interface_is_qinq($ifcfg['if']) != NULL ||
2535
			    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'])) {
2536
				// Do not check these interfaces.
2537
				$i++;
2538
				continue;
2539
			} else if (does_interface_exist($ifcfg['if']) == false) {
2540
				$missing_interfaces[] = $ifcfg['if'];
2541
				$do_assign = true;
2542
			} else {
2543
				$i++;
2544
			}
2545
		}
2546
	}
2547

    
2548
	/* VLAN/QinQ-only interface mismatch detection
2549
	 * see https://redmine.pfsense.org/issues/12170 */
2550
	init_config_arr(array('vlans', 'vlan'));
2551
	foreach ($config['vlans']['vlan'] as $vlan) {
2552
		if (!does_interface_exist($vlan['if']) &&
2553
		    !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'])) {
2554
			$missing_interfaces[] = $vlan['if'];
2555
			$do_assign = true;
2556
		}
2557
	}
2558
	init_config_arr(array('qinqs', 'qinqentry'));
2559
	foreach ($config['qinqs']['qinqentry'] as $qinq) {
2560
		if (!does_interface_exist($qinq['if']) &&
2561
		    !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'])) {
2562
			$missing_interfaces[] = $qinq['if'];
2563
			$do_assign = true;
2564
		}
2565
	}
2566

    
2567
	if (file_exists("{$g['tmp_path']}/assign_complete")) {
2568
		$do_assign = false;
2569
	}
2570

    
2571
	if (!empty($missing_interfaces) && $do_assign) {
2572
		file_put_contents("{$g['tmp_path']}/missing_interfaces", implode(' ', $missing_interfaces));
2573
	} else {
2574
		@unlink("{$g['tmp_path']}/missing_interfaces");
2575
	}
2576

    
2577
	return $do_assign;
2578
}
2579

    
2580
/* sync carp entries to other firewalls */
2581
function carp_sync_client() {
2582
	send_event("filter sync");
2583
}
2584

    
2585
/****f* util/isAjax
2586
 * NAME
2587
 *   isAjax - reports if the request is driven from prototype
2588
 * INPUTS
2589
 *   none
2590
 * RESULT
2591
 *   true/false
2592
 ******/
2593
function isAjax() {
2594
	return isset ($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
2595
}
2596

    
2597
/****f* util/timeout
2598
 * NAME
2599
 *   timeout - console input with timeout countdown. Note: erases 2 char of screen for timer. Leave space.
2600
 * INPUTS
2601
 *   optional, seconds to wait before timeout. Default 9 seconds.
2602
 * RESULT
2603
 *   returns 1 char of user input or null if no input.
2604
 ******/
2605
function timeout($timer = 9) {
2606
	while (!isset($key)) {
2607
		if ($timer >= 9) {
2608
			echo chr(8) . chr(8) . ($timer == 9 ? chr(32) : null) . "{$timer}";
2609
		} else {
2610
			echo chr(8). "{$timer}";
2611
		}
2612
		`/bin/stty -icanon min 0 time 25`;
2613
		$key = trim(`KEY=\`dd count=1 2>/dev/null\`; echo \$KEY`);
2614
		`/bin/stty icanon`;
2615
		if ($key == '') {
2616
			unset($key);
2617
		}
2618
		$timer--;
2619
		if ($timer == 0) {
2620
			break;
2621
		}
2622
	}
2623
	return $key;
2624
}
2625

    
2626
/****f* util/msort
2627
 * NAME
2628
 *   msort - sort array
2629
 * INPUTS
2630
 *   $array to be sorted, field to sort by, direction of sort
2631
 * RESULT
2632
 *   returns newly sorted array
2633
 ******/
2634
function msort($array, $id = "id", $sort_ascending = true) {
2635
	$temp_array = array();
2636
	if (!is_array($array)) {
2637
		return $temp_array;
2638
	}
2639
	while (count($array)>0) {
2640
		$lowest_id = 0;
2641
		$index = 0;
2642
		foreach ($array as $item) {
2643
			if (isset($item[$id])) {
2644
				if ($array[$lowest_id][$id]) {
2645
					if (strtolower($item[$id]) < strtolower($array[$lowest_id][$id])) {
2646
						$lowest_id = $index;
2647
					}
2648
				}
2649
			}
2650
			$index++;
2651
		}
2652
		$temp_array[] = $array[$lowest_id];
2653
		$array = array_merge(array_slice($array, 0, $lowest_id), array_slice($array, $lowest_id + 1));
2654
	}
2655
	if ($sort_ascending) {
2656
		return $temp_array;
2657
	} else {
2658
		return array_reverse($temp_array);
2659
	}
2660
}
2661

    
2662
/****f* util/is_URL
2663
 * NAME
2664
 *   is_URL
2665
 * INPUTS
2666
 *   $url: string to check
2667
 *   $httponly: Only allow HTTP or HTTPS scheme
2668
 * RESULT
2669
 *   Returns true if item is a URL
2670
 ******/
2671
function is_URL($url, $httponly = false) {
2672
	if (filter_var($url, FILTER_VALIDATE_URL)) {
2673
		if ($httponly) {
2674
			return in_array(strtolower(parse_url($url, PHP_URL_SCHEME)), ['http', 'https']);
2675
		}
2676
		return true;
2677
	}
2678
	return false;
2679
}
2680

    
2681
function is_file_included($file = "") {
2682
	$files = get_included_files();
2683
	if (in_array($file, $files)) {
2684
		return true;
2685
	}
2686

    
2687
	return false;
2688
}
2689

    
2690
/*
2691
 * Replace a value on a deep associative array using regex
2692
 */
2693
function array_replace_values_recursive($data, $match, $replace) {
2694
	if (empty($data)) {
2695
		return $data;
2696
	}
2697

    
2698
	if (is_string($data)) {
2699
		$data = preg_replace("/{$match}/", $replace, $data);
2700
	} else if (is_array($data)) {
2701
		foreach ($data as $k => $v) {
2702
			$data[$k] = array_replace_values_recursive($v, $match, $replace);
2703
		}
2704
	}
2705

    
2706
	return $data;
2707
}
2708

    
2709
/*
2710
	This function was borrowed from a comment on PHP.net at the following URL:
2711
	https://www.php.net/manual/en/function.array-merge-recursive.php#73843
2712
 */
2713
function array_merge_recursive_unique($array0, $array1) {
2714

    
2715
	$arrays = func_get_args();
2716
	$remains = $arrays;
2717

    
2718
	// We walk through each arrays and put value in the results (without
2719
	// considering previous value).
2720
	$result = array();
2721

    
2722
	// loop available array
2723
	foreach ($arrays as $array) {
2724

    
2725
		// The first remaining array is $array. We are processing it. So
2726
		// we remove it from remaining arrays.
2727
		array_shift($remains);
2728

    
2729
		// We don't care non array param, like array_merge since PHP 5.0.
2730
		if (is_array($array)) {
2731
			// Loop values
2732
			foreach ($array as $key => $value) {
2733
				if (is_array($value)) {
2734
					// we gather all remaining arrays that have such key available
2735
					$args = array();
2736
					foreach ($remains as $remain) {
2737
						if (array_key_exists($key, $remain)) {
2738
							array_push($args, $remain[$key]);
2739
						}
2740
					}
2741

    
2742
					if (count($args) > 2) {
2743
						// put the recursion
2744
						$result[$key] = call_user_func_array(__FUNCTION__, $args);
2745
					} else {
2746
						foreach ($value as $vkey => $vval) {
2747
							if (!is_array($result[$key])) {
2748
								$result[$key] = array();
2749
							}
2750
							$result[$key][$vkey] = $vval;
2751
						}
2752
					}
2753
				} else {
2754
					// simply put the value
2755
					$result[$key] = $value;
2756
				}
2757
			}
2758
		}
2759
	}
2760
	return $result;
2761
}
2762

    
2763

    
2764
/*
2765
 * converts a string like "a,b,c,d"
2766
 * into an array like array("a" => "b", "c" => "d")
2767
 */
2768
function explode_assoc($delimiter, $string) {
2769
	$array = explode($delimiter, $string);
2770
	$result = array();
2771
	$numkeys = floor(count($array) / 2);
2772
	for ($i = 0; $i < $numkeys; $i += 1) {
2773
		$result[$array[$i * 2]] = $array[$i * 2 + 1];
2774
	}
2775
	return $result;
2776
}
2777

    
2778
/*
2779
 * Given a string of text with some delimiter, look for occurrences
2780
 * of some string and replace all of those.
2781
 * $text - the text string (e.g. "abc,defg,x123,ipv4,xyz")
2782
 * $delimiter - the delimiter (e.g. ",")
2783
 * $element - the element to match (e.g. "defg")
2784
 * $replacement - the string to replace it with (e.g. "42")
2785
 * Returns the resulting delimited string (e.g. "abc,42,x123,ipv4,xyz")
2786
 */
2787
function replace_element_in_list($text, $delimiter, $element, $replacement) {
2788
	$textArray = explode($delimiter, $text);
2789
	while (($entry = array_search($element, $textArray)) !== false) {
2790
		$textArray[$entry] = $replacement;
2791
	}
2792
	return implode(',', $textArray);
2793
}
2794

    
2795
/* Return system's route table */
2796
function route_table() {
2797
	exec("/usr/bin/netstat --libxo json -nWr", $rawdata, $rc);
2798

    
2799
	if ($rc != 0) {
2800
		return array();
2801
	}
2802

    
2803
	$netstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
2804
	$netstatarr = $netstatarr['statistics']['route-information']
2805
	    ['route-table']['rt-family'];
2806

    
2807
	$result = array();
2808
	$result['inet'] = array();
2809
	$result['inet6'] = array();
2810
	foreach ($netstatarr as $item) {
2811
		if ($item['address-family'] == 'Internet') {
2812
			$result['inet'] = $item['rt-entry'];
2813
		} else if ($item['address-family'] == 'Internet6') {
2814
			$result['inet6'] = $item['rt-entry'];
2815
		}
2816
	}
2817
	unset($netstatarr);
2818

    
2819
	return $result;
2820
}
2821

    
2822
/* check if route is static (not BGP/OSPF) */
2823
function is_static_route($target, $ipprotocol = '') {
2824
	if (is_v4($target) || (($target == 'default') && ($ipprotocol == 'inet'))) {
2825
		$inet = '4';
2826
	} elseif (is_v6($target) || (($target == 'default') && ($ipprotocol == 'inet6'))) {
2827
		$inet = '6';
2828
	} else {
2829
		return false;
2830
	}
2831

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

    
2836
	return false;
2837
}
2838

    
2839
/* Get static route for specific destination */
2840
function route_get($target, $ipprotocol = '', $useroute = false) {
2841
	global $config;
2842

    
2843
	if (!empty($ipprotocol)) {
2844
		$family = $ipprotocol;
2845
	} else if (is_v4($target)) {
2846
		$family = 'inet';
2847
	} else if (is_v6($target)) {
2848
		$family = 'inet6';
2849
	}
2850

    
2851
	if (empty($family)) {
2852
		return array();
2853
	}
2854

    
2855
	if ($useroute) {
2856
		if ($family == 'inet') {
2857
			$inet = '4';
2858
		} else {
2859
			$inet = '6';
2860
		}
2861
		$interface = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/interface:/{print $2}'");
2862
		if (empty($interface)) {
2863
			return array();
2864
		} elseif ($interface == 'lo0') {
2865
			// interface assigned IP address
2866
			foreach (array_keys($config['interfaces']) as $intf) {
2867
				if ((($inet == '4') && (get_interface_ip($intf) == $target)) ||
2868
				    (($inet == '6') && (get_interface_ipv6($intf) == $target))) {
2869
					$interface = convert_friendly_interface_to_real_interface_name($intf);
2870
					$gateway = $interface;
2871
					break;
2872
				}
2873
			}
2874
		} else {
2875
			$gateway = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/gateway:/{print $2}'");
2876
			if (!$gateway) {
2877
				// non-local gateway
2878
				$gateway = get_interface_mac($interface);
2879
			}
2880
		}
2881
		$result[] = array('gateway' => $gateway, 'interface-name' => $interface);
2882
	} else {
2883
		$rtable = route_table();
2884
		if (empty($rtable)) {
2885
			return array();
2886
		}
2887

    
2888
		$result = array();
2889
		foreach ($rtable[$family] as $item) {
2890
			if ($item['destination'] == $target ||
2891
			    ip_in_subnet($target, $item['destination'])) {
2892
				$result[] = $item;
2893
			}
2894
		}
2895
	}
2896

    
2897
	return $result;
2898
}
2899

    
2900
/* Get default route */
2901
function route_get_default($ipprotocol) {
2902
	if (empty($ipprotocol) || ($ipprotocol != 'inet' &&
2903
	    $ipprotocol != 'inet6')) {
2904
		return '';
2905
	}
2906

    
2907
	$route = route_get('default', $ipprotocol, true);
2908

    
2909
	if (empty($route)) {
2910
		return '';
2911
	}
2912

    
2913
	if (!isset($route[0]['gateway'])) {
2914
		return '';
2915
	}
2916

    
2917
	return $route[0]['gateway'];
2918
}
2919

    
2920
/* Delete a static route */
2921
function route_del($target, $ipprotocol = '') {
2922
	global $config;
2923

    
2924
	if (empty($target)) {
2925
		return;
2926
	}
2927

    
2928
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
2929
	    $ipprotocol != 'inet6') {
2930
		return false;
2931
	}
2932

    
2933
	$route = route_get($target, $ipprotocol, true);
2934

    
2935
	if (empty($route)) {
2936
		return;
2937
	}
2938

    
2939
	$target_prefix = '';
2940
	if (is_subnet($target)) {
2941
		$target_prefix = '-net';
2942
	} else if (is_ipaddr($target)) {
2943
		$target_prefix = '-host';
2944
	}
2945

    
2946
	if (!empty($ipprotocol)) {
2947
		$target_prefix .= " -{$ipprotocol}";
2948
	} else if (is_v6($target)) {
2949
		$target_prefix .= ' -inet6';
2950
	} else if (is_v4($target)) {
2951
		$target_prefix .= ' -inet';
2952
	}
2953

    
2954
	foreach ($route as $item) {
2955
		if (substr($item['gateway'], 0, 5) == 'link#') {
2956
			continue;
2957
		}
2958

    
2959
		if (is_macaddr($item['gateway'])) {
2960
			$gw = '-iface ' . $item['interface-name'];
2961
		} else {
2962
			$gw = $item['gateway'];
2963
		}
2964

    
2965
		exec(escapeshellcmd("/sbin/route del {$target_prefix} " .
2966
		    "{$target} {$gw}"), $output, $rc);
2967

    
2968
		if (isset($config['system']['route-debug'])) {
2969
			log_error("ROUTING debug: " . microtime() .
2970
			    " - DEL RC={$rc} - {$target} - gw: " . $gw);
2971
			file_put_contents("/dev/console", "\n[" . getmypid() .
2972
			    "] ROUTE DEL: {$target_prefix} {$target} {$gw} " .
2973
			    "result: {$rc}");
2974
		}
2975
	}
2976
}
2977

    
2978
/*
2979
 * Add static route.  If it already exists, remove it and re-add
2980
 *
2981
 * $target - IP, subnet or 'default'
2982
 * $gw     - gateway address
2983
 * $iface  - Network interface
2984
 * $args   - Extra arguments for /sbin/route
2985
 * $ipprotocol - 'inet' or 'inet6'.  Mandatory when $target == 'default'
2986
 *
2987
 */
2988
function route_add_or_change($target, $gw, $iface = '', $args = '',
2989
    $ipprotocol = '') {
2990
	global $config;
2991

    
2992
	if (empty($target) || (empty($gw) && empty($iface))) {
2993
		return false;
2994
	}
2995

    
2996
	if ($target == 'default' && empty($ipprotocol)) {
2997
		return false;
2998
	}
2999

    
3000
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
3001
	    $ipprotocol != 'inet6') {
3002
		return false;
3003
	}
3004

    
3005
	/* use '-host' for IPv6 /128 routes, see https://redmine.pfsense.org/issues/11594 */
3006
	if (is_subnetv4($target) || (is_subnetv6($target) && (subnet_size($target) > 1))) {
3007
		$target_prefix = '-net';
3008
	} else if (is_ipaddr($target)) {
3009
		$target_prefix = '-host';
3010
	}
3011

    
3012
	if (!empty($ipprotocol)) {
3013
		$target_prefix .= " -{$ipprotocol}";
3014
	} else if (is_v6($target)) {
3015
		$target_prefix .= ' -inet6';
3016
	} else if (is_v4($target)) {
3017
		$target_prefix .= ' -inet';
3018
	}
3019

    
3020
	/* If there is another route to the same target, remove it */
3021
	route_del($target, $ipprotocol);
3022

    
3023
	$params = '';
3024
	if (!empty($iface) && does_interface_exist($iface)) {
3025
		$params .= " -iface {$iface}";
3026
	}
3027
	if (is_ipaddr($gw)) {
3028
		/* set correct linklocal gateway address,
3029
		 * see https://redmine.pfsense.org/issues/11713
3030
		 * and https://redmine.pfsense.org/issues/11806 */
3031
		if (is_ipaddrv6($gw) && is_linklocal($gw) && empty(get_ll_scope($gw))) {
3032
			$routeget = route_get($gw, 'inet6', true);
3033
			$gw .= "%" . $routeget[0]['interface-name'];
3034
		}
3035
		$params .= " " . $gw;
3036
	}
3037

    
3038
	if (empty($params)) {
3039
		log_error("route_add_or_change: Invalid gateway {$gw} and/or " .
3040
		    "network interface {$iface}");
3041
		return false;
3042
	}
3043

    
3044
	exec(escapeshellcmd("/sbin/route add {$target_prefix} " .
3045
	    "{$target} {$args} {$params}"), $output, $rc);
3046

    
3047
	if (isset($config['system']['route-debug'])) {
3048
		log_error("ROUTING debug: " . microtime() .
3049
		    " - ADD RC={$rc} - {$target} {$args}");
3050
		file_put_contents("/dev/console", "\n[" . getmypid() .
3051
		    "] ROUTE ADD: {$target_prefix} {$target} {$args} " .
3052
		    "{$params} result: {$rc}");
3053
	}
3054

    
3055
	return ($rc == 0);
3056
}
3057

    
3058
function set_ipv6routes_mtu($interface, $mtu) {
3059
	global $config;
3060

    
3061
	$ipv6mturoutes = array();
3062
	$if = convert_real_interface_to_friendly_interface_name($interface);
3063
	if (!$config['interfaces'][$if]['ipaddrv6']) {
3064
		return;
3065
	}
3066
	$a_gateways = return_gateways_array();
3067
	$a_staticroutes = get_staticroutes(false, false, true);
3068
	foreach ($a_gateways as $gate) {
3069
		foreach ($a_staticroutes as $sroute) {
3070
			if (($gate['interface'] == $interface) &&
3071
			    ($sroute['gateway'] == $gate['name']) &&
3072
			    (is_ipaddrv6($gate['gateway']))) {
3073
				$tgt = $sroute['network'];
3074
				$gateway = $gate['gateway'];
3075
				$ipv6mturoutes[$tgt] = $gateway;
3076
			}
3077
		}
3078
		if (($gate['interface'] == $interface) &&
3079
		    $gate['isdefaultgw'] && is_ipaddrv6($gate['gateway'])) {
3080
			$tgt = "default";
3081
			$gateway = $gate['gateway'];
3082
			$ipv6mturoutes[$tgt] = $gateway;
3083
		}
3084
	}
3085
	foreach ($ipv6mturoutes as $tgt => $gateway) {
3086
		mwexec("/sbin/route change -6 -mtu " . escapeshellarg($mtu) .
3087
		    " " . escapeshellarg($tgt) . " " .
3088
		    escapeshellarg($gateway));
3089
	}
3090
}
3091

    
3092
function alias_to_subnets_recursive($name, $returnhostnames = false) {
3093
	global $aliastable;
3094
	$result = array();
3095
	if (!isset($aliastable[$name])) {
3096
		return $result;
3097
	}
3098
	$subnets = preg_split('/\s+/', $aliastable[$name]);
3099
	foreach ($subnets as $net) {
3100
		if (is_alias($net)) {
3101
			$sub = alias_to_subnets_recursive($net, $returnhostnames);
3102
			$result = array_merge($result, $sub);
3103
			continue;
3104
		} elseif (!is_subnet($net)) {
3105
			if (is_ipaddrv4($net)) {
3106
				$net .= "/32";
3107
			} else if (is_ipaddrv6($net)) {
3108
				$net .= "/128";
3109
			} else if ($returnhostnames === false || !is_fqdn($net)) {
3110
				continue;
3111
			}
3112
		}
3113
		$result[] = $net;
3114
	}
3115
	return $result;
3116
}
3117

    
3118
function get_staticroutes($returnsubnetsonly = false, $returnhostnames = false, $returnenabledroutesonly = false) {
3119
	global $config;
3120

    
3121
	/* Bail if there are no routes, but return an array always so callers don't have to check. */
3122
	init_config_arr(array('staticroutes', 'route'));
3123
	if (empty($config['staticroutes']['route'])) {
3124
		return array();
3125
	}
3126

    
3127
	$allstaticroutes = array();
3128
	$allsubnets = array();
3129
	/* Loop through routes and expand aliases as we find them. */
3130
	foreach ($config['staticroutes']['route'] as $route) {
3131
		if ($returnenabledroutesonly && isset($route['disabled'])) {
3132
			continue;
3133
		}
3134

    
3135
		if (is_alias($route['network'])) {
3136
			foreach (alias_to_subnets_recursive($route['network'], $returnhostnames) as $net) {
3137
				$temproute = $route;
3138
				$temproute['network'] = $net;
3139
				$allstaticroutes[] = $temproute;
3140
				$allsubnets[] = $net;
3141
			}
3142
		} elseif (is_subnet($route['network'])) {
3143
			$allstaticroutes[] = $route;
3144
			$allsubnets[] = $route['network'];
3145
		}
3146
	}
3147
	if ($returnsubnetsonly) {
3148
		return $allsubnets;
3149
	} else {
3150
		return $allstaticroutes;
3151
	}
3152
}
3153

    
3154
/****f* util/get_alias_list
3155
 * NAME
3156
 *   get_alias_list - Provide a list of aliases.
3157
 * INPUTS
3158
 *   $type          - Optional, can be a string or array specifying what type(s) of aliases you need.
3159
 * RESULT
3160
 *   Array containing list of aliases.
3161
 *   If $type is unspecified, all aliases are returned.
3162
 *   If $type is a string, all aliases of the type specified in $type are returned.
3163
 *   If $type is an array, all aliases of any type specified in any element of $type are returned.
3164
 */
3165
function get_alias_list($type = null) {
3166
	$result = array();
3167
	foreach (config_get_path('aliases/alias', []) as $alias) {
3168
		if ($type === null) {
3169
			$result[] = $alias['name'];
3170
		} else if (is_array($type)) {
3171
			if (in_array($alias['type'], $type)) {
3172
				$result[] = $alias['name'];
3173
			}
3174
		} else if ($type === $alias['type']) {
3175
			$result[] = $alias['name'];
3176
		}
3177
	}
3178
	return $result;
3179
}
3180

    
3181
/* Returns the current alias contents sorted by name in case insensitive and
3182
 * natural order.
3183
 * https://redmine.pfsense.org/issues/14015 */
3184
function get_sorted_aliases() {
3185
	$aliases = config_get_path('aliases/alias', []);
3186
	uasort($aliases, function ($a, $b) { return strnatcasecmp($a['name'], $b['name']); });
3187
	return $aliases;
3188
}
3189

    
3190
/* returns an array consisting of every element of $haystack that is not equal to $needle. */
3191
function array_exclude($needle, $haystack) {
3192
	$result = array();
3193
	if (is_array($haystack)) {
3194
		foreach ($haystack as $thing) {
3195
			if ($needle !== $thing) {
3196
				$result[] = $thing;
3197
			}
3198
		}
3199
	}
3200
	return $result;
3201
}
3202

    
3203
/* Define what is preferred, IPv4 or IPv6 */
3204
function prefer_ipv4_or_ipv6() {
3205
	if (config_path_enabled('system', 'prefer_ipv4')) {
3206
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv4");
3207
	} else {
3208
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv6");
3209
	}
3210
}
3211

    
3212
/* Redirect to page passing parameters via POST */
3213
function post_redirect($page, $params) {
3214
	if (!is_array($params)) {
3215
		return;
3216
	}
3217

    
3218
	print "<html><body><form action=\"{$page}\" name=\"formredir\" method=\"post\">\n";
3219
	foreach ($params as $key => $value) {
3220
		print "<input type=\"hidden\" name=\"{$key}\" value=\"{$value}\" />\n";
3221
	}
3222
	print "</form>\n";
3223
	print "<script type=\"text/javascript\">\n";
3224
	print "//<![CDATA[\n";
3225
	print "document.formredir.submit();\n";
3226
	print "//]]>\n";
3227
	print "</script>\n";
3228
	print "</body></html>\n";
3229
}
3230

    
3231
/* Locate disks that can be queried for S.M.A.R.T. data. */
3232
function get_smart_drive_list() {
3233
	/* SMART supports some disks directly, and some controllers directly,
3234
	 * See https://redmine.pfsense.org/issues/9042 */
3235
	$supported_disk_types = array("ad", "da", "ada");
3236
	$supported_controller_types = array("nvme");
3237
	$disk_list = explode(" ", get_single_sysctl("kern.disks"));
3238
	foreach ($disk_list as $id => $disk) {
3239
		// We only want certain kinds of disks for S.M.A.R.T.
3240
		// 1 is a match, 0 is no match, False is any problem processing the regex
3241
		if (preg_match("/^(" . implode("|", $supported_disk_types) . ").*[0-9]{1,2}$/", $disk) !== 1) {
3242
			unset($disk_list[$id]);
3243
			continue;
3244
		}
3245
	}
3246
	foreach ($supported_controller_types as $controller) {
3247
		$devices = glob("/dev/{$controller}*");
3248
		if (!is_array($devices)) {
3249
			continue;
3250
		}
3251
		foreach ($devices as $device) {
3252
			$disk_list[] = basename($device);
3253
		}
3254
	}
3255
	sort($disk_list);
3256
	return $disk_list;
3257
}
3258

    
3259
// Validate a network address
3260
//	$addr: the address to validate
3261
//	$type: IPV4|IPV6|IPV4V6
3262
//	$label: the label used by the GUI to display this value. Required to compose an error message
3263
//	$err_msg: pointer to the callers error message array so that error messages can be added to it here
3264
//	$alias: are aliases permitted for this address?
3265
// Returns:
3266
//	IPV4 - if $addr is a valid IPv4 address
3267
//	IPV6 - if $addr is a valid IPv6 address
3268
//	ALIAS - if $alias=true and $addr is an alias
3269
//	false - otherwise
3270

    
3271
function validateipaddr(&$addr, $type, $label, &$err_msg, $alias=false) {
3272
	switch ($type) {
3273
		case IPV4:
3274
			if (is_ipaddrv4($addr)) {
3275
				return IPV4;
3276
			} else if ($alias) {
3277
				if (is_alias($addr)) {
3278
					return ALIAS;
3279
				} else {
3280
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address or alias."), $label);
3281
					return false;
3282
				}
3283
			} else {
3284
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address."), $label);
3285
				return false;
3286
			}
3287
		break;
3288
		case IPV6:
3289
			if (is_ipaddrv6($addr)) {
3290
				$addr = strtolower($addr);
3291
				return IPV6;
3292
			} else if ($alias) {
3293
				if (is_alias($addr)) {
3294
					return ALIAS;
3295
				} else {
3296
					$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address or alias."), $label);
3297
					return false;
3298
				}
3299
			} else {
3300
				$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address."), $label);
3301
				return false;
3302
			}
3303
		break;
3304
		case IPV4V6:
3305
			if (is_ipaddrv6($addr)) {
3306
				$addr = strtolower($addr);
3307
				return IPV6;
3308
			} else if (is_ipaddrv4($addr)) {
3309
				return IPV4;
3310
			} else if ($alias) {
3311
				if (is_alias($addr)) {
3312
					return ALIAS;
3313
				} else {
3314
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address or alias."), $label);
3315
					return false;
3316
				}
3317
			} else {
3318
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address."), $label);
3319
				return false;
3320
			}
3321
		break;
3322
	}
3323

    
3324
	return false;
3325
}
3326

    
3327
/* From DUID configuration inputs, format a string that looks (more) like the expected raw DUID format:
3328
 * 1) For DUIDs entered as a known DUID type, convert to a hexstring and prepend the DUID number, after having done the following:
3329
 *     a) For DUID-LLT and DUID-EN, convert the time/enterprise ID input to hex and append the link-layer address/identifier input.
3330
 *     b) For DUID-LLT and DUID-LL, prepend a hardware type of 1.
3331
 *     c) For DUID-UUID, remove any "-".
3332
 * 2) Replace any remaining "-" with ":".
3333
 * 3) If any components are input with just a single char (hex digit hopefully), put a "0" in front.
3334
 * 4) The first two components should be a 16-bit integer (little- or big-endian, depending on the current machine type) that
3335
 *    is equal to the number of other components. If not, prepend this as "nn:00" (all pfSense builds are little-endian).
3336
 *    This is convenience, because the DUID reported by dhcp6c in logs does not include this count, which corresponds to the
3337
 *    option-len field of DHCPv6's OPTION_CLIENTID option.
3338
 *
3339
 * The final result should be closer to:
3340
 *
3341
 * "nn:00:00:0n:nn:nn:nn:..."
3342
 *
3343
 * This function does not validate the input. is_duid() will do validation.
3344
 */
3345
function format_duid($duidtype, $duidpt1, $duidpt2=null) {
3346
	if ($duidpt2)
3347
		$duidpt1 = implode(':', str_split(str_pad(dechex($duidpt1), 8, '0', STR_PAD_LEFT), 2)) . ':' . $duidpt2;
3348

    
3349
	/* Make hexstrings */
3350
	if ($duidtype) {
3351
		switch ($duidtype) {
3352
		/* Add a hardware type to DUID-LLT and DUID-LL; assume Ethernet */
3353
		case 1:
3354
		case 3:
3355
			$duidpt1 = '00:01:' . $duidpt1;
3356
			break;
3357
		/* Remove '-' from given UUID and insert ':' every 2 characters */
3358
		case 4:
3359
			$duidpt1 = implode(':', str_split(str_replace('-', '', $duidpt1), 2));
3360
			break;
3361
		default:
3362
		}
3363
		$duidpt1 = '00:0' . $duidtype . ':' . $duidpt1;
3364
	}
3365

    
3366
	$values = explode(':', strtolower(str_replace('-', ':', $duidpt1)));
3367

    
3368
	if (hexdec($values[0]) != count($values) - 2)
3369
		array_unshift($values, dechex(count($values)), '00');
3370

    
3371
	array_walk($values, function(&$value) {
3372
		$value = str_pad($value, 2, '0', STR_PAD_LEFT);
3373
	});
3374

    
3375
	return implode(":", $values);
3376
}
3377

    
3378
/* Returns true if $dhcp6duid is a valid DUID entry.
3379
 * Parse the entry to check for valid length according to known DUID types.
3380
 */
3381
function is_duid($dhcp6duid) {
3382
	$values = explode(":", $dhcp6duid);
3383
	if (hexdec($values[0]) == count($values) - 2) {
3384
		switch (hexdec($values[2] . $values[3])) {
3385
		case 0:
3386
			return false;
3387
			break;
3388
		case 1:
3389
			if (count($values) != 16 || strlen($dhcp6duid) != 47)
3390
				return false;
3391
			break;
3392
		case 3:
3393
			if (count($values) != 12 || strlen($dhcp6duid) != 35)
3394
				return false;
3395
			break;
3396
		case 4:
3397
			if (count($values) != 20 || strlen($dhcp6duid) != 59)
3398
				return false;
3399
			break;
3400
		/* DUID is up to 128 octets; allow 2 octets for type code, 2 more for option-len */
3401
		default:
3402
			if (count($values) > 132 || strlen($dhcp6duid) != count($values) * 3 - 1)
3403
				return false;
3404
		}
3405
	} else
3406
		return false;
3407

    
3408
	for ($i = 0; $i < count($values); $i++) {
3409
		if (ctype_xdigit($values[$i]) == false)
3410
			return false;
3411
		if (hexdec($values[$i]) < 0 || hexdec($values[$i]) > 255)
3412
			return false;
3413
	}
3414

    
3415
	return true;
3416
}
3417

    
3418
/* Write the DHCP6 DUID file */
3419
function write_dhcp6_duid($duidstring) {
3420
	// Create the hex array from the dhcp6duid config entry and write to file
3421
	global $g;
3422

    
3423
	if(!is_duid($duidstring)) {
3424
		log_error(gettext("Error: attempting to write DUID file - Invalid DUID detected"));
3425
		return false;
3426
	}
3427
	$temp = str_replace(":","",$duidstring);
3428
	$duid_binstring = pack("H*",$temp);
3429
	if ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "wb")) {
3430
		fwrite($fd, $duid_binstring);
3431
		fclose($fd);
3432
		return true;
3433
	}
3434
	log_error(gettext("Error: attempting to write DUID file - File write error"));
3435
	return false;
3436
}
3437

    
3438
/* returns duid string from 'vardb_path']}/dhcp6c_duid' */
3439
function get_duid_from_file() {
3440
	global $g;
3441

    
3442
	$duid_ASCII = "";
3443
	$count = 0;
3444

    
3445
	if (file_exists("{$g['vardb_path']}/dhcp6c_duid") &&
3446
	    ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "r"))) {
3447
		$fsize = filesize("{$g['vardb_path']}/dhcp6c_duid");
3448
		if ($fsize <= 132) {
3449
			$buffer = fread($fd, $fsize);
3450
			while($count < $fsize) {
3451
				$duid_ASCII .= bin2hex($buffer[$count]);
3452
				$count++;
3453
				if($count < $fsize) {
3454
					$duid_ASCII .= ":";
3455
				}
3456
			}
3457
		}
3458
		fclose($fd);
3459
	}
3460
	//if no file or error with read then the string returns blanked DUID string
3461
	if(!is_duid($duid_ASCII)) {
3462
		return "--:--:--:--:--:--:--:--:--:--:--:--:--:--:--:--";
3463
	}
3464
	return($duid_ASCII);
3465
}
3466

    
3467
/* Replaces the Mac OS 9 and earlier (\r) and DOS/Windows (\r\n) newlines with the Unix equivalent (\n). */
3468
function unixnewlines($text) {
3469
	return preg_replace('/\r\n?/', "\n", $text);
3470
}
3471

    
3472
function array_remove_duplicate($array, $field) {
3473
	$cmp = array();
3474
	foreach ($array as $sub) {
3475
		if (isset($sub[$field])) {
3476
			$cmp[] = $sub[$field];
3477
		}
3478
	}
3479
	$unique = array_unique(array_reverse($cmp, true));
3480
	foreach (array_keys($unique) as $k) {
3481
		$new[] = $array[$k];
3482
	}
3483
	return $new;
3484
}
3485

    
3486
/**
3487
 * Return a value specified by a path of keys in a nested array, if it exists.
3488
 * @param $arr array value to search
3489
 * @param $path string path with '/' separators
3490
 * @param $default mixed value to return if the path is not found
3491
 * @returns mixed value at path or $default if the path does not exist or if the
3492
 *          path keys an empty string and $default is non-null
3493
 */
3494
function array_get_path(array &$arr, string $path, $default = null) {
3495
	$vpath = explode('/', $path);
3496
	$el = $arr;
3497
	foreach ($vpath as $key) {
3498
		if (mb_strlen($key) == 0) {
3499
			continue;
3500
		}
3501
		if (is_array($el) && array_key_exists($key, $el)) {
3502
			$el = $el[$key];
3503
		} else {
3504
			return ($default);
3505
		}
3506
	}
3507

    
3508
	if (($default !== null) && ($el === '')) {
3509
		return ($default);
3510
	}
3511

    
3512
	return ($el);
3513
}
3514

    
3515
/*
3516
 * Initialize an arbitrary array multiple levels deep only if unset
3517
 * @param $arr top of array
3518
 * @param $path string path with '/' separators
3519
 */
3520
function array_init_path(mixed &$arr, ?string $path)
3521
{
3522
	if (!is_array($arr)) {
3523
		$arr = [];
3524
	}
3525
	if (is_null($path)) {
3526
		return;
3527
	}
3528
	$tmp = &$arr;
3529
	foreach (explode('/', $path) as $key) {
3530
		if (!is_array($tmp[$key])) {
3531
			$tmp[$key] = [];
3532
		}
3533
		$tmp = &$tmp[$key];
3534
	}
3535
}
3536

    
3537
/**
3538
 * Set a value by path in a nested array, creating arrays for intermediary keys
3539
 * as necessary. If the path cannot be reached because an intermediary exists
3540
 * but is not empty or an array, return $default.
3541
 * @param $arr array value to search
3542
 * @param $path string path with '/' separators
3543
 * @param $value mixed
3544
 * @param $default mixed value to return if the path is not found
3545
 * @returns mixed $val or $default if the path prefix does not exist
3546
 */
3547
function array_set_path(array &$arr, string $path, $value, $default = null) {
3548
	$vpath = explode('/', $path);
3549
	$vkey = null;
3550
	do {
3551
		$vkey = array_pop($vpath);
3552
	} while (mb_strlen($vkey) == 0);
3553
	if ($vkey == null) {
3554
		return ($default);
3555
	}
3556
	$el =& $arr;
3557
	foreach ($vpath as $key) {
3558
		if (mb_strlen($key) == 0) {
3559
			continue;
3560
		}
3561
		if (array_key_exists($key, $el) && !empty($el[$key])) {
3562
			if (!is_array($el[$key])) {
3563
					return ($default);
3564
			}
3565
		} else {
3566
				$el[$key] = [];
3567
		}
3568
		$el =& $el[$key];
3569
	}
3570
	$el[$vkey] = $value;
3571
	return ($value);
3572
}
3573

    
3574
/**
3575
 * Determine whether a path in a nested array has a non-null value keyed by
3576
 * $enable_key.
3577
 * @param $arr array value to search
3578
 * @param $path string path with '/' separators
3579
 * @param $enable_key string an optional alternative key value for the enable key
3580
 * @returns bool true if $enable_key exists in the array at $path, and has a
3581
 * non-null value, otherwise false
3582
 */
3583
function array_path_enabled(array &$arr, string $path, $enable_key = "enable") {
3584
	$el = array_get_path($arr, $path, []);
3585
	if (is_array($el) && isset($el[$enable_key])) {
3586
		return (true);
3587
	}
3588
	return (false);
3589
}
3590

    
3591
/**
3592
 * Remove a key from the nested array by path.
3593
 * @param $arr array value to search
3594
 * @param $path string path with '/' separators
3595
 * @returns array copy of the removed value or null
3596
 */
3597
function array_del_path(array &$arr, string $path) {
3598
	$vpath = explode('/', $path);
3599
	$vkey = array_pop($vpath);
3600
	$el =& $arr;
3601
	foreach($vpath as $key) {
3602
		if (mb_strlen($key) == 0) {
3603
			continue;
3604
		}
3605
		if (is_array($el) && array_key_exists($key, $el)) {
3606
			$el =& $el[$key];
3607
		} else {
3608
			return null;
3609
		}
3610
	}
3611

    
3612
	if (!(is_array($el) && array_key_exists($vkey, $el))) {
3613
		return null;
3614
	}
3615

    
3616
	$ret = $el[$vkey];
3617
	unset($el[$vkey]);
3618
	return ($ret);
3619
}
3620

    
3621

    
3622
function dhcpd_date_adjust_gmt($dt) {
3623
	init_config_arr(array('dhcpd'));
3624

    
3625
	foreach (config_get_path('dhcpd', []) as $dhcpditem) {
3626
		if (empty($dhcpditem)) {
3627
			continue;
3628
		}
3629
		if ($dhcpditem['dhcpleaseinlocaltime'] == "yes") {
3630
			$ts = strtotime($dt . " GMT");
3631
			if ($ts !== false) {
3632
				return strftime("%Y/%m/%d %H:%M:%S", $ts);
3633
			}
3634
		}
3635
	}
3636

    
3637
	/*
3638
	 * If we did not need to convert to local time or the conversion
3639
	 * failed, just return the input.
3640
	 */
3641
	return $dt;
3642
}
3643

    
3644
global $supported_image_types;
3645
$supported_image_types = array(
3646
	IMAGETYPE_JPEG,
3647
	IMAGETYPE_PNG,
3648
	IMAGETYPE_GIF,
3649
	IMAGETYPE_WEBP
3650
);
3651

    
3652
function is_supported_image($image_filename) {
3653
	global $supported_image_types;
3654
	$img_info = getimagesize($image_filename);
3655

    
3656
	/* If it's not an image, or it isn't in the supported list, return false */
3657
	if (($img_info === false) ||
3658
	    !in_array($img_info[2], array_keys($supported_image_types))) {
3659
		return false;
3660
	} else {
3661
		return $img_info[2];
3662
	}
3663
}
3664

    
3665
function get_lagg_ports ($laggport) {
3666
	$laggp = array();
3667
	foreach ($laggport as $lgp) {
3668
		list($lpname, $lpinfo) = explode(" ", $lgp);
3669
		preg_match('~<(.+)>~', $lpinfo, $lgportmode);
3670
		if ($lgportmode[1]) {
3671
			$laggp[] = $lpname . " (" . $lgportmode[1] . ")";
3672
		} else {
3673
			$laggp[] = $lpname;
3674
		}
3675
	}
3676
	if ($laggp) {
3677
		return implode(", ", $laggp);
3678
	} else {
3679
		return false;
3680
	}
3681
}
3682

    
3683
function cisco_to_cidr($addr) {
3684
	if (!is_ipaddr($addr)) {
3685
		throw new Exception('Value is not in dotted quad notation.');
3686
	}
3687

    
3688
	$mask = decbin(~ip2long($addr));
3689
	$mask = substr($mask, -32);
3690
	$k = 0;
3691
	for ($i = 0; $i <= 32; $i++) {
3692
		$k += intval($mask[$i]);
3693
	}
3694
	return $k;
3695
}
3696

    
3697
function cisco_extract_index($prule) {
3698
	$index = explode("#", $prule);
3699
	if (is_numeric($index[1])) {
3700
		return intval($index[1]);
3701
	} else {
3702
		syslog(LOG_WARNING, "Error parsing rule {$prule}: Could not extract index");
3703
	}
3704
	return -1;;
3705
}
3706

    
3707
function parse_cisco_acl_rule($rule, $devname, $dir, $proto) {
3708
	$rule_orig = $rule;
3709
	$rule = explode(" ", $rule);
3710
	$tmprule = "";
3711
	$index = 0;
3712

    
3713
	if ($rule[$index] == "permit") {
3714
		$startrule = "pass {$dir} quick on {$devname} ";
3715
	} else if ($rule[$index] == "deny") {
3716
		$startrule = "block {$dir} quick on {$devname} ";
3717
	} else {
3718
		return;
3719
	}
3720

    
3721
	$index++;
3722

    
3723
	switch ($rule[$index]) {
3724
		case "ip":
3725
			break;
3726
		case "icmp":
3727
			$icmp = ($proto == "inet") ? "icmp" : "ipv6-icmp";
3728
			$tmprule .= "proto {$icmp} ";
3729
			break;
3730
		case "tcp":
3731
		case "udp":
3732
			$tmprule .= "proto {$rule[$index]} ";
3733
			break;
3734
		default:
3735
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid protocol.");
3736
			return;
3737
	}
3738
	$index++;
3739

    
3740
	/* Source */
3741
	if (trim($rule[$index]) == "host") {
3742
		$index++;
3743
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
3744
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
3745
			if ($GLOBALS['attributes']['framed_ip']) {
3746
				$tmprule .= "from {$GLOBALS['attributes']['framed_ip']} ";
3747
			} else {
3748
				$tmprule .= "from {$rule[$index]} ";
3749
			}
3750
			$index++;
3751
		} else {
3752
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source host '{$rule[$index]}'.");
3753
			return;
3754
		}
3755
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
3756
		$tmprule .= "from {$rule[$index]} ";
3757
		$index++;
3758
	} elseif (trim($rule[$index]) == "any") {
3759
		$tmprule .= "from any ";
3760
		$index++;
3761
	} else {
3762
		$network = $rule[$index];
3763
		$netmask = $rule[++$index];
3764

    
3765
		if (is_ipaddrv4($network) && ($proto == "inet")) {
3766
			try {
3767
				$netmask = cisco_to_cidr($netmask);
3768
			} catch(Exception $e) {
3769
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source netmask '$netmask' (" . $e->getMessage() . ").");
3770
				return;
3771
			}
3772
			$tmprule .= "from {$network}/{$netmask} ";
3773
		} else {
3774
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source network '$network'.");
3775
			return;
3776
		}
3777

    
3778
		$index++;
3779
	}
3780

    
3781
	/* Source Operator */
3782
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
3783
		switch(trim($rule[$index])) {
3784
			case "lt":
3785
				$operator = "<";
3786
				break;
3787
			case "gt":
3788
				$operator = ">";
3789
				break;
3790
			case "eq":
3791
				$operator = "=";
3792
				break;
3793
			case "neq":
3794
				$operator = "!=";
3795
				break;
3796
		}
3797

    
3798
		$port = $rule[++$index];
3799
		if (is_port($port)) {
3800
			$tmprule .= "port {$operator} {$port} ";
3801
		} else {
3802
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source port: '$port' not a numeric value between 0 and 65535.");
3803
			return;
3804
		}
3805
		$index++;
3806
	} else if (trim($rule[$index]) == "range") {
3807
		$port = array($rule[++$index], $rule[++$index]);
3808
		if (is_port($port[0]) && is_port($port[1])) {
3809
			$tmprule .= "port {$port[0]}:{$port[1]} ";
3810
		} else {
3811
			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.");
3812
			return;
3813
		}
3814
		$index++;
3815
	}
3816

    
3817
	/* Destination */
3818
	if (trim($rule[$index]) == "host") {
3819
		$index++;
3820
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
3821
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
3822
			$tmprule .= "to {$rule[$index]} ";
3823
			$index++;
3824
		} else {
3825
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination host '{$rule[$index]}'.");
3826
			return;
3827
		}
3828
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
3829
		$tmprule .= "to {$rule[$index]} ";
3830
		$index++;
3831
	} elseif (trim($rule[$index]) == "any") {
3832
		$tmprule .= "to any ";
3833
		$index++;
3834
	} else {
3835
		$network = $rule[$index];
3836
		$netmask = $rule[++$index];
3837

    
3838
		if (is_ipaddrv4($network) && ($proto == "inet")) {
3839
			try {
3840
				$netmask = cisco_to_cidr($netmask);
3841
			} catch(Exception $e) {
3842
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination netmask '$netmask' (" . $e->getMessage() . ").");
3843
				return;
3844
			}
3845
			$tmprule .= "to {$network}/{$netmask} ";
3846
		} else {
3847
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination network '$network'.");
3848
			return;
3849
		}
3850

    
3851
		$index++;
3852
	}
3853

    
3854
	/* Destination Operator */
3855
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
3856
		switch(trim($rule[$index])) {
3857
			case "lt":
3858
				$operator = "<";
3859
				break;
3860
			case "gt":
3861
				$operator = ">";
3862
				break;
3863
			case "eq":
3864
				$operator = "=";
3865
				break;
3866
			case "neq":
3867
				$operator = "!=";
3868
				break;
3869
		}
3870

    
3871
		$port = $rule[++$index];
3872
		if (is_port($port)) {
3873
			$tmprule .= "port {$operator} {$port} ";
3874
		} else {
3875
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination port: '$port' not a numeric value between 0 and 65535.");
3876
			return;
3877
		}
3878
		$index++;
3879
	} else if (trim($rule[$index]) == "range") {
3880
		$port = array($rule[++$index], $rule[++$index]);
3881
		if (is_port($port[0]) && is_port($port[1])) {
3882
			$tmprule .= "port {$port[0]}:{$port[1]} ";
3883
		} else {
3884
			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.");
3885
			return;
3886
		}
3887
		$index++;
3888
	}
3889

    
3890
	$tmprule = $startrule . $proto . " " . $tmprule;
3891
	return $tmprule;
3892
}
3893

    
3894
function parse_cisco_acl($attribs, $dev) {
3895
	global $attributes;
3896

    
3897
	if (!is_array($attribs)) {
3898
		return "";
3899
	}
3900
	$finalrules = "";
3901
	if (is_array($attribs['ciscoavpair'])) {
3902
		$inrules = array('inet' => array(), 'inet6' => array());
3903
		$outrules = array('inet' => array(), 'inet6' => array());
3904
		foreach ($attribs['ciscoavpair'] as $avrules) {
3905
			$rule = explode("=", $avrules);
3906
			$dir = "";
3907
			if (strstr($rule[0], "inacl")) {
3908
				$dir = "in";
3909
			} else if (strstr($rule[0], "outacl")) {
3910
				$dir = "out";
3911
			} else if (strstr($rule[0], "dns-servers")) {
3912
				$attributes['dns-servers'] = explode(" ", $rule[1]);
3913
				continue;
3914
			} else if (strstr($rule[0], "route")) {
3915
				if (!is_array($attributes['routes'])) {
3916
					$attributes['routes'] = array();
3917
				}
3918
				$attributes['routes'][] = $rule[1];
3919
				continue;
3920
			}
3921
			$rindex = cisco_extract_index($rule[0]);
3922
			if ($rindex < 0) {
3923
				continue;
3924
			}
3925

    
3926
			if (strstr($rule[0], "ipv6")) {
3927
				$proto = "inet6";
3928
			} else {
3929
				$proto = "inet";
3930
			}
3931

    
3932
			$tmprule = parse_cisco_acl_rule($rule[1], $dev, $dir, $proto);
3933
			if (!empty($tmprule)) {
3934
				if ($dir == "in") {
3935
					$inrules[$proto][$rindex] = $tmprule;
3936
				} else if ($dir == "out") {
3937
					$outrules[$proto][$rindex] = $tmprule;
3938
				}
3939
			}
3940
		}
3941

    
3942

    
3943
		$state = "";
3944
		foreach (array('inet', 'inet6') as $ip) {
3945
			if (!empty($outrules[$ip])) {
3946
				$state = "no state";
3947
			}
3948
			ksort($inrules[$ip], SORT_NUMERIC);
3949
			foreach ($inrules[$ip] as $inrule) {
3950
				$finalrules .= "{$inrule} {$state}\n";
3951
			}
3952
			if (!empty($outrules[$ip])) {
3953
				ksort($outrules[$ip], SORT_NUMERIC);
3954
				foreach ($outrules[$ip] as $outrule) {
3955
					$finalrules .= "{$outrule} {$state}\n";
3956
				}
3957
			}
3958
		}
3959
	}
3960
	return $finalrules;
3961
}
3962

    
3963
function alias_idn_to_utf8($alias) {
3964
	if (is_alias($alias)) {
3965
		return $alias;
3966
	} else {
3967
		return idn_to_utf8($alias);
3968
	}
3969
}
3970

    
3971
function alias_idn_to_ascii($alias) {
3972
	if (is_alias($alias)) {
3973
		return $alias;
3974
	} else {
3975
		return idn_to_ascii($alias);
3976
	}
3977
}
3978

    
3979
// These functions were in guiconfig.inc but have been moved here so non GUI processes can use them
3980
function address_to_pconfig($adr, &$padr, &$pmask, &$pnot, &$pbeginport, &$pendport) {
3981
	if (isset($adr['any'])) {
3982
		$padr = "any";
3983
	} else if ($adr['network']) {
3984
		$padr = $adr['network'];
3985
	} else if ($adr['address']) {
3986
		list($padr, $pmask) = explode("/", $adr['address']);
3987
		if (!$pmask) {
3988
			if (is_ipaddrv6($padr)) {
3989
				$pmask = 128;
3990
			} else {
3991
				$pmask = 32;
3992
			}
3993
		}
3994
	}
3995

    
3996
	if (isset($adr['not'])) {
3997
		$pnot = 1;
3998
	} else {
3999
		$pnot = 0;
4000
	}
4001

    
4002
	if ($adr['port']) {
4003
		list($pbeginport, $pendport) = explode("-", $adr['port']);
4004
		if (!$pendport) {
4005
			$pendport = $pbeginport;
4006
		}
4007
	} else if (!is_alias($pbeginport) && !is_alias($pendport)) {
4008
		$pbeginport = "any";
4009
		$pendport = "any";
4010
	}
4011
}
4012

    
4013
function pconfig_to_address(&$adr, $padr, $pmask, $pnot = false, $pbeginport = 0, $pendport = 0, $addmask = false) {
4014
	$adr = array();
4015

    
4016
	if ($padr == "any") {
4017
		$adr['any'] = true;
4018
	} else if (get_specialnet($padr)) {
4019
		if ($addmask) {
4020
			$padr .= "/" . $pmask;
4021
		}
4022
		$adr['network'] = $padr;
4023
	} else {
4024
		$adr['address'] = $padr;
4025
		if (is_ipaddrv6($padr)) {
4026
			if ($pmask != 128) {
4027
				$adr['address'] .= "/" . $pmask;
4028
			}
4029
		} else {
4030
			if ($pmask != 32) {
4031
				$adr['address'] .= "/" . $pmask;
4032
			}
4033
		}
4034
	}
4035

    
4036
	if ($pnot) {
4037
		$adr['not'] = true;
4038
	} else {
4039
		unset($adr['not']);
4040
	}
4041

    
4042
	if (($pbeginport != 0) && ($pbeginport != "any")) {
4043
		if ($pbeginport != $pendport) {
4044
			$adr['port'] = $pbeginport . "-" . $pendport;
4045
		} else {
4046
			$adr['port'] = $pbeginport;
4047
		}
4048
	}
4049

    
4050
	/*
4051
	 * If the port is still unset, then it must not be numeric, but could
4052
	 * be an alias or a well-known/registered service.
4053
	 * See https://redmine.pfsense.org/issues/8410
4054
	 */
4055
	if (!isset($adr['port']) && is_port_or_alias($pbeginport)) {
4056
		$adr['port'] = $pbeginport;
4057
	}
4058
}
4059

    
4060
function get_specialnet(?string $net = '', array $flags = [], ?string $if = ''):bool|array {
4061
	if ($net === null || $if === null) {
4062
		return false;
4063
	}
4064
	$checkpermission = false;
4065
	$allnetflags = [SPECIALNET_ANY, SPECIALNET_SELF, SPECIALNET_CLIENTS,
4066
	                SPECIALNET_IFADDR, SPECIALNET_IFSUB, SPECIALNET_IFNET,
4067
	                SPECIALNET_GROUP, SPECIALNET_VIPS];
4068

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

    
4224
	if (empty($net)) {
4225
		return $specialnet;
4226
	} elseif (array_key_exists($net, $specialnet)) {
4227
		return true;
4228
	} else {
4229
		return false;
4230
	}
4231
}
4232

    
4233
function is_interface_ipaddr($interface) {
4234
	if (!empty(config_get_path("interfaces/{$interface}/ipaddr"))) {
4235
		return true;
4236
	}
4237
	return false;
4238
}
4239

    
4240
function is_interface_ipaddrv6($interface) {
4241
	if (!empty(config_get_path("interfaces/{$interface}/ipaddrv6"))) {
4242
		return true;
4243
	}
4244
	return false;
4245
}
4246

    
4247
function escape_filter_regex($filtertext) {
4248
	/* If the caller (user) has not already put a backslash before a slash, to escape it in the regex, */
4249
	/* then this will do it. Take out any "\/" already there, then turn all ordinary "/" into "\/".    */
4250
	return str_replace('/', '\/', str_replace('\/', '/', $filtertext));
4251
}
4252

    
4253
/*
4254
 * Check if a given pattern has the same number of two different unescaped
4255
 * characters.
4256
 * For example, it can ensure a pattern has balanced sets of parentheses,
4257
 * braces, and brackets.
4258
 */
4259
function is_pattern_balanced_char($pattern, $open, $close) {
4260
	/* First remove escaped versions */
4261
	$pattern = str_replace('\\' . $open, '', $pattern);
4262
	$pattern = str_replace('\\' . $close, '', $pattern);
4263
	/* Check if the counts of both characters match in the target pattern */
4264
	return (substr_count($pattern, $open) == substr_count($pattern, $close));
4265
}
4266

    
4267
/*
4268
 * Check if a pattern contains balanced sets of parentheses, braces, and
4269
 * brackets.
4270
 */
4271
function is_pattern_balanced($pattern) {
4272
	if (is_pattern_balanced_char($pattern, '(', ')') &&
4273
	    is_pattern_balanced_char($pattern, '{', '}') &&
4274
	    is_pattern_balanced_char($pattern, '[', ']')) {
4275
		/* Balanced if all are true */
4276
		return true;
4277
	}
4278
	return false;
4279
}
4280

    
4281
function cleanup_regex_pattern($filtertext) {
4282
	/* Cleanup filter to prevent backreferences. */
4283
	$filtertext = escape_filter_regex($filtertext);
4284

    
4285
	/* Remove \<digit>+ backreferences
4286
	 * To match \ it must be escaped as \\\\ in PHP for preg_replace() */
4287
	$filtertext = preg_replace('/\\\\\\d+/', '', $filtertext);
4288

    
4289
	/* Check for unbalanced parentheses, braces, and brackets which
4290
	 * may be an error or attempt to circumvent protections.
4291
	 * Also discard any pattern that attempts problematic duplication
4292
	 * methods. */
4293
	if (!is_pattern_balanced($filtertext) ||
4294
	    (substr_count($filtertext, ')*') > 0) ||
4295
	    (substr_count($filtertext, ')+') > 0) ||
4296
	    (substr_count($filtertext, '{') > 0)) {
4297
		return '';
4298
	}
4299

    
4300
	return $filtertext;
4301
}
4302

    
4303
function ip6_to_asn1($addr) {
4304
	/* IPv6 MIB uses an OCTET STRING of length 16 to represent
4305
	 * 128-bit IPv6 address in network byte order.
4306
	 * see https://datatracker.ietf.org/doc/html/rfc2465#section-3
4307
	 * i.e. fc00:3::4 = 252.0.0.3.0.0.0.0.0.0.0.0.0.0.0.4
4308
	 */
4309

    
4310
	if (!is_ipaddrv6($addr)) {
4311
		return false;
4312
	}
4313
	$ipv6str = "";
4314
	$octstr = "";
4315
	foreach (explode(':', Net_IPv6::uncompress($addr)) as $v) {
4316
		$ipv6str .= str_pad($v, 4, '0', STR_PAD_LEFT);
4317
	}
4318
	foreach (str_split($ipv6str, 2) as $v) {
4319
		$octstr .= base_convert($v, 16, 10) . '.';
4320
	}
4321

    
4322
	return $octstr;
4323
}
4324

    
4325
function interfaces_interrupts() {
4326
	exec("/usr/bin/vmstat -i --libxo json", $rawdata, $rc);
4327
	$interrupts = array();
4328
	if ($rc == 0) {
4329
		$vnstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
4330
		$interruptarr = $vnstatarr['interrupt-statistics']['interrupt'];
4331

    
4332
		foreach ($interruptarr as $int){
4333
			preg_match("/irq\d+: ([a-z0-9]+)/", $int['name'], $matches);
4334
			$name = $matches[1];
4335
			if (array_key_exists($name, $interrupts)) {
4336
				/* interface with multiple queues */
4337
				$interrupts[$name]['total'] += $int['total'];
4338
				$interrupts[$name]['rate'] += $int['rate'];
4339
			} else {
4340
				$interrupts[$name]['total'] = $int['total'];
4341
				$interrupts[$name]['rate'] = $int['rate'];
4342
			}
4343
		}
4344
	}
4345

    
4346
	return $interrupts;
4347
}
4348

    
4349
function dummynet_load_module($max_qlimit) {
4350
	if (!is_module_loaded("dummynet.ko")) {
4351
		mute_kernel_msgs();
4352
		mwexec("/sbin/kldload dummynet");
4353
		unmute_kernel_msgs();
4354
	}
4355
	$sysctls = (array(
4356
			"net.inet.ip.dummynet.io_fast" => "1",
4357
			"net.inet.ip.dummynet.hash_size" => "256",
4358
			"net.inet.ip.dummynet.pipe_slot_limit" => $max_qlimit
4359
	));
4360
	init_config_arr(array('sysctl', 'item'));
4361
	foreach (config_get_path('sysctl/item', []) as $item) {
4362
		if (preg_match('/net\.inet\.ip\.dummynet\./', $item['tunable'])) {
4363
			$sysctls[$item['tunable']] = $item['value'];
4364
		}
4365
	}
4366
	set_sysctl($sysctls);
4367
}
4368

    
4369
function get_interface_vip_ips($interface) {
4370
	global $config;
4371
	$vipips = '';
4372

    
4373
	init_config_arr(array('virtualip', 'vip'));
4374
	foreach ($config['virtualip']['vip'] as $vip) {
4375
		if (($vip['interface'] == $interface) &&
4376
		    (($vip['mode'] == "carp") || ($vip['mode'] == "ipalias"))) {
4377
			$vipips .= $vip['subnet'] . ' ';
4378
		}
4379
	}
4380
	return $vipips;
4381
}
4382

    
4383
?>
(54-54/61)