Project

General

Profile

Download (122 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-2024 Rubicon Communications, LLC (Netgate)
9
 * All rights reserved.
10
 *
11
 * originally part of m0n0wall (http://m0n0.ch/wall)
12
 * Copyright (c) 2003-2004 Manuel Kasper <mk@neon1.net>.
13
 * All rights reserved.
14
 *
15
 * Licensed under the Apache License, Version 2.0 (the "License");
16
 * you may not use this file except in compliance with the License.
17
 * You may obtain a copy of the License at
18
 *
19
 * http://www.apache.org/licenses/LICENSE-2.0
20
 *
21
 * Unless required by applicable law or agreed to in writing, software
22
 * distributed under the License is distributed on an "AS IS" BASIS,
23
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
 * See the License for the specific language governing permissions and
25
 * limitations under the License.
26
 */
27
//require_once('interfaces.inc');
28
require_once('Net/IPv6.php');
29
define('VIP_ALL', 1);
30
define('VIP_CARP', 2);
31
define('VIP_IPALIAS', 3);
32
// Special networks flags
33
define('SPECIALNET_NONE', 0);
34
define('SPECIALNET_ANY', 1);
35
define('SPECIALNET_COMPAT_ADDR', 22);
36
define('SPECIALNET_ADDR', 2);
37
define('SPECIALNET_COMPAT_ADDRAL', 33);
38
define('SPECIALNET_ADDRAL', 3);
39
define('SPECIALNET_NET', 4);
40
define('SPECIALNET_NETAL', 5);
41
define('SPECIALNET_SELF', 6);
42
define('SPECIALNET_CLIENTS', 7);
43
define('SPECIALNET_IFADDR', 8);
44
define('SPECIALNET_IFSUB', 9);
45
define('SPECIALNET_IFNET', 10);
46
define('SPECIALNET_GROUP', 11);
47
define('SPECIALNET_VIPS', 12);
48
define('SPECIALNET_CHECKPERM', 98);
49
define('SPECIALNET_EXCLUDE', 99);
50

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

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

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

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

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

    
92
	return 0;
93
}
94

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

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

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

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

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

    
120
	return false;
121
}
122

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

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

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

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

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

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

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

    
183
		return $fp;
184
	}
185

    
186
	return NULL;
187
}
188

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
447
	foreach ( $parts as $v ) {
448

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

    
452
	}
453

    
454
	return $binstr;
455
}
456

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

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

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

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

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

    
478
	return $ip;
479
}
480

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

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

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

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

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

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

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

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

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

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

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

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

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

    
592
	return $rangeaddresses;
593
}
594

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
715
	return $out;
716
}
717

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
896
}
897

    
898
/* returns true if $subnet is a valid IPv4 or IPv6 subnet in CIDR format
899
	false - if not a valid subnet
900
	true (numeric 4 or 6) - if valid, gives type of subnet */
901
function is_subnet($subnet) {
902
	if (is_string($subnet) && preg_match('/^(?:([0-9.]{7,15})|([0-9a-f:]{2,39}|[0-9a-f:]{2,30}[0-9.]{7,15}))\/(\d{1,3})$/i', $subnet, $parts)) {
903
		if (is_ipaddrv4($parts[1]) && $parts[3] <= 32) {
904
			return 4;
905
		}
906
		if (is_ipaddrv6($parts[2]) && $parts[3] <= 128) {
907
			return 6;
908
		}
909
	}
910
	return false;
911
}
912

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1105
	return $result;
1106
}
1107

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

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

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

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

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

    
1157
/* returns true if $domain is a valid domain name */
1158
function is_domain($domain, $allow_wildcard=false, $trailing_dot=true) {
1159
	if (!is_string($domain)) {
1160
		return false;
1161
	}
1162
	if (!$trailing_dot && ($domain[strlen($domain)-1] == ".")) {
1163
		return false;
1164
	}
1165
	if ($allow_wildcard) {
1166
		$domain_regex = '/^(?:(?:[a-z_0-9\*]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9])\.)*(?:[a-z_0-9]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9\.])$/i';
1167
	} else {
1168
		$domain_regex = '/^(?:(?:[a-z_0-9]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9])\.)*(?:[a-z_0-9]|[a-z_0-9][a-z_0-9\-]*[a-z_0-9\.])$/i';
1169
	}
1170

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

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

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

    
1197
	return true;
1198
}
1199

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

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

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

    
1217
	if (!is_string($name) || strlen($name) >= 32 || preg_match('/(^_*$|^\d*$|[^a-z0-9_])/i', $name)) {
1218
		if ($return_message) {
1219
			return sprintf(gettext('The %1$s name must be less than 32 characters long, may not consist of only numbers, may not consist of only underscores, and may only contain the following characters: %2$s'), $object, 'a-z, A-Z, 0-9, _');
1220
		} else {
1221
			return false;
1222
		}
1223
	}
1224
	if (in_array($name, $reserved, true)) {
1225
		if ($return_message) {
1226
			return sprintf(gettext('The %1$s name must not be either of the reserved words %2$s or %3$s.'), $object, "'port'", "'pass'");
1227
		} else {
1228
			return false;
1229
		}
1230
	}
1231
	if (getprotobyname($name)) {
1232
		if ($return_message) {
1233
			return sprintf(gettext('The %1$s name must not be an IP protocol name such as TCP, UDP, ICMP etc.'), $object);
1234
		} else {
1235
			return false;
1236
		}
1237
	}
1238
	if (getservbyname($name, "tcp") || getservbyname($name, "udp")) {
1239
		if ($return_message) {
1240
			return sprintf(gettext('The %1$s name must not be a well-known or registered TCP or UDP port name such as ssh, smtp, pop3, tftp, http, openvpn etc.'), $object);
1241
		} else {
1242
			return false;
1243
		}
1244
	}
1245
	if ($return_message) {
1246
		return sprintf(gettext('The %1$s name is valid.'), $object);
1247
	} else {
1248
		return true;
1249
	}
1250
}
1251

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

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

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

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

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

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

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

    
1283
	return true;
1284
}
1285

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1419
	return false;
1420
}
1421

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

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

    
1433
	return false;
1434
}
1435

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

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

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

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

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

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

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

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

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

    
1493
	return($list);
1494
}
1495

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1605
	return (NULL);
1606
}
1607

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

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

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

    
1641
	return ($iflist);
1642
}
1643

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

    
1664
	return ($iflist);
1665
}
1666

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

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

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

    
1690
	return ($iflist);
1691
}
1692

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

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

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

    
1731
	return $ip_array;
1732
}
1733

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

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

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

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

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

    
1906
	return ($plist);
1907
}
1908

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

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

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

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

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

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

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

    
1999
	return $retval;
2000
}
2001

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

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

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

    
2035
	$aliastable = array();
2036

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

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

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

    
2054
function alias_get_type($name) {
2055

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

    
2062
	return "";
2063
}
2064

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

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

    
2109
function alias_expand_urltable($name) {
2110
	$urltable_prefix = "/var/db/aliastables/";
2111
	$urltable_filename = $urltable_prefix . $name . ".txt";
2112

    
2113
	foreach (config_get_path('aliases/alias', []) as $alias) {
2114
		if (!preg_match("/urltable/i", $alias['type']) ||
2115
		    ($alias['name'] != $name)) {
2116
			continue;
2117
		}
2118

    
2119
		if (is_URL($alias["url"]) && file_exists($urltable_filename)) {
2120
			if (!filesize($urltable_filename)) {
2121
				// file exists, but is empty, try to sync
2122
				send_event("service sync alias {$name}");
2123
			}
2124
			return $urltable_filename;
2125
		} else {
2126
			send_event("service sync alias {$name}");
2127
			break;
2128
		}
2129
	}
2130
	return null;
2131
}
2132

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

    
2158
/* return a fieldname that is safe for xml usage */
2159
function xml_safe_fieldname($fieldname) {
2160
	$replace = array(
2161
	    '/', '-', ' ', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
2162
	    '_', '+', '=', '{', '}', '[', ']', '|', '/', '<', '>', '?',
2163
	    ':', ',', '.', '\'', '\\'
2164
	);
2165
	return strtolower(str_replace($replace, "", $fieldname));
2166
}
2167

    
2168
function mac_format($clientmac) {
2169
	global $config, $cpzone;
2170

    
2171
	$mac = explode(":", $clientmac);
2172
	$mac_format = $cpzone ? $config['captiveportal'][$cpzone]['radmac_format'] : false;
2173

    
2174
	switch ($mac_format) {
2175
		case 'singledash':
2176
			return "$mac[0]$mac[1]$mac[2]-$mac[3]$mac[4]$mac[5]";
2177

    
2178
		case 'ietf':
2179
			return "$mac[0]-$mac[1]-$mac[2]-$mac[3]-$mac[4]-$mac[5]";
2180

    
2181
		case 'cisco':
2182
			return "$mac[0]$mac[1].$mac[2]$mac[3].$mac[4]$mac[5]";
2183

    
2184
		case 'unformatted':
2185
			return "$mac[0]$mac[1]$mac[2]$mac[3]$mac[4]$mac[5]";
2186

    
2187
		default:
2188
			return $clientmac;
2189
	}
2190
}
2191

    
2192
function resolve_retry($hostname, $protocol = 'inet') {
2193
	$retries = 10;
2194
	$numrecords = 1;
2195
	$recresult = array();
2196

    
2197
	switch ($protocol) {
2198
		case 'any':
2199
			$checkproto = 'is_ipaddr';
2200
			$dnsproto = DNS_ANY;
2201
			$dnstype = array('A', 'AAAA');
2202
			break;
2203
		case 'inet6':
2204
			$checkproto = 'is_ipaddrv6';
2205
			$dnsproto = DNS_AAAA;
2206
			$dnstype = array('AAAA');
2207
			break;
2208
		case 'inet':
2209
		default:
2210
			$checkproto = 'is_ipaddrv4';
2211
			$dnsproto = DNS_A;
2212
			$dnstype = array('A');
2213
			break;
2214
	}
2215

    
2216
	for ($i = 0; $i < $retries; $i++) {
2217
		if ($checkproto($hostname)) {
2218
			return $hostname;
2219
		}
2220

    
2221
		$dnsresult = @dns_get_record($hostname, $dnsproto);
2222

    
2223
		if (!empty($dnsresult)) {
2224
			foreach ($dnsresult as $ip) {
2225
				if (is_array($ip)) {
2226
					if (in_array($ip['type'], $dnstype)) {
2227
						if ($checkproto($ip['ip'])) {
2228
							$recresult[] = $ip['ip'];
2229
						}
2230

    
2231
						if ($checkproto($ip['ipv6'])) {
2232
							$recresult[] = $ip['ipv6'];
2233
						}
2234
					}
2235
				}
2236
			}
2237
		}
2238

    
2239
		// Return on success
2240
		if (!empty($recresult)) {
2241
			if ($numrecords == 1) {
2242
				return $recresult[0];
2243
			} else {
2244
				return array_slice($recresult, 0, $numrecords);
2245
			}
2246
		}
2247

    
2248
		usleep(100000);
2249
	}
2250

    
2251
	return false;
2252
}
2253

    
2254
function format_bytes($bytes) {
2255
	if ($bytes >= 1099511627776) {
2256
		return sprintf("%.2f TiB", $bytes/1099511627776);
2257
	} else if ($bytes >= 1073741824) {
2258
		return sprintf("%.2f GiB", $bytes/1073741824);
2259
	} else if ($bytes >= 1048576) {
2260
		return sprintf("%.2f MiB", $bytes/1048576);
2261
	} else if ($bytes >= 1024) {
2262
		return sprintf("%.0f KiB", $bytes/1024);
2263
	} else {
2264
		return sprintf("%d B", $bytes);
2265
	}
2266
}
2267

    
2268
function format_number(int $num, int $precision = 3): string
2269
{
2270
    $units = ['', 'K', 'M', 'G', 'T'];
2271
    for ($i = 0; $num >= 1000; $i++) {
2272
        $num /= 1000;
2273
    }
2274
    return (round($num, $precision) . $units[$i]);
2275
}
2276

    
2277
function unformat_number($formated_num) {
2278
	$num = strtoupper($formated_num);
2279

    
2280
	if ( strpos($num,"T") !== false ) {
2281
		$num = str_replace("T","",$num) * 1000 * 1000 * 1000 * 1000;
2282
	} else if ( strpos($num,"G") !== false ) {
2283
		$num = str_replace("G","",$num) * 1000 * 1000 * 1000;
2284
	} else if ( strpos($num,"M") !== false ) {
2285
		$num = str_replace("M","",$num) * 1000 * 1000;
2286
	} else if ( strpos($num,"K") !== false ) {
2287
		$num = str_replace("K","",$num) * 1000;
2288
	}
2289

    
2290
	return $num;
2291
}
2292

    
2293
function update_filter_reload_status($text, $new=false) {
2294
	global $g;
2295

    
2296
	if ($new) {
2297
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL);
2298
	} else {
2299
		file_put_contents("{$g['varrun_path']}/filter_reload_status", $text  . PHP_EOL, FILE_APPEND);
2300
	}
2301
}
2302

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

    
2321
				if (empty($filter_regex) || preg_match($filter_regex, $file)) {
2322
					array_push($dir_array, $file);
2323
				}
2324
			}
2325
			closedir($dh);
2326
		}
2327
	}
2328
	return $dir_array;
2329
}
2330

    
2331
function run_plugins($directory) {
2332
	/* process packager manager custom rules */
2333
	$files = return_dir_as_array($directory);
2334
	if (is_array($files)) {
2335
		foreach ($files as $file) {
2336
			if (stristr($file, ".sh") == true) {
2337
				mwexec($directory . $file . " start");
2338
			} else if (!is_dir($directory . "/" . $file) && stristr($file, ".inc")) {
2339
				require_once($directory . "/" . $file);
2340
			}
2341
		}
2342
	}
2343
}
2344

    
2345
/*
2346
 *    safe_mkdir($path, $mode = 0755)
2347
 *    create directory if it doesn't already exist and isn't a file!
2348
 */
2349
function safe_mkdir($path, $mode = 0755) {
2350
	if (!is_file($path) && !is_dir($path)) {
2351
		return @mkdir($path, $mode, true);
2352
	} else {
2353
		return false;
2354
	}
2355
}
2356

    
2357
/*
2358
 * get_sysctl($names)
2359
 * Get values of sysctl OID's listed in $names (accepts an array or a single
2360
 * name) and return an array of key/value pairs set for those that exist
2361
 */
2362
function get_sysctl($names) {
2363
	if (empty($names)) {
2364
		return array();
2365
	}
2366

    
2367
	if (is_array($names)) {
2368
		$name_list = array();
2369
		foreach ($names as $name) {
2370
			$name_list[] = escapeshellarg($name);
2371
		}
2372
	} else {
2373
		$name_list = array(escapeshellarg($names));
2374
	}
2375

    
2376
	/* https://redmine.pfsense.org/issues/14648
2377
	 * exec()'ing sysctl must never fail and valid output is expected, except
2378
	 * when the OID is absent for the system. Retry up to 2 times and log sysctl
2379
	 * failure for troubleshooting */
2380
	$retries = 2;
2381
	do {
2382
		$ret = exec("/sbin/sysctl -iq " . implode(" ", $name_list), $output, $status);
2383
	} while ($ret === false && $retries-- > 0);
2384
	if ($ret === false) {
2385
		log_error("Warning: \"/sbin/sysctl -iq " . implode(" ", $name_list) . "\" returned status: {$status}");
2386
	}
2387

    
2388
	$values = array();
2389
	foreach ($output as $line) {
2390
		$line = explode(": ", $line, 2);
2391
		if (count($line) == 2) {
2392
			$values[$line[0]] = $line[1];
2393
		}
2394
	}
2395

    
2396
	return $values;
2397
}
2398

    
2399
/*
2400
 * get_single_sysctl($name)
2401
 * Wrapper for get_sysctl() to simplify read of a single sysctl value
2402
 * return the value for sysctl $name or empty string if it doesn't exist
2403
 */
2404
function get_single_sysctl($name) {
2405
	if (empty($name)) {
2406
		return "";
2407
	}
2408

    
2409
	$value = get_sysctl($name);
2410
	if (empty($value) || !isset($value[$name])) {
2411
		return "";
2412
	}
2413

    
2414
	return $value[$name];
2415
}
2416

    
2417
/*
2418
 * set_sysctl($value_list)
2419
 * Set sysctl OID's listed as key/value pairs and return
2420
 * an array with keys set for those that succeeded
2421
 */
2422
function set_sysctl($values) {
2423
	if (empty($values)) {
2424
		return array();
2425
	}
2426

    
2427
	$value_list = array();
2428
	foreach ($values as $key => $value) {
2429
		$value_list[] = escapeshellarg($key) . "=" . escapeshellarg($value);
2430
	}
2431

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

    
2434
	/* Retry individually if failed (one or more read-only) */
2435
	if ($success <> 0 && count($value_list) > 1) {
2436
		foreach ($value_list as $value) {
2437
			exec("/sbin/sysctl -iq " . $value, $output);
2438
		}
2439
	}
2440

    
2441
	$ret = array();
2442
	foreach ($output as $line) {
2443
		$line = explode(": ", $line, 2);
2444
		if (count($line) == 2) {
2445
			$ret[$line[0]] = true;
2446
		}
2447
	}
2448

    
2449
	return $ret;
2450
}
2451

    
2452
/*
2453
 * set_single_sysctl($name, $value)
2454
 * Wrapper to set_sysctl() to make it simple to set only one sysctl
2455
 * returns boolean meaning if it succeeded
2456
 */
2457
function set_single_sysctl($name, $value) {
2458
	if (empty($name)) {
2459
		return false;
2460
	}
2461

    
2462
	$result = set_sysctl(array($name => $value));
2463

    
2464
	if (!isset($result[$name]) || $result[$name] != $value) {
2465
		return false;
2466
	}
2467

    
2468
	return true;
2469
}
2470

    
2471
/*
2472
 *     get_memory()
2473
 *     returns an array listing the amount of
2474
 *     memory installed in the hardware
2475
 *     [0] net memory available for the OS (FreeBSD) after some is taken by BIOS, video or whatever - e.g. 235 MBytes
2476
 *     [1] real (actual) memory of the system, should be the size of the RAM card/s - e.g. 256 MBytes
2477
 */
2478
function get_memory() {
2479
	$physmem = get_single_sysctl("hw.physmem");
2480
	$realmem = get_single_sysctl("hw.realmem");
2481
	/* Ensure $physmem has a sane value */
2482
	if (!is_numericint($physmem) &&
2483
	    is_numericint($realmem)) {
2484
		$physmem = $realmem;
2485
	}
2486
	/* Ensure $realmem has a sane value */
2487
	if (!is_numericint($realmem) &&
2488
	    is_numericint($physmem)) {
2489
		$realmem = $physmem;
2490
	}
2491
	/* If both are invalid, something deeper is wrong */
2492
	if (!is_numericint($physmem) &&
2493
	    is_numericint($realmem)) {
2494
		/* Try checking by pages instead */
2495
		$membypages = (int) get_single_sysctl("vm.stats.vm.v_page_count") * (int) get_single_sysctl("vm.stats.vm.v_page_size");
2496
		if (is_numericint($membypages)) {
2497
			$physmem = $membypages;
2498
			$realmem = $membypages;
2499
		} else {
2500
			/* Everything failed, return zeroes */
2501
			$physmem = 0;
2502
			$realmem = 0;
2503
		}
2504
	}
2505
	/* convert from bytes to megabytes */
2506
	return array(((int) $physmem/1048576), ((int) $realmem/1048576));
2507
}
2508

    
2509
function get_php_default_memory($ARCH) {
2510
	if ($ARCH == "amd64") {
2511
		$default_mem = 512;
2512
	} else {
2513
		return 128;
2514
	}
2515

    
2516
	// Ensure the default isn't taking all of the available RAM with a minimum of 128M
2517
	$system_mem = get_memory()[0];
2518
	if ($default_mem >= $system_mem) {
2519
		$default_mem = $system_mem / 2;
2520
		if ($default_mem < 128) {
2521
			$default_mem = 128;
2522
		}
2523
	}
2524

    
2525
	return floor($default_mem);
2526
}
2527

    
2528
function get_php_max_memory() {
2529
	$system_mem = floor(get_memory()[0]);
2530
	$max_mem = $system_mem - 512;
2531
	$default_mem = get_php_default_memory(php_uname("m"));
2532

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

    
2537
		// If there still isn't enough RAM available, disable the ability to increase the PHP limit
2538
		if ($max_mem  < 128) {
2539
			return get_php_default_memory("");
2540
		}
2541
	} elseif ($max_mem < $default_mem) {
2542
		/* Do not restrict the maximum to less than the default */
2543
		$max_mem = $default_mem;
2544
	}
2545

    
2546
	return $max_mem;
2547
}
2548

    
2549
function mute_kernel_msgs() {
2550
	if (config_path_enabled('system','enableserial')) {
2551
		return;
2552
	}
2553
	exec("/sbin/conscontrol mute on");
2554
}
2555

    
2556
function unmute_kernel_msgs() {
2557
	exec("/sbin/conscontrol mute off");
2558
}
2559

    
2560
function start_devd() {
2561
	global $g;
2562

    
2563
	/* Generate hints for the kernel loader. */
2564
	$module_paths = explode(";", get_single_sysctl("kern.module_path"));
2565
	foreach ($module_paths as $path) {
2566
		if (!is_dir($path) ||
2567
		    (($files = scandir($path)) == false)) {
2568
			continue;
2569
		}
2570
		$found = false;
2571
		foreach ($files as $file) {
2572
			if (strlen($file) > 3 &&
2573
			    strcasecmp(substr($file, -3), ".ko") == 0) {
2574
				$found = true;
2575
				break;
2576
			}
2577
		}
2578
		if ($found == false) {
2579
			continue;
2580
		}
2581
		$_gb = exec("/usr/sbin/kldxref $path");
2582
		unset($_gb);
2583
	}
2584

    
2585
	/* Use the undocumented -q options of devd to quiet its log spamming */
2586
	$_gb = exec("/sbin/devd -q -f /etc/{$g['product_name']}-devd.conf");
2587
	sleep(1);
2588
	unset($_gb);
2589
}
2590

    
2591
function is_interface_vlan_mismatch() {
2592
	foreach (config_get_path('vlans/vlan', []) as $vlan) {
2593
		if (substr($vlan['if'], 0, 4) == "lagg") {
2594
			return false;
2595
		}
2596
		if (does_interface_exist($vlan['if']) == false) {
2597
			return true;
2598
		}
2599
	}
2600

    
2601
	return false;
2602
}
2603

    
2604
function is_interface_mismatch() {
2605
	global $config, $g;
2606

    
2607
	$do_assign = false;
2608
	$i = 0;
2609
	$missing_interfaces = array();
2610
	if (is_array($config['interfaces'])) {
2611
		foreach (config_get_path('interfaces', []) as $ifcfg) {
2612
			if (interface_is_vlan($ifcfg['if']) != NULL ||
2613
			    interface_is_qinq($ifcfg['if']) != NULL ||
2614
			    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'])) {
2615
				// Do not check these interfaces.
2616
				$i++;
2617
				continue;
2618
			} else if (does_interface_exist($ifcfg['if']) == false) {
2619
				$missing_interfaces[] = $ifcfg['if'];
2620
				$do_assign = true;
2621
			} else {
2622
				$i++;
2623
			}
2624
		}
2625
	}
2626

    
2627
	/* VLAN/QinQ-only interface mismatch detection
2628
	 * see https://redmine.pfsense.org/issues/12170 */
2629
	init_config_arr(array('vlans', 'vlan'));
2630
	foreach (config_get_path('vlans/vlan', []) as $vlan) {
2631
		if (!does_interface_exist($vlan['if']) &&
2632
		    !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'])) {
2633
			$missing_interfaces[] = $vlan['if'];
2634
			$do_assign = true;
2635
		}
2636
	}
2637
	init_config_arr(array('qinqs', 'qinqentry'));
2638
	foreach (config_get_path('qinqs/qinqentry', []) as $qinq) {
2639
		if (!does_interface_exist($qinq['if']) &&
2640
		    !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'])) {
2641
			$missing_interfaces[] = $qinq['if'];
2642
			$do_assign = true;
2643
		}
2644
	}
2645

    
2646
	if (file_exists("{$g['tmp_path']}/assign_complete")) {
2647
		$do_assign = false;
2648
	}
2649

    
2650
	if (!empty($missing_interfaces) && $do_assign) {
2651
		file_put_contents("{$g['tmp_path']}/missing_interfaces", implode(' ', $missing_interfaces));
2652
	} else {
2653
		@unlink("{$g['tmp_path']}/missing_interfaces");
2654
	}
2655

    
2656
	return $do_assign;
2657
}
2658

    
2659
/* sync carp entries to other firewalls */
2660
function carp_sync_client() {
2661
	send_event("filter sync");
2662
}
2663

    
2664
/****f* util/isAjax
2665
 * NAME
2666
 *   isAjax - reports if the request is driven from prototype
2667
 * INPUTS
2668
 *   none
2669
 * RESULT
2670
 *   true/false
2671
 ******/
2672
function isAjax() {
2673
	return isset ($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
2674
}
2675

    
2676
/****f* util/timeout
2677
 * NAME
2678
 *   timeout - console input with timeout countdown. Note: erases 2 char of screen for timer. Leave space.
2679
 * INPUTS
2680
 *   optional, seconds to wait before timeout. Default 9 seconds.
2681
 * RESULT
2682
 *   returns 1 char of user input or null if no input.
2683
 ******/
2684
function timeout($timer = 9) {
2685
	while (!isset($key)) {
2686
		if ($timer >= 9) {
2687
			echo chr(8) . chr(8) . ($timer == 9 ? chr(32) : null) . "{$timer}";
2688
		} else {
2689
			echo chr(8). "{$timer}";
2690
		}
2691
		`/bin/stty -icanon min 0 time 25`;
2692
		$key = trim(`KEY=\`dd count=1 2>/dev/null\`; echo \$KEY`);
2693
		`/bin/stty icanon`;
2694
		if ($key == '') {
2695
			unset($key);
2696
		}
2697
		$timer--;
2698
		if ($timer == 0) {
2699
			break;
2700
		}
2701
	}
2702
	return $key;
2703
}
2704

    
2705
/****f* util/msort
2706
 * NAME
2707
 *   msort - sort array
2708
 * INPUTS
2709
 *   $array to be sorted, field to sort by, direction of sort
2710
 * RESULT
2711
 *   returns newly sorted array
2712
 ******/
2713
function msort($array, $id = "id", $sort_ascending = true) {
2714
	$temp_array = array();
2715
	if (!is_array($array)) {
2716
		return $temp_array;
2717
	}
2718
	while (count($array)>0) {
2719
		$lowest_id = 0;
2720
		$index = 0;
2721
		foreach ($array as $item) {
2722
			if (isset($item[$id])) {
2723
				if ($array[$lowest_id][$id]) {
2724
					if (strtolower($item[$id]) < strtolower($array[$lowest_id][$id])) {
2725
						$lowest_id = $index;
2726
					}
2727
				}
2728
			}
2729
			$index++;
2730
		}
2731
		$temp_array[] = $array[$lowest_id];
2732
		$array = array_merge(array_slice($array, 0, $lowest_id), array_slice($array, $lowest_id + 1));
2733
	}
2734
	if ($sort_ascending) {
2735
		return $temp_array;
2736
	} else {
2737
		return array_reverse($temp_array);
2738
	}
2739
}
2740

    
2741
/****f* util/is_URL
2742
 * NAME
2743
 *   is_URL
2744
 * INPUTS
2745
 *   $url: string to check
2746
 *   $httponly: Only allow HTTP or HTTPS scheme
2747
 * RESULT
2748
 *   Returns true if item is a URL
2749
 ******/
2750
function is_URL($url, $httponly = false) {
2751
	if (filter_var($url, FILTER_VALIDATE_URL)) {
2752
		if ($httponly) {
2753
			return in_array(strtolower(parse_url($url, PHP_URL_SCHEME)), ['http', 'https']);
2754
		}
2755
		return true;
2756
	}
2757
	return false;
2758
}
2759

    
2760
function is_file_included($file = "") {
2761
	$files = get_included_files();
2762
	if (in_array($file, $files)) {
2763
		return true;
2764
	}
2765

    
2766
	return false;
2767
}
2768

    
2769
/*
2770
 * Replace a value on a deep associative array using regex
2771
 */
2772
function array_replace_values_recursive($data, $match, $replace) {
2773
	if (empty($data)) {
2774
		return $data;
2775
	}
2776

    
2777
	if (is_string($data)) {
2778
		$data = preg_replace("/{$match}/", $replace, $data);
2779
	} else if (is_array($data)) {
2780
		foreach ($data as $k => $v) {
2781
			$data[$k] = array_replace_values_recursive($v, $match, $replace);
2782
		}
2783
	}
2784

    
2785
	return $data;
2786
}
2787

    
2788
/*
2789
	This function was borrowed from a comment on PHP.net at the following URL:
2790
	https://www.php.net/manual/en/function.array-merge-recursive.php#73843
2791
 */
2792
function array_merge_recursive_unique($array0, $array1) {
2793

    
2794
	$arrays = func_get_args();
2795
	$remains = $arrays;
2796

    
2797
	// We walk through each arrays and put value in the results (without
2798
	// considering previous value).
2799
	$result = array();
2800

    
2801
	// loop available array
2802
	foreach ($arrays as $array) {
2803

    
2804
		// The first remaining array is $array. We are processing it. So
2805
		// we remove it from remaining arrays.
2806
		array_shift($remains);
2807

    
2808
		// We don't care non array param, like array_merge since PHP 5.0.
2809
		if (is_array($array)) {
2810
			// Loop values
2811
			foreach ($array as $key => $value) {
2812
				if (is_array($value)) {
2813
					// we gather all remaining arrays that have such key available
2814
					$args = array();
2815
					foreach ($remains as $remain) {
2816
						if (array_key_exists($key, $remain)) {
2817
							array_push($args, $remain[$key]);
2818
						}
2819
					}
2820

    
2821
					if (count($args) > 2) {
2822
						// put the recursion
2823
						$result[$key] = call_user_func_array(__FUNCTION__, $args);
2824
					} else {
2825
						foreach ($value as $vkey => $vval) {
2826
							if (!is_array($result[$key])) {
2827
								$result[$key] = array();
2828
							}
2829
							$result[$key][$vkey] = $vval;
2830
						}
2831
					}
2832
				} else {
2833
					// simply put the value
2834
					$result[$key] = $value;
2835
				}
2836
			}
2837
		}
2838
	}
2839
	return $result;
2840
}
2841

    
2842

    
2843
/*
2844
 * converts a string like "a,b,c,d"
2845
 * into an array like array("a" => "b", "c" => "d")
2846
 */
2847
function explode_assoc($delimiter, $string) {
2848
	$array = explode($delimiter, $string);
2849
	$result = array();
2850
	$numkeys = floor(count($array) / 2);
2851
	for ($i = 0; $i < $numkeys; $i += 1) {
2852
		$result[$array[$i * 2]] = $array[$i * 2 + 1];
2853
	}
2854
	return $result;
2855
}
2856

    
2857
/*
2858
 * Given a string of text with some delimiter, look for occurrences
2859
 * of some string and replace all of those.
2860
 * $text - the text string (e.g. "abc,defg,x123,ipv4,xyz")
2861
 * $delimiter - the delimiter (e.g. ",")
2862
 * $element - the element to match (e.g. "defg")
2863
 * $replacement - the string to replace it with (e.g. "42")
2864
 * Returns the resulting delimited string (e.g. "abc,42,x123,ipv4,xyz")
2865
 */
2866
function replace_element_in_list($text, $delimiter, $element, $replacement) {
2867
	$textArray = explode($delimiter, $text);
2868
	while (($entry = array_search($element, $textArray)) !== false) {
2869
		$textArray[$entry] = $replacement;
2870
	}
2871
	return implode(',', $textArray);
2872
}
2873

    
2874
/* Return system's route table */
2875
function route_table() {
2876
	exec("/usr/bin/netstat --libxo json -nWr", $rawdata, $rc);
2877

    
2878
	if ($rc != 0) {
2879
		return array();
2880
	}
2881

    
2882
	$netstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
2883
	$netstatarr = $netstatarr['statistics']['route-information']
2884
	    ['route-table']['rt-family'];
2885

    
2886
	$result = array();
2887
	$result['inet'] = array();
2888
	$result['inet6'] = array();
2889
	foreach ($netstatarr as $item) {
2890
		if ($item['address-family'] == 'Internet') {
2891
			$result['inet'] = $item['rt-entry'];
2892
		} else if ($item['address-family'] == 'Internet6') {
2893
			$result['inet6'] = $item['rt-entry'];
2894
		}
2895
	}
2896
	unset($netstatarr);
2897

    
2898
	return $result;
2899
}
2900

    
2901
/**
2902
 * Checks if a route was sourced from a dynamic routing protocol like
2903
 * BGP or OSPF by looking at the route flags.
2904
 * 
2905
 * @param string $target IP address or subnet, or 'default' for the default route
2906
 * @param string $ipprotocol either 'inet' for IPv4, or 'inet6' for IPv6
2907
 * 
2908
 * @return bool true if it's a dynamic route
2909
 */
2910
function is_dynamic_route(string $target, string $ipprotocol = ''): bool {
2911
	if (is_v4($target) || (($target == 'default') && ($ipprotocol == 'inet'))) {
2912
		$inet = '4';
2913
	} elseif (is_v6($target) || (($target == 'default') && ($ipprotocol == 'inet6'))) {
2914
		$inet = '6';
2915
	} else {
2916
		return false;
2917
	}
2918

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

    
2926
	return false;
2927
}
2928

    
2929
/* Get static route for specific destination */
2930
function route_get($target, $ipprotocol = '', $useroute = false) {
2931
	global $config;
2932

    
2933
	if (!empty($ipprotocol)) {
2934
		$family = $ipprotocol;
2935
	} else if (is_v4($target)) {
2936
		$family = 'inet';
2937
	} else if (is_v6($target)) {
2938
		$family = 'inet6';
2939
	}
2940

    
2941
	if (empty($family)) {
2942
		return array();
2943
	}
2944

    
2945
	if ($useroute) {
2946
		if ($family == 'inet') {
2947
			$inet = '4';
2948
		} else {
2949
			$inet = '6';
2950
		}
2951
		$interface = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/interface:/{print $2}'");
2952
		if (empty($interface)) {
2953
			return array();
2954
		} elseif ($interface == 'lo0') {
2955
			// interface assigned IP address
2956
			foreach (array_keys($config['interfaces']) as $intf) {
2957
				if ((($inet == '4') && (get_interface_ip($intf) == $target)) ||
2958
				    (($inet == '6') && (get_interface_ipv6($intf) == $target))) {
2959
					$interface = convert_friendly_interface_to_real_interface_name($intf);
2960
					$gateway = $interface;
2961
					break;
2962
				}
2963
			}
2964
		} else {
2965
			$gateway = exec("/sbin/route -n{$inet} get {$target} 2>/dev/null | /usr/bin/awk '/gateway:/{print $2}'");
2966
			if (!$gateway) {
2967
				// non-local gateway
2968
				$gateway = get_interface_mac($interface);
2969
			}
2970
		}
2971
		$result[] = array('gateway' => $gateway, 'interface-name' => $interface);
2972
	} else {
2973
		$rtable = route_table();
2974
		if (empty($rtable)) {
2975
			return array();
2976
		}
2977

    
2978
		$result = array();
2979
		foreach ($rtable[$family] as $item) {
2980
			if ($item['destination'] == $target ||
2981
			    ip_in_subnet($target, $item['destination'])) {
2982
				$result[] = $item;
2983
			}
2984
		}
2985
	}
2986

    
2987
	return $result;
2988
}
2989

    
2990
/* Get default route */
2991
function route_get_default($ipprotocol) {
2992
	if (empty($ipprotocol) || ($ipprotocol != 'inet' &&
2993
	    $ipprotocol != 'inet6')) {
2994
		return '';
2995
	}
2996

    
2997
	$route = route_get('default', $ipprotocol, true);
2998

    
2999
	if (empty($route)) {
3000
		return '';
3001
	}
3002

    
3003
	if (!isset($route[0]['gateway'])) {
3004
		return '';
3005
	}
3006

    
3007
	return $route[0]['gateway'];
3008
}
3009

    
3010
/* Delete a static route */
3011
function route_del($target, $ipprotocol = '') {
3012
	global $config;
3013

    
3014
	if (empty($target)) {
3015
		return;
3016
	}
3017

    
3018
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
3019
	    $ipprotocol != 'inet6') {
3020
		return false;
3021
	}
3022

    
3023
	$route = route_get($target, $ipprotocol, true);
3024

    
3025
	if (empty($route)) {
3026
		return;
3027
	}
3028

    
3029
	$target_prefix = '';
3030
	if (is_subnet($target)) {
3031
		$target_prefix = '-net';
3032
	} else if (is_ipaddr($target)) {
3033
		$target_prefix = '-host';
3034
	}
3035

    
3036
	if (!empty($ipprotocol)) {
3037
		$target_prefix .= " -{$ipprotocol}";
3038
	} else if (is_v6($target)) {
3039
		$target_prefix .= ' -inet6';
3040
	} else if (is_v4($target)) {
3041
		$target_prefix .= ' -inet';
3042
	}
3043

    
3044
	foreach ($route as $item) {
3045
		if (substr($item['gateway'], 0, 5) == 'link#') {
3046
			continue;
3047
		}
3048

    
3049
		if (is_macaddr($item['gateway'])) {
3050
			$gw = '-iface ' . $item['interface-name'];
3051
		} else {
3052
			$gw = $item['gateway'];
3053
		}
3054

    
3055
		exec(escapeshellcmd("/sbin/route del {$target_prefix} " .
3056
		    "{$target} {$gw}"), $output, $rc);
3057

    
3058
		if (isset($config['system']['route-debug'])) {
3059
			log_error("ROUTING debug: " . microtime() .
3060
			    " - DEL RC={$rc} - {$target} - gw: " . $gw);
3061
			file_put_contents("/dev/console", "\n[" . getmypid() .
3062
			    "] ROUTE DEL: {$target_prefix} {$target} {$gw} " .
3063
			    "result: {$rc}");
3064
		}
3065
	}
3066
}
3067

    
3068
/*
3069
 * Add static route.  If it already exists, remove it and re-add
3070
 *
3071
 * $target - IP, subnet or 'default'
3072
 * $gw     - gateway address
3073
 * $iface  - Network interface
3074
 * $args   - Extra arguments for /sbin/route
3075
 * $ipprotocol - 'inet' or 'inet6'.  Mandatory when $target == 'default'
3076
 *
3077
 */
3078
function route_add_or_change($target, $gw, $iface = '', $args = '',
3079
    $ipprotocol = '') {
3080
	global $config;
3081

    
3082
	if (empty($target) || (empty($gw) && empty($iface))) {
3083
		return false;
3084
	}
3085

    
3086
	if ($target == 'default' && empty($ipprotocol)) {
3087
		return false;
3088
	}
3089

    
3090
	if (!empty($ipprotocol) && $ipprotocol != 'inet' &&
3091
	    $ipprotocol != 'inet6') {
3092
		return false;
3093
	}
3094

    
3095
	/* use '-host' for IPv6 /128 routes, see https://redmine.pfsense.org/issues/11594 */
3096
	if (is_subnetv4($target) || (is_subnetv6($target) && (subnet_size($target) > 1))) {
3097
		$target_prefix = '-net';
3098
	} else if (is_ipaddr($target)) {
3099
		$target_prefix = '-host';
3100
	}
3101

    
3102
	if (!empty($ipprotocol)) {
3103
		$target_prefix .= " -{$ipprotocol}";
3104
	} else if (is_v6($target)) {
3105
		$target_prefix .= ' -inet6';
3106
	} else if (is_v4($target)) {
3107
		$target_prefix .= ' -inet';
3108
	}
3109

    
3110
	/* If there is another route to the same target, remove it */
3111
	route_del($target, $ipprotocol);
3112

    
3113
	$params = '';
3114
	if (!empty($iface) && does_interface_exist($iface)) {
3115
		$params .= " -iface {$iface}";
3116
	}
3117
	if (is_ipaddr($gw)) {
3118
		/* set correct linklocal gateway address,
3119
		 * see https://redmine.pfsense.org/issues/11713
3120
		 * and https://redmine.pfsense.org/issues/11806 */
3121
		if (is_ipaddrv6($gw) && is_linklocal($gw) && empty(get_ll_scope($gw))) {
3122
			$routeget = route_get($gw, 'inet6', true);
3123
			$gw .= "%" . $routeget[0]['interface-name'];
3124
		}
3125
		$params .= " " . $gw;
3126
	}
3127

    
3128
	if (empty($params)) {
3129
		log_error("route_add_or_change: Invalid gateway {$gw} and/or " .
3130
		    "network interface {$iface}");
3131
		return false;
3132
	}
3133

    
3134
	exec(escapeshellcmd("/sbin/route add {$target_prefix} " .
3135
	    "{$target} {$args} {$params}"), $output, $rc);
3136

    
3137
	if (isset($config['system']['route-debug'])) {
3138
		log_error("ROUTING debug: " . microtime() .
3139
		    " - ADD RC={$rc} - {$target} {$args}");
3140
		file_put_contents("/dev/console", "\n[" . getmypid() .
3141
		    "] ROUTE ADD: {$target_prefix} {$target} {$args} " .
3142
		    "{$params} result: {$rc}");
3143
	}
3144

    
3145
	return ($rc == 0);
3146
}
3147

    
3148
function set_ipv6routes_mtu($interface, $mtu) {
3149
	global $config;
3150

    
3151
	$ipv6mturoutes = array();
3152
	$if = convert_real_interface_to_friendly_interface_name($interface);
3153
	if (!$config['interfaces'][$if]['ipaddrv6']) {
3154
		return;
3155
	}
3156
	$a_gateways = get_gateways();
3157
	$a_staticroutes = get_staticroutes(false, false, true);
3158
	foreach ($a_gateways as $gate) {
3159
		foreach ($a_staticroutes as $sroute) {
3160
			if (($gate['interface'] == $interface) &&
3161
			    ($sroute['gateway'] == $gate['name']) &&
3162
			    (is_ipaddrv6($gate['gateway']))) {
3163
				$tgt = $sroute['network'];
3164
				$gateway = $gate['gateway'];
3165
				$ipv6mturoutes[$tgt] = $gateway;
3166
			}
3167
		}
3168
		if (($gate['interface'] == $interface) &&
3169
		    $gate['isdefaultgw'] && is_ipaddrv6($gate['gateway'])) {
3170
			$tgt = "default";
3171
			$gateway = $gate['gateway'];
3172
			$ipv6mturoutes[$tgt] = $gateway;
3173
		}
3174
	}
3175
	foreach ($ipv6mturoutes as $tgt => $gateway) {
3176
		mwexec("/sbin/route change -6 -mtu " . escapeshellarg($mtu) .
3177
		    " " . escapeshellarg($tgt) . " " .
3178
		    escapeshellarg($gateway));
3179
	}
3180
}
3181

    
3182
function alias_to_subnets_recursive($name, $returnhostnames = false) {
3183
	global $aliastable;
3184
	$result = array();
3185
	if (!isset($aliastable[$name])) {
3186
		return $result;
3187
	}
3188
	$subnets = preg_split('/\s+/', $aliastable[$name]);
3189
	foreach ($subnets as $net) {
3190
		if (is_alias($net)) {
3191
			$sub = alias_to_subnets_recursive($net, $returnhostnames);
3192
			$result = array_merge($result, $sub);
3193
			continue;
3194
		} elseif (!is_subnet($net)) {
3195
			if (is_ipaddrv4($net)) {
3196
				$net .= "/32";
3197
			} else if (is_ipaddrv6($net)) {
3198
				$net .= "/128";
3199
			} else if ($returnhostnames === false || !is_fqdn($net)) {
3200
				continue;
3201
			}
3202
		}
3203
		$result[] = $net;
3204
	}
3205
	return $result;
3206
}
3207

    
3208
function get_staticroutes($returnsubnetsonly = false, $returnhostnames = false, $returnenabledroutesonly = false) {
3209
	global $config;
3210

    
3211
	/* Bail if there are no routes, but return an array always so callers don't have to check. */
3212
	init_config_arr(array('staticroutes', 'route'));
3213
	if (empty($config['staticroutes']['route'])) {
3214
		return array();
3215
	}
3216

    
3217
	$allstaticroutes = array();
3218
	$allsubnets = array();
3219
	/* Loop through routes and expand aliases as we find them. */
3220
	foreach (config_get_path('staticroutes/route', []) as $route) {
3221
		if ($returnenabledroutesonly && isset($route['disabled'])) {
3222
			continue;
3223
		}
3224

    
3225
		if (is_alias($route['network'])) {
3226
			foreach (alias_to_subnets_recursive($route['network'], $returnhostnames) as $net) {
3227
				$temproute = $route;
3228
				$temproute['network'] = $net;
3229
				$allstaticroutes[] = $temproute;
3230
				$allsubnets[] = $net;
3231
			}
3232
		} elseif (is_subnet($route['network'])) {
3233
			$allstaticroutes[] = $route;
3234
			$allsubnets[] = $route['network'];
3235
		}
3236
	}
3237
	if ($returnsubnetsonly) {
3238
		return $allsubnets;
3239
	} else {
3240
		return $allstaticroutes;
3241
	}
3242
}
3243

    
3244
/****f* util/get_alias_list
3245
 * NAME
3246
 *   get_alias_list - Provide a list of aliases.
3247
 * INPUTS
3248
 *   $type          - Optional, can be a string or array specifying what type(s) of aliases you need.
3249
 * RESULT
3250
 *   Array containing list of aliases.
3251
 *   If $type is unspecified, all aliases are returned.
3252
 *   If $type is a string, all aliases of the type specified in $type are returned.
3253
 *   If $type is an array, all aliases of any type specified in any element of $type are returned.
3254
 */
3255
function get_alias_list($type = null) {
3256
	$result = array();
3257
	foreach (config_get_path('aliases/alias', []) as $alias) {
3258
		if ($type === null) {
3259
			$result[] = $alias['name'];
3260
		} else if (is_array($type)) {
3261
			if (in_array($alias['type'], $type)) {
3262
				$result[] = $alias['name'];
3263
			}
3264
		} else if ($type === $alias['type']) {
3265
			$result[] = $alias['name'];
3266
		}
3267
	}
3268
	return $result;
3269
}
3270

    
3271
/* Returns the current alias contents sorted by name in case insensitive and
3272
 * natural order.
3273
 * https://redmine.pfsense.org/issues/14015 */
3274
function get_sorted_aliases() {
3275
	$aliases = config_get_path('aliases/alias', []);
3276
	uasort($aliases, function ($a, $b) { return strnatcasecmp($a['name'], $b['name']); });
3277
	return $aliases;
3278
}
3279

    
3280
/* returns an array consisting of every element of $haystack that is not equal to $needle. */
3281
function array_exclude($needle, $haystack) {
3282
	$result = array();
3283
	if (is_array($haystack)) {
3284
		foreach ($haystack as $thing) {
3285
			if ($needle !== $thing) {
3286
				$result[] = $thing;
3287
			}
3288
		}
3289
	}
3290
	return $result;
3291
}
3292

    
3293
/* Define what is preferred, IPv4 or IPv6 */
3294
function prefer_ipv4_or_ipv6() {
3295
	if (config_path_enabled('system', 'prefer_ipv4')) {
3296
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv4");
3297
	} else {
3298
		mwexec("/etc/rc.d/ip6addrctl prefer_ipv6");
3299
	}
3300
}
3301

    
3302
/* Redirect to page passing parameters via POST */
3303
function post_redirect($page, $params) {
3304
	if (!is_array($params)) {
3305
		return;
3306
	}
3307

    
3308
	print "<html><body><form action=\"{$page}\" name=\"formredir\" method=\"post\">\n";
3309
	foreach ($params as $key => $value) {
3310
		print "<input type=\"hidden\" name=\"{$key}\" value=\"{$value}\" />\n";
3311
	}
3312
	print "</form>\n";
3313
	print "<script type=\"text/javascript\">\n";
3314
	print "//<![CDATA[\n";
3315
	print "document.formredir.submit();\n";
3316
	print "//]]>\n";
3317
	print "</script>\n";
3318
	print "</body></html>\n";
3319
}
3320

    
3321
/* Locate disks that can be queried for S.M.A.R.T. data. */
3322
function get_smart_drive_list() {
3323
	/* SMART supports some disks directly, and some controllers directly,
3324
	 * See https://redmine.pfsense.org/issues/9042 */
3325
	$supported_disk_types = array("ad", "da", "ada");
3326
	$supported_controller_types = array("nvme");
3327
	$disk_list = explode(" ", get_single_sysctl("kern.disks"));
3328
	foreach ($disk_list as $id => $disk) {
3329
		// We only want certain kinds of disks for S.M.A.R.T.
3330
		// 1 is a match, 0 is no match, False is any problem processing the regex
3331
		if (preg_match("/^(" . implode("|", $supported_disk_types) . ").*[0-9]{1,2}$/", $disk) !== 1) {
3332
			unset($disk_list[$id]);
3333
			continue;
3334
		}
3335
	}
3336
	foreach ($supported_controller_types as $controller) {
3337
		$devices = glob("/dev/{$controller}*");
3338
		if (!is_array($devices)) {
3339
			continue;
3340
		}
3341
		foreach ($devices as $device) {
3342
			$disk_list[] = basename($device);
3343
		}
3344
	}
3345
	sort($disk_list);
3346
	return $disk_list;
3347
}
3348

    
3349
// Validate a network address
3350
//	$addr: the address to validate
3351
//	$type: IPV4|IPV6|IPV4V6
3352
//	$label: the label used by the GUI to display this value. Required to compose an error message
3353
//	$err_msg: pointer to the callers error message array so that error messages can be added to it here
3354
//	$alias: are aliases permitted for this address?
3355
// Returns:
3356
//	IPV4 - if $addr is a valid IPv4 address
3357
//	IPV6 - if $addr is a valid IPv6 address
3358
//	ALIAS - if $alias=true and $addr is an alias
3359
//	false - otherwise
3360

    
3361
function validateipaddr(&$addr, $type, $label, &$err_msg, $alias=false) {
3362
	switch ($type) {
3363
		case IPV4:
3364
			if (is_ipaddrv4($addr)) {
3365
				return IPV4;
3366
			} else if ($alias) {
3367
				if (is_alias($addr)) {
3368
					return ALIAS;
3369
				} else {
3370
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address or alias."), $label);
3371
					return false;
3372
				}
3373
			} else {
3374
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 address."), $label);
3375
				return false;
3376
			}
3377
		break;
3378
		case IPV6:
3379
			if (is_ipaddrv6($addr)) {
3380
				$addr = strtolower($addr);
3381
				return IPV6;
3382
			} else if ($alias) {
3383
				if (is_alias($addr)) {
3384
					return ALIAS;
3385
				} else {
3386
					$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address or alias."), $label);
3387
					return false;
3388
				}
3389
			} else {
3390
				$err_msg[] = sprintf(gettext("%s must be a valid IPv6 address."), $label);
3391
				return false;
3392
			}
3393
		break;
3394
		case IPV4V6:
3395
			if (is_ipaddrv6($addr)) {
3396
				$addr = strtolower($addr);
3397
				return IPV6;
3398
			} else if (is_ipaddrv4($addr)) {
3399
				return IPV4;
3400
			} else if ($alias) {
3401
				if (is_alias($addr)) {
3402
					return ALIAS;
3403
				} else {
3404
					$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address or alias."), $label);
3405
					return false;
3406
				}
3407
			} else {
3408
				$err_msg[] = sprintf(gettext("%s must be a valid IPv4 or IPv6 address."), $label);
3409
				return false;
3410
			}
3411
		break;
3412
	}
3413

    
3414
	return false;
3415
}
3416

    
3417
/* From DUID configuration inputs, format a string that looks (more) like the expected raw DUID format:
3418
 * 1) For DUIDs entered as a known DUID type, convert to a hexstring and prepend the DUID number, after having done the following:
3419
 *     a) For DUID-LLT and DUID-EN, convert the time/enterprise ID input to hex and append the link-layer address/identifier input.
3420
 *     b) For DUID-LLT and DUID-LL, prepend a hardware type of 1.
3421
 *     c) For DUID-UUID, remove any "-".
3422
 * 2) Replace any remaining "-" with ":".
3423
 * 3) If any components are input with just a single char (hex digit hopefully), put a "0" in front.
3424
 * 4) The first two components should be a 16-bit integer (little- or big-endian, depending on the current machine type) that
3425
 *    is equal to the number of other components. If not, prepend this as "nn:00" (all pfSense builds are little-endian).
3426
 *    This is convenience, because the DUID reported by dhcp6c in logs does not include this count, which corresponds to the
3427
 *    option-len field of DHCPv6's OPTION_CLIENTID option.
3428
 *
3429
 * The final result should be closer to:
3430
 *
3431
 * "nn:00:00:0n:nn:nn:nn:..."
3432
 *
3433
 * This function does not validate the input. is_duid() will do validation.
3434
 */
3435
function format_duid($duidtype, $duidpt1, $duidpt2=null) {
3436
	if ($duidpt2)
3437
		$duidpt1 = implode(':', str_split(str_pad(dechex($duidpt1), 8, '0', STR_PAD_LEFT), 2)) . ':' . $duidpt2;
3438

    
3439
	/* Make hexstrings */
3440
	if ($duidtype) {
3441
		switch ($duidtype) {
3442
		/* Add a hardware type to DUID-LLT and DUID-LL; assume Ethernet */
3443
		case 1:
3444
		case 3:
3445
			$duidpt1 = '00:01:' . $duidpt1;
3446
			break;
3447
		/* Remove '-' from given UUID and insert ':' every 2 characters */
3448
		case 4:
3449
			$duidpt1 = implode(':', str_split(str_replace('-', '', $duidpt1), 2));
3450
			break;
3451
		default:
3452
		}
3453
		$duidpt1 = '00:0' . $duidtype . ':' . $duidpt1;
3454
	}
3455

    
3456
	$values = explode(':', strtolower(str_replace('-', ':', $duidpt1)));
3457

    
3458
	if (hexdec($values[0]) != count($values) - 2)
3459
		array_unshift($values, dechex(count($values)), '00');
3460

    
3461
	array_walk($values, function(&$value) {
3462
		$value = str_pad($value, 2, '0', STR_PAD_LEFT);
3463
	});
3464

    
3465
	return implode(":", $values);
3466
}
3467

    
3468
/* Returns true if $dhcp6duid is a valid DUID entry.
3469
 * Parse the entry to check for valid length according to known DUID types.
3470
 */
3471
function is_duid($dhcp6duid) {
3472
	$values = explode(":", $dhcp6duid);
3473
	if (hexdec($values[0]) == count($values) - 2) {
3474
		switch (hexdec($values[2] . $values[3])) {
3475
		case 0:
3476
			return false;
3477
			break;
3478
		case 1:
3479
			if (count($values) != 16 || strlen($dhcp6duid) != 47)
3480
				return false;
3481
			break;
3482
		case 3:
3483
			if (count($values) != 12 || strlen($dhcp6duid) != 35)
3484
				return false;
3485
			break;
3486
		case 4:
3487
			if (count($values) != 20 || strlen($dhcp6duid) != 59)
3488
				return false;
3489
			break;
3490
		/* DUID is up to 128 octets; allow 2 octets for type code, 2 more for option-len */
3491
		default:
3492
			if (count($values) > 132 || strlen($dhcp6duid) != count($values) * 3 - 1)
3493
				return false;
3494
		}
3495
	} else
3496
		return false;
3497

    
3498
	for ($i = 0; $i < count($values); $i++) {
3499
		if (ctype_xdigit($values[$i]) == false)
3500
			return false;
3501
		if (hexdec($values[$i]) < 0 || hexdec($values[$i]) > 255)
3502
			return false;
3503
	}
3504

    
3505
	return true;
3506
}
3507

    
3508
/* Write the DHCP6 DUID file */
3509
function write_dhcp6_duid($duidstring) {
3510
	// Create the hex array from the dhcp6duid config entry and write to file
3511
	global $g;
3512

    
3513
	if(!is_duid($duidstring)) {
3514
		log_error(gettext("Error: attempting to write DUID file - Invalid DUID detected"));
3515
		return false;
3516
	}
3517
	$temp = str_replace(":","",$duidstring);
3518
	$duid_binstring = pack("H*",$temp);
3519
	if ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "wb")) {
3520
		fwrite($fd, $duid_binstring);
3521
		fclose($fd);
3522
		return true;
3523
	}
3524
	log_error(gettext("Error: attempting to write DUID file - File write error"));
3525
	return false;
3526
}
3527

    
3528
/* returns duid string from 'vardb_path']}/dhcp6c_duid' */
3529
function get_duid_from_file() {
3530
	global $g;
3531

    
3532
	$duid_ASCII = "";
3533
	$count = 0;
3534

    
3535
	if (file_exists("{$g['vardb_path']}/dhcp6c_duid") &&
3536
	    ($fd = fopen("{$g['vardb_path']}/dhcp6c_duid", "r"))) {
3537
		$fsize = filesize("{$g['vardb_path']}/dhcp6c_duid");
3538
		if ($fsize <= 132) {
3539
			$buffer = fread($fd, $fsize);
3540
			while($count < $fsize) {
3541
				$duid_ASCII .= bin2hex($buffer[$count]);
3542
				$count++;
3543
				if($count < $fsize) {
3544
					$duid_ASCII .= ":";
3545
				}
3546
			}
3547
		}
3548
		fclose($fd);
3549
	}
3550
	//if no file or error with read then the string returns blanked DUID string
3551
	if(!is_duid($duid_ASCII)) {
3552
		return "--:--:--:--:--:--:--:--:--:--:--:--:--:--:--:--";
3553
	}
3554
	return($duid_ASCII);
3555
}
3556

    
3557
/* Replaces the Mac OS 9 and earlier (\r) and DOS/Windows (\r\n) newlines with the Unix equivalent (\n). */
3558
function unixnewlines($text) {
3559
	return preg_replace('/\r\n?/', "\n", $text);
3560
}
3561

    
3562
function array_remove_duplicate($array, $field) {
3563
	$cmp = array();
3564
	foreach ($array as $sub) {
3565
		if (isset($sub[$field])) {
3566
			$cmp[] = $sub[$field];
3567
		}
3568
	}
3569
	$unique = array_unique(array_reverse($cmp, true));
3570
	foreach (array_keys($unique) as $k) {
3571
		$new[] = $array[$k];
3572
	}
3573
	return $new;
3574
}
3575

    
3576
/**
3577
 * Return a value specified by a path of keys in a nested array, if it exists.
3578
 * @param $arr array value to search
3579
 * @param $path string path with '/' separators
3580
 * @param $default mixed value to return if the path is not found
3581
 * @returns mixed value at path or $default if the path does not exist or if the
3582
 *          path keys an empty string and $default is non-null
3583
 */
3584
function array_get_path(array &$arr, string $path, $default = null) {
3585
	$vpath = explode('/', $path);
3586
	$el = $arr;
3587
	foreach ($vpath as $key) {
3588
		if (mb_strlen($key) == 0) {
3589
			continue;
3590
		}
3591
		if (is_array($el) && array_key_exists($key, $el)) {
3592
			$el = $el[$key];
3593
		} else {
3594
			return ($default);
3595
		}
3596
	}
3597

    
3598
	if (($default !== null) && ($el === '')) {
3599
		return ($default);
3600
	}
3601

    
3602
	return ($el);
3603
}
3604

    
3605
/*
3606
 * Initialize an arbitrary array multiple levels deep only if unset
3607
 * @param $arr top of array
3608
 * @param $path string path with '/' separators
3609
 */
3610
function array_init_path(mixed &$arr, ?string $path)
3611
{
3612
	if (!is_array($arr)) {
3613
		$arr = [];
3614
	}
3615
	if (is_null($path)) {
3616
		return;
3617
	}
3618
	$tmp = &$arr;
3619
	foreach (explode('/', $path) as $key) {
3620
		if (!is_array($tmp[$key])) {
3621
			$tmp[$key] = [];
3622
		}
3623
		$tmp = &$tmp[$key];
3624
	}
3625
}
3626

    
3627
/**
3628
 * Set a value by path in a nested array, creating arrays for intermediary keys
3629
 * as necessary. If the path cannot be reached because an intermediary exists
3630
 * but is not empty or an array, return $default.
3631
 * @param $arr array value to search
3632
 * @param $path string path with '/' separators
3633
 * @param $value mixed
3634
 * @param $default mixed value to return if the path is not found
3635
 * @returns mixed $val or $default if the path prefix does not exist
3636
 */
3637
function array_set_path(array &$arr, string $path, $value, $default = null) {
3638
	$vpath = explode('/', $path);
3639
	$vkey = null;
3640
	do {
3641
		$vkey = array_pop($vpath);
3642
	} while (mb_strlen($vkey) == 0);
3643
	if ($vkey == null) {
3644
		return ($default);
3645
	}
3646
	$el =& $arr;
3647
	foreach ($vpath as $key) {
3648
		if (mb_strlen($key) == 0) {
3649
			continue;
3650
		}
3651
		if (array_key_exists($key, $el) && !empty($el[$key])) {
3652
			if (!is_array($el[$key])) {
3653
					return ($default);
3654
			}
3655
		} else {
3656
				$el[$key] = [];
3657
		}
3658
		$el =& $el[$key];
3659
	}
3660
	$el[$vkey] = $value;
3661
	return ($value);
3662
}
3663

    
3664
/**
3665
 * Determine whether a path in a nested array has a non-null value keyed by
3666
 * $enable_key.
3667
 * @param $arr array value to search
3668
 * @param $path string path with '/' separators
3669
 * @param $enable_key string an optional alternative key value for the enable key
3670
 * @returns bool true if $enable_key exists in the array at $path, and has a
3671
 * non-null value, otherwise false
3672
 */
3673
function array_path_enabled(array &$arr, string $path, $enable_key = "enable") {
3674
	$el = array_get_path($arr, $path, []);
3675
	if (is_array($el) && isset($el[$enable_key])) {
3676
		return (true);
3677
	}
3678
	return (false);
3679
}
3680

    
3681
/**
3682
 * Remove a key from the nested array by path.
3683
 * @param $arr array value to search
3684
 * @param $path string path with '/' separators
3685
 * @returns array copy of the removed value or null
3686
 */
3687
function array_del_path(array &$arr, string $path) {
3688
	$vpath = explode('/', $path);
3689
	$vkey = array_pop($vpath);
3690
	$el =& $arr;
3691
	foreach($vpath as $key) {
3692
		if (mb_strlen($key) == 0) {
3693
			continue;
3694
		}
3695
		if (is_array($el) && array_key_exists($key, $el)) {
3696
			$el =& $el[$key];
3697
		} else {
3698
			return null;
3699
		}
3700
	}
3701

    
3702
	if (!(is_array($el) && array_key_exists($vkey, $el))) {
3703
		return null;
3704
	}
3705

    
3706
	$ret = $el[$vkey];
3707
	unset($el[$vkey]);
3708
	return ($ret);
3709
}
3710

    
3711

    
3712
function dhcpd_date_adjust_gmt($dt) {
3713
	init_config_arr(array('dhcpd'));
3714

    
3715
	foreach (config_get_path('dhcpd', []) as $dhcpditem) {
3716
		if (empty($dhcpditem)) {
3717
			continue;
3718
		}
3719
		if ($dhcpditem['dhcpleaseinlocaltime'] == "yes") {
3720
			$ts = strtotime($dt . " GMT");
3721
			if ($ts !== false) {
3722
				return strftime("%Y/%m/%d %H:%M:%S", $ts);
3723
			}
3724
		}
3725
	}
3726

    
3727
	/*
3728
	 * If we did not need to convert to local time or the conversion
3729
	 * failed, just return the input.
3730
	 */
3731
	return $dt;
3732
}
3733

    
3734
global $supported_image_types;
3735
$supported_image_types = array(
3736
	IMAGETYPE_JPEG,
3737
	IMAGETYPE_PNG,
3738
	IMAGETYPE_GIF,
3739
	IMAGETYPE_WEBP
3740
);
3741

    
3742
function is_supported_image($image_filename) {
3743
	global $supported_image_types;
3744
	$img_info = getimagesize($image_filename);
3745

    
3746
	/* If it's not an image, or it isn't in the supported list, return false */
3747
	if (($img_info === false) ||
3748
	    !in_array($img_info[2], array_keys($supported_image_types))) {
3749
		return false;
3750
	} else {
3751
		return $img_info[2];
3752
	}
3753
}
3754

    
3755
function get_lagg_ports ($laggport) {
3756
	$laggp = array();
3757
	foreach ($laggport as $lgp) {
3758
		list($lpname, $lpinfo) = explode(" ", $lgp);
3759
		preg_match('~<(.+)>~', $lpinfo, $lgportmode);
3760
		if ($lgportmode[1]) {
3761
			$laggp[] = $lpname . " (" . $lgportmode[1] . ")";
3762
		} else {
3763
			$laggp[] = $lpname;
3764
		}
3765
	}
3766
	if ($laggp) {
3767
		return implode(", ", $laggp);
3768
	} else {
3769
		return false;
3770
	}
3771
}
3772

    
3773
function cisco_to_cidr($addr) {
3774
	if (!is_ipaddr($addr)) {
3775
		throw new Exception('Value is not in dotted quad notation.');
3776
	}
3777

    
3778
	$mask = decbin(~ip2long($addr));
3779
	$mask = substr($mask, -32);
3780
	$k = 0;
3781
	for ($i = 0; $i <= 32; $i++) {
3782
		$k += intval($mask[$i]);
3783
	}
3784
	return $k;
3785
}
3786

    
3787
function cisco_extract_index($prule) {
3788
	$index = explode("#", $prule);
3789
	if (is_numeric($index[1])) {
3790
		return intval($index[1]);
3791
	} else {
3792
		syslog(LOG_WARNING, "Error parsing rule {$prule}: Could not extract index");
3793
	}
3794
	return -1;;
3795
}
3796

    
3797
function parse_cisco_acl_rule($rule, $devname, $dir, $proto) {
3798
	$rule_orig = $rule;
3799
	$rule = explode(" ", $rule);
3800
	$tmprule = "";
3801
	$index = 0;
3802

    
3803
	if ($rule[$index] == "permit") {
3804
		$startrule = "pass {$dir} quick on {$devname} ";
3805
	} else if ($rule[$index] == "deny") {
3806
		$startrule = "block {$dir} quick on {$devname} ";
3807
	} else {
3808
		return;
3809
	}
3810

    
3811
	$index++;
3812

    
3813
	switch ($rule[$index]) {
3814
		case "ip":
3815
			break;
3816
		case "icmp":
3817
			$icmp = ($proto == "inet") ? "icmp" : "ipv6-icmp";
3818
			$tmprule .= "proto {$icmp} ";
3819
			break;
3820
		case "tcp":
3821
		case "udp":
3822
			$tmprule .= "proto {$rule[$index]} ";
3823
			break;
3824
		default:
3825
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid protocol.");
3826
			return;
3827
	}
3828
	$index++;
3829

    
3830
	/* Source */
3831
	if (trim($rule[$index]) == "host") {
3832
		$index++;
3833
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
3834
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
3835
			if ($GLOBALS['attributes']['framed_ip']) {
3836
				$tmprule .= "from {$GLOBALS['attributes']['framed_ip']} ";
3837
			} else {
3838
				$tmprule .= "from {$rule[$index]} ";
3839
			}
3840
			$index++;
3841
		} else {
3842
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source host '{$rule[$index]}'.");
3843
			return;
3844
		}
3845
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
3846
		$tmprule .= "from {$rule[$index]} ";
3847
		$index++;
3848
	} elseif (trim($rule[$index]) == "any") {
3849
		$tmprule .= "from any ";
3850
		$index++;
3851
	} else {
3852
		$network = $rule[$index];
3853
		$netmask = $rule[++$index];
3854

    
3855
		if (is_ipaddrv4($network) && ($proto == "inet")) {
3856
			try {
3857
				$netmask = cisco_to_cidr($netmask);
3858
			} catch(Exception $e) {
3859
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source netmask '$netmask' (" . $e->getMessage() . ").");
3860
				return;
3861
			}
3862
			$tmprule .= "from {$network}/{$netmask} ";
3863
		} else {
3864
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source network '$network'.");
3865
			return;
3866
		}
3867

    
3868
		$index++;
3869
	}
3870

    
3871
	/* Source Operator */
3872
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
3873
		switch(trim($rule[$index])) {
3874
			case "lt":
3875
				$operator = "<";
3876
				break;
3877
			case "gt":
3878
				$operator = ">";
3879
				break;
3880
			case "eq":
3881
				$operator = "=";
3882
				break;
3883
			case "neq":
3884
				$operator = "!=";
3885
				break;
3886
		}
3887

    
3888
		$port = $rule[++$index];
3889
		if (is_port($port)) {
3890
			$tmprule .= "port {$operator} {$port} ";
3891
		} else {
3892
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid source port: '$port' not a numeric value between 0 and 65535.");
3893
			return;
3894
		}
3895
		$index++;
3896
	} else if (trim($rule[$index]) == "range") {
3897
		$port = array($rule[++$index], $rule[++$index]);
3898
		if (is_port($port[0]) && is_port($port[1])) {
3899
			$tmprule .= "port {$port[0]}:{$port[1]} ";
3900
		} else {
3901
			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.");
3902
			return;
3903
		}
3904
		$index++;
3905
	}
3906

    
3907
	/* Destination */
3908
	if (trim($rule[$index]) == "host") {
3909
		$index++;
3910
		if ((($proto == 'inet') && (is_ipaddrv4(trim($rule[$index])) || (trim($rule[$index]) == "{clientip}"))) ||
3911
		    (($proto == 'inet6') && (is_ipaddrv6(trim($rule[$index])) || (trim($rule[$index]) == "{clientipv6}")))) {
3912
			$tmprule .= "to {$rule[$index]} ";
3913
			$index++;
3914
		} else {
3915
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination host '{$rule[$index]}'.");
3916
			return;
3917
		}
3918
	} elseif (is_subnetv6(trim($rule[$index])) && ($proto == "inet6")) {
3919
		$tmprule .= "to {$rule[$index]} ";
3920
		$index++;
3921
	} elseif (trim($rule[$index]) == "any") {
3922
		$tmprule .= "to any ";
3923
		$index++;
3924
	} else {
3925
		$network = $rule[$index];
3926
		$netmask = $rule[++$index];
3927

    
3928
		if (is_ipaddrv4($network) && ($proto == "inet")) {
3929
			try {
3930
				$netmask = cisco_to_cidr($netmask);
3931
			} catch(Exception $e) {
3932
				syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination netmask '$netmask' (" . $e->getMessage() . ").");
3933
				return;
3934
			}
3935
			$tmprule .= "to {$network}/{$netmask} ";
3936
		} else {
3937
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination network '$network'.");
3938
			return;
3939
		}
3940

    
3941
		$index++;
3942
	}
3943

    
3944
	/* Destination Operator */
3945
	if (in_array(trim($rule[$index]), array("lt", "gt", "eq", "neq"))) {
3946
		switch(trim($rule[$index])) {
3947
			case "lt":
3948
				$operator = "<";
3949
				break;
3950
			case "gt":
3951
				$operator = ">";
3952
				break;
3953
			case "eq":
3954
				$operator = "=";
3955
				break;
3956
			case "neq":
3957
				$operator = "!=";
3958
				break;
3959
		}
3960

    
3961
		$port = $rule[++$index];
3962
		if (is_port($port)) {
3963
			$tmprule .= "port {$operator} {$port} ";
3964
		} else {
3965
			syslog(LOG_WARNING, "Error parsing rule {$rule_orig}: Invalid destination port: '$port' not a numeric value between 0 and 65535.");
3966
			return;
3967
		}
3968
		$index++;
3969
	} else if (trim($rule[$index]) == "range") {
3970
		$port = array($rule[++$index], $rule[++$index]);
3971
		if (is_port($port[0]) && is_port($port[1])) {
3972
			$tmprule .= "port {$port[0]}:{$port[1]} ";
3973
		} else {
3974
			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.");
3975
			return;
3976
		}
3977
		$index++;
3978
	}
3979

    
3980
	$tmprule = $startrule . $proto . " " . $tmprule;
3981
	return $tmprule;
3982
}
3983

    
3984
function parse_cisco_acl($attribs, $dev) {
3985
	global $attributes;
3986

    
3987
	if (!is_array($attribs)) {
3988
		return "";
3989
	}
3990
	$finalrules = "";
3991
	if (is_array($attribs['ciscoavpair'])) {
3992
		$inrules = array('inet' => array(), 'inet6' => array());
3993
		$outrules = array('inet' => array(), 'inet6' => array());
3994
		foreach ($attribs['ciscoavpair'] as $avrules) {
3995
			$rule = explode("=", $avrules);
3996
			$dir = "";
3997
			if (strstr($rule[0], "inacl")) {
3998
				$dir = "in";
3999
			} else if (strstr($rule[0], "outacl")) {
4000
				$dir = "out";
4001
			} else if (strstr($rule[0], "dns-servers")) {
4002
				$attributes['dns-servers'] = explode(" ", $rule[1]);
4003
				continue;
4004
			} else if (strstr($rule[0], "route")) {
4005
				if (!is_array($attributes['routes'])) {
4006
					$attributes['routes'] = array();
4007
				}
4008
				$attributes['routes'][] = $rule[1];
4009
				continue;
4010
			}
4011
			$rindex = cisco_extract_index($rule[0]);
4012
			if ($rindex < 0) {
4013
				continue;
4014
			}
4015

    
4016
			if (strstr($rule[0], "ipv6")) {
4017
				$proto = "inet6";
4018
			} else {
4019
				$proto = "inet";
4020
			}
4021

    
4022
			$tmprule = parse_cisco_acl_rule($rule[1], $dev, $dir, $proto);
4023
			if (!empty($tmprule)) {
4024
				if ($dir == "in") {
4025
					$inrules[$proto][$rindex] = $tmprule;
4026
				} else if ($dir == "out") {
4027
					$outrules[$proto][$rindex] = $tmprule;
4028
				}
4029
			}
4030
		}
4031

    
4032

    
4033
		$state = "";
4034
		foreach (array('inet', 'inet6') as $ip) {
4035
			if (!empty($outrules[$ip])) {
4036
				$state = "no state";
4037
			}
4038
			ksort($inrules[$ip], SORT_NUMERIC);
4039
			foreach ($inrules[$ip] as $inrule) {
4040
				$finalrules .= "{$inrule} {$state}\n";
4041
			}
4042
			if (!empty($outrules[$ip])) {
4043
				ksort($outrules[$ip], SORT_NUMERIC);
4044
				foreach ($outrules[$ip] as $outrule) {
4045
					$finalrules .= "{$outrule} {$state}\n";
4046
				}
4047
			}
4048
		}
4049
	}
4050
	return $finalrules;
4051
}
4052

    
4053
function alias_idn_to_utf8($alias) {
4054
	if (is_alias($alias)) {
4055
		return $alias;
4056
	} else {
4057
		return idn_to_utf8($alias);
4058
	}
4059
}
4060

    
4061
function alias_idn_to_ascii($alias) {
4062
	if (is_alias($alias)) {
4063
		return $alias;
4064
	} else {
4065
		return idn_to_ascii($alias);
4066
	}
4067
}
4068

    
4069
// These functions were in guiconfig.inc but have been moved here so non GUI processes can use them
4070
function address_to_pconfig($adr, &$padr, &$pmask, &$pnot, &$pbeginport, &$pendport) {
4071
	if (isset($adr['any'])) {
4072
		$padr = "any";
4073
	} else if ($adr['network']) {
4074
		$padr = $adr['network'];
4075
	} else if ($adr['address']) {
4076
		list($padr, $pmask) = explode("/", $adr['address']);
4077
		if (!$pmask) {
4078
			if (is_ipaddrv6($padr)) {
4079
				$pmask = 128;
4080
			} else {
4081
				$pmask = 32;
4082
			}
4083
		}
4084
	}
4085

    
4086
	if (isset($adr['not'])) {
4087
		$pnot = 1;
4088
	} else {
4089
		$pnot = 0;
4090
	}
4091

    
4092
	if ($adr['port']) {
4093
		list($pbeginport, $pendport) = explode("-", $adr['port']);
4094
		if (!$pendport) {
4095
			$pendport = $pbeginport;
4096
		}
4097
	} else if (!is_alias($pbeginport) && !is_alias($pendport)) {
4098
		$pbeginport = "any";
4099
		$pendport = "any";
4100
	}
4101
}
4102

    
4103
// does not support specialnet VIPs
4104
function pconfig_to_address(&$adr, $padr, $pmask, $pnot = false, $pbeginport = 0, $pendport = 0, $addmask = false, $specialnet_flags = []) {
4105
	$adr = array();
4106

    
4107
	if ($padr == "any") {
4108
		$adr['any'] = true;
4109
	} else if (get_specialnet($padr, $specialnet_flags)) {
4110
		if ($addmask) {
4111
			$padr .= "/" . $pmask;
4112
		}
4113
		$adr['network'] = $padr;
4114
	} else {
4115
		$adr['address'] = $padr;
4116
		if (is_ipaddrv6($padr)) {
4117
			if ($pmask != 128) {
4118
				$adr['address'] .= "/" . $pmask;
4119
			}
4120
		} else {
4121
			if ($pmask != 32) {
4122
				$adr['address'] .= "/" . $pmask;
4123
			}
4124
		}
4125
	}
4126

    
4127
	if ($pnot) {
4128
		$adr['not'] = true;
4129
	} else {
4130
		unset($adr['not']);
4131
	}
4132

    
4133
	if (($pbeginport != 0) && ($pbeginport != "any")) {
4134
		if ($pbeginport != $pendport) {
4135
			$adr['port'] = $pbeginport . "-" . $pendport;
4136
		} else {
4137
			$adr['port'] = $pbeginport;
4138
		}
4139
	}
4140

    
4141
	/*
4142
	 * If the port is still unset, then it must not be numeric, but could
4143
	 * be an alias or a well-known/registered service.
4144
	 * See https://redmine.pfsense.org/issues/8410
4145
	 */
4146
	if (!isset($adr['port']) && is_port_or_alias($pbeginport)) {
4147
		$adr['port'] = $pbeginport;
4148
	}
4149
}
4150

    
4151
function get_specialnet(?string $net = '', array $flags = [], ?string $if = ''):bool|array {
4152
	if ($net === null || $if === null) {
4153
		return false;
4154
	}
4155
	$checkpermission = false;
4156
	$allnetflags = [SPECIALNET_ANY, SPECIALNET_SELF, SPECIALNET_CLIENTS,
4157
	                SPECIALNET_IFADDR, SPECIALNET_IFSUB, SPECIALNET_IFNET,
4158
	                SPECIALNET_GROUP, SPECIALNET_VIPS];
4159

    
4160
	if (empty($flags)) {
4161
		$flags = $allnetflags;
4162
	} else {
4163
		if (in_array(SPECIALNET_CHECKPERM, $flags)) {
4164
			$checkpermission = true;
4165
		}
4166
		if (in_array(SPECIALNET_EXCLUDE, $flags)) {
4167
			$flags = array_diff($allnetflags, $flags);
4168
		}
4169
	}
4170
	$specialnet = [];
4171
	if (in_array(SPECIALNET_IFADDR, $flags) || in_array(SPECIALNET_IFSUB, $flags) || in_array(SPECIALNET_IFNET, $flags)) {
4172
		$ifmacros = get_configured_interface_with_descr();
4173
	}
4174
	foreach ($flags as $include) {
4175
		switch ($include) {
4176
			case SPECIALNET_NONE:
4177
				$specialnet = array_merge($specialnet, ['none' => gettext('None')]);
4178
				break;
4179
			case SPECIALNET_ANY:
4180
				$specialnet = array_merge($specialnet, ['any' => gettext('Any')]);
4181
				break;
4182
			case SPECIALNET_COMPAT_ADDR:
4183
			case SPECIALNET_ADDR:
4184
				if ($include == SPECIALNET_COMPAT_ADDR) {
4185
					$ifname = 'single';
4186
				} else {
4187
					$ifname = 'address';
4188
				}
4189
				$specialnet = array_merge($specialnet, [$ifname => gettext('Address')]);
4190
				break;
4191
			case SPECIALNET_COMPAT_ADDRAL:
4192
			case SPECIALNET_ADDRAL:
4193
				if ($include == SPECIALNET_COMPAT_ADDRAL) {
4194
					$ifname = 'single';
4195
				} else {
4196
					$ifname = 'address';
4197
				}
4198
				$specialnet = array_merge($specialnet, [$ifname => gettext('Address or Alias')]);
4199
				break;
4200
			case SPECIALNET_NET:
4201
			case SPECIALNET_NETAL:
4202
				if ($include == SPECIALNET_NET) {
4203
					$ifdescr = 'Network';
4204
				} else {
4205
					$ifdescr = 'Network or Alias';
4206
				}
4207
				$specialnet = array_merge($specialnet, ['network' => gettext($ifdescr)]);
4208
				break;
4209
			case SPECIALNET_SELF:
4210
				$specialnet = array_merge($specialnet, ['(self)' => gettext('This Firewall (self)')]);
4211
				break;
4212
			case SPECIALNET_CLIENTS:
4213
				if ($checkpermission) {
4214
					$ifallowed = [];
4215
					if (have_ruleint_access("pptp")) {
4216
						$ifallowed['pptp'] = gettext('PPTP clients');
4217
					}
4218
					if (have_ruleint_access("pppoe")) {
4219
						$ifallowed['pppoe'] = gettext('PPPoE clients');
4220
					}
4221
					if (have_ruleint_access("l2tp")) {
4222
						$ifallowed['l2tp'] = gettext('L2TP clients');
4223
					}
4224
					$specialnet = array_merge($specialnet, $ifallowed);
4225
				} else {
4226
					$specialnet = array_merge($specialnet, [
4227
					                          'pptp' => gettext('PPTP clients'),
4228
					                          'pppoe' => gettext('PPPoE clients'),
4229
					                          'l2tp' => gettext('L2TP clients')]);
4230
				}
4231
				break;
4232
			case SPECIALNET_IFADDR:
4233
			case SPECIALNET_IFSUB:
4234
			case SPECIALNET_IFNET:
4235
				if ($include == SPECIALNET_IFADDR) {
4236
					$ifname_suffix = 'ip';
4237
					$ifdescr_suffix = ' address';
4238
				} elseif ($include == SPECIALNET_IFSUB) {
4239
					$ifname_suffix = '';
4240
					$ifdescr_suffix = ' subnet';
4241
				} else {
4242
					$ifname_suffix = '';
4243
					$ifdescr_suffix = ' subnets';
4244
				}
4245
				foreach ($ifmacros as $ifname => $ifdescr) {
4246
					// Only add macros for allowed interfaces when checking permissions
4247
					if ($checkpermission && !have_ruleint_access($ifname)) {
4248
						continue;
4249
					}
4250
					// If an interface is specified, only add interface macros for that interface
4251
					if (!empty($if)) {
4252
						if ($if != $ifname) {
4253
							continue;
4254
						} else {
4255
							$specialnet[$ifname . $ifname_suffix] = $ifdescr . $ifdescr_suffix;
4256
							break;
4257
						}
4258
					} else {
4259
						$specialnet[$ifname . $ifname_suffix] = $ifdescr . $ifdescr_suffix;
4260
					}
4261
				}
4262
				break;
4263
			case SPECIALNET_GROUP:
4264
				foreach (config_get_path('ifgroups/ifgroupentry', []) as $ifgen) {
4265
					$ifgenmembers = explode(' ', $ifgen['members']);
4266
					foreach ($ifgenmembers as $ifgmember) {
4267
						// Skip interface groups with members which are not allowed
4268
						if ($checkpermission && !have_ruleint_access($ifgmember)) {
4269
							continue 2;
4270
						}
4271
						// Skip interface groups which do not include the specified interface
4272
						if (!empty($if) && $if != $ifgmember &&
4273
						    $ifgmember == $ifgenmembers[array_key_last($ifgenmembers)]) {
4274
							continue 2;
4275
						}
4276
					}
4277
					$specialnet[$ifgen['ifname']] = $ifgen['ifname'] . ' networks';
4278
				}
4279
				break;
4280
			case SPECIALNET_VIPS:
4281
				$tmp_vips = [];
4282
				foreach (config_get_path('virtualip/vip', []) as $vip) {
4283
					// Only add VIPs for allowed interfaces when checking permissions
4284
					if ($checkpermission && !empty($vip['interface']) && !have_ruleint_access($vip['interface'])) {
4285
						continue;
4286
					}
4287
					$tmp_vips[$vip['subnet'] . '/' . $vip['subnet_bits']] = $vip;
4288
				}
4289
				asort($tmp_vips, SORT_NATURAL);
4290
				foreach ($tmp_vips as $vipsn => $sn) {
4291
					if (($sn['mode'] == "proxyarp" || $sn['mode'] == "other") && $sn['type'] == "network") {
4292
						$specialnet[$vipsn] = 'Subnet: ' . $sn['subnet'] . '/' . $sn['subnet_bits'] . ' (' . $sn['descr'] . ')';
4293
						// Skip expanding IPv4 VIPs with the option, as well as any IPv6 network
4294
						if (isset($sn['noexpand']) || strstr($sn['subnet'], ':' !== false)) {
4295
							continue;
4296
						}
4297
						$start = ip2long32(gen_subnet($sn['subnet'], $sn['subnet_bits']));
4298
						$end = ip2long32(gen_subnet_max($sn['subnet'], $sn['subnet_bits']));
4299
						$len = $end - $start;
4300
						for ($i = 0; $i <= $len; $i++) {
4301
							$snip = ip2long32($start+$i);
4302
			
4303
							$specialnet[$snip] = $snip . ' (' . $sn['descr'] . ')';
4304
						}
4305
					} else {
4306
						$specialnet[$sn['subnet']] = $sn['subnet'] . ' (' . $sn['descr'] . ')';
4307
					}
4308
				}
4309
				break;
4310
			default:
4311
				break;
4312
		}
4313
	}
4314

    
4315
	if (empty($net)) {
4316
		return $specialnet;
4317
	} elseif (array_key_exists($net, $specialnet)) {
4318
		return true;
4319
	} else {
4320
		return false;
4321
	}
4322
}
4323

    
4324
function is_interface_ipaddr($interface) {
4325
	if (!empty(config_get_path("interfaces/{$interface}/ipaddr"))) {
4326
		return true;
4327
	}
4328
	return false;
4329
}
4330

    
4331
function is_interface_ipaddrv6($interface) {
4332
	if (!empty(config_get_path("interfaces/{$interface}/ipaddrv6"))) {
4333
		return true;
4334
	}
4335
	return false;
4336
}
4337

    
4338
function escape_filter_regex($filtertext) {
4339
	/* If the caller (user) has not already put a backslash before a slash, to escape it in the regex, */
4340
	/* then this will do it. Take out any "\/" already there, then turn all ordinary "/" into "\/".    */
4341
	return str_replace('/', '\/', str_replace('\/', '/', $filtertext));
4342
}
4343

    
4344
/*
4345
 * Check if a given pattern has the same number of two different unescaped
4346
 * characters.
4347
 * For example, it can ensure a pattern has balanced sets of parentheses,
4348
 * braces, and brackets.
4349
 */
4350
function is_pattern_balanced_char($pattern, $open, $close) {
4351
	/* First remove escaped versions */
4352
	$pattern = str_replace('\\' . $open, '', $pattern);
4353
	$pattern = str_replace('\\' . $close, '', $pattern);
4354
	/* Check if the counts of both characters match in the target pattern */
4355
	return (substr_count($pattern, $open) == substr_count($pattern, $close));
4356
}
4357

    
4358
/*
4359
 * Check if a pattern contains balanced sets of parentheses, braces, and
4360
 * brackets.
4361
 */
4362
function is_pattern_balanced($pattern) {
4363
	if (is_pattern_balanced_char($pattern, '(', ')') &&
4364
	    is_pattern_balanced_char($pattern, '{', '}') &&
4365
	    is_pattern_balanced_char($pattern, '[', ']')) {
4366
		/* Balanced if all are true */
4367
		return true;
4368
	}
4369
	return false;
4370
}
4371

    
4372
function cleanup_regex_pattern($filtertext) {
4373
	/* Cleanup filter to prevent backreferences. */
4374
	$filtertext = escape_filter_regex($filtertext);
4375

    
4376
	/* Remove \<digit>+ backreferences
4377
	 * To match \ it must be escaped as \\\\ in PHP for preg_replace() */
4378
	$filtertext = preg_replace('/\\\\\\d+/', '', $filtertext);
4379

    
4380
	/* Check for unbalanced parentheses, braces, and brackets which
4381
	 * may be an error or attempt to circumvent protections.
4382
	 * Also discard any pattern that attempts problematic duplication
4383
	 * methods. */
4384
	if (!is_pattern_balanced($filtertext) ||
4385
	    (substr_count($filtertext, ')*') > 0) ||
4386
	    (substr_count($filtertext, ')+') > 0) ||
4387
	    (substr_count($filtertext, '{') > 0)) {
4388
		return '';
4389
	}
4390

    
4391
	return $filtertext;
4392
}
4393

    
4394
function ip6_to_asn1($addr) {
4395
	/* IPv6 MIB uses an OCTET STRING of length 16 to represent
4396
	 * 128-bit IPv6 address in network byte order.
4397
	 * see https://datatracker.ietf.org/doc/html/rfc2465#section-3
4398
	 * i.e. fc00:3::4 = 252.0.0.3.0.0.0.0.0.0.0.0.0.0.0.4
4399
	 */
4400

    
4401
	if (!is_ipaddrv6($addr)) {
4402
		return false;
4403
	}
4404
	$ipv6str = "";
4405
	$octstr = "";
4406
	foreach (explode(':', Net_IPv6::uncompress($addr)) as $v) {
4407
		$ipv6str .= str_pad($v, 4, '0', STR_PAD_LEFT);
4408
	}
4409
	foreach (str_split($ipv6str, 2) as $v) {
4410
		$octstr .= base_convert($v, 16, 10) . '.';
4411
	}
4412

    
4413
	return $octstr;
4414
}
4415

    
4416
function interfaces_interrupts() {
4417
	exec("/usr/bin/vmstat -i --libxo json", $rawdata, $rc);
4418
	$interrupts = array();
4419
	if ($rc == 0) {
4420
		$vnstatarr = json_decode(implode(" ", $rawdata), JSON_OBJECT_AS_ARRAY);
4421
		$interruptarr = $vnstatarr['interrupt-statistics']['interrupt'];
4422

    
4423
		foreach ($interruptarr as $int){
4424
			preg_match("/irq\d+: ([a-z0-9]+)/", $int['name'], $matches);
4425
			$name = $matches[1];
4426
			if (array_key_exists($name, $interrupts)) {
4427
				/* interface with multiple queues */
4428
				$interrupts[$name]['total'] += $int['total'];
4429
				$interrupts[$name]['rate'] += $int['rate'];
4430
			} else {
4431
				$interrupts[$name]['total'] = $int['total'];
4432
				$interrupts[$name]['rate'] = $int['rate'];
4433
			}
4434
		}
4435
	}
4436

    
4437
	return $interrupts;
4438
}
4439

    
4440
function dummynet_load_module($max_qlimit) {
4441
	if (!is_module_loaded("dummynet.ko")) {
4442
		mute_kernel_msgs();
4443
		mwexec("/sbin/kldload dummynet");
4444
		unmute_kernel_msgs();
4445
	}
4446
	$sysctls = (array(
4447
			"net.inet.ip.dummynet.io_fast" => "1",
4448
			"net.inet.ip.dummynet.hash_size" => "256",
4449
			"net.inet.ip.dummynet.pipe_slot_limit" => $max_qlimit
4450
	));
4451
	init_config_arr(array('sysctl', 'item'));
4452
	foreach (config_get_path('sysctl/item', []) as $item) {
4453
		if (preg_match('/net\.inet\.ip\.dummynet\./', $item['tunable'])) {
4454
			$sysctls[$item['tunable']] = $item['value'];
4455
		}
4456
	}
4457
	set_sysctl($sysctls);
4458
}
4459

    
4460
function get_interface_vip_ips($interface) {
4461
	global $config;
4462
	$vipips = '';
4463

    
4464
	init_config_arr(array('virtualip', 'vip'));
4465
	foreach (config_get_path('virtualip/vip', []) as $vip) {
4466
		if (($vip['interface'] == $interface) &&
4467
		    (($vip['mode'] == "carp") || ($vip['mode'] == "ipalias"))) {
4468
			$vipips .= $vip['subnet'] . ' ';
4469
		}
4470
	}
4471
	return $vipips;
4472
}
4473

    
4474
/**
4475
 * Checks for DNS availability.
4476
 * 
4477
 * @param int $protocol IP family to check with [AF_INET|AF_INET6]
4478
 * 
4479
 * @return bool true if DNS is available, false otherwise
4480
 */
4481
function get_dnsavailable(?int $protocol = null): bool {
4482
	switch ($protocol) {
4483
		case AF_INET:
4484
			$proto = '-4';
4485
			break;
4486
		case AF_INET6:
4487
			$proto = '-6';
4488
			break;
4489
		default:
4490
			$proto = '';
4491
			break;
4492
	}
4493

    
4494
	// The domain doesn't matter, only that there's an answer
4495
	exec("/usr/bin/host -W 1 {$proto} netgate.com.", $null, $result_code);
4496
	if ($result_code === 0) {
4497
		// DNS server responded
4498
		return true;
4499
	}
4500

    
4501
	// no answer
4502
	return false;
4503
}
4504

    
4505
/**
4506
 * Verifies the existence of a DNS A/AAAA/SRV/PTR record for an address.
4507
 * 
4508
 * @param string $address URL, name, or IPv4/6 address to check.
4509
 * 
4510
 * @return string The address can be resolved. If $address is an IP address,
4511
 *                return the reverse lookup name, otherwise return the
4512
 *                unmodified input if it's a host/domain name.
4513
 * @return false Cannot resolve - no records found.
4514
 */
4515
function resolve_address(?string $address): string|false {
4516
	if (empty($address)) {
4517
		return false;
4518
	}
4519

    
4520
	// Try once per server and timeout after 3 seconds per attempt - see resolver(5)
4521
	putenv("RES_OPTIONS=timeout:3 attempts:1");
4522
	if (filter_var($address, FILTER_VALIDATE_IP)) {
4523
		$result = gethostbyaddr($address);
4524
		if (empty($result) || ($result == $address)) {
4525
			$result = null;
4526
		}
4527
	} elseif (filter_var($address, FILTER_VALIDATE_URL)) {
4528
		if (!empty(dns_get_record(parse_url($address, PHP_URL_HOST), DNS_A | DNS_AAAA | DNS_SRV))) {
4529
			$result = $address;
4530
		}
4531
	} elseif (filter_var($address, FILTER_VALIDATE_DOMAIN)) {
4532
		if (!empty(dns_get_record($address, DNS_A | DNS_AAAA | DNS_SRV))) {
4533
			$result = $address;
4534
		}
4535
	}
4536
	putenv('RES_OPTIONS');
4537

    
4538
	if (!empty($result)) {
4539
		return $result;
4540
	}
4541

    
4542
	return false;
4543
}
4544

    
4545
?>
(54-54/61)