Project

General

Profile

Download (14.9 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * wg.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2021 Rubicon Communications, LLC (Netgate)
7
 * All rights reserved.
8
 *
9
 * Licensed under the Apache License, Version 2.0 (the "License");
10
 * you may not use this file except in compliance with the License.
11
 * You may obtain a copy of the License at
12
 *
13
 * http://www.apache.org/licenses/LICENSE-2.0
14
 *
15
 * Unless required by applicable law or agreed to in writing, software
16
 * distributed under the License is distributed on an "AS IS" BASIS,
17
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
 * See the License for the specific language governing permissions and
19
 * limitations under the License.
20
 */
21

    
22
require_once("globals.inc");
23
require_once('config.inc');
24

    
25
$wgbinpath = '/usr/local/bin/wg';
26

    
27
// Gnerate a preshared key
28
function gerneratePSK() {
29
	global $wgbinpath;
30

    
31
	return exec("{$wgbinpath} genpsk");
32
}
33

    
34
// Return the next available WireGuard port
35
function next_wg_port() {
36
	global $config;
37

    
38
	init_config_arr(array('wireguard', 'tunnel'));
39
	$tunnels = &$config['wireguard']['tunnel'];
40

    
41
	for ($idx=51820; $idx<65535; $idx++) {
42
		// Check to see if the port is already in use
43
		$found = false;
44
		foreach ($tunnels as $tunnel) {
45
			if ($tunnel['interface']['listenport'] == $idx) {
46
				$found = true;
47
				break;
48
			}
49
		}
50

    
51
		// If not, it can be used
52
		if (!$found) {
53
			return $idx;
54
		}
55
	}
56

    
57
	return 51820;
58
}
59

    
60
// Return the next available WireGuard interface
61
function next_wg_if() {
62
	global $config;
63
	init_config_arr(array('wireguard', 'tunnel'));
64
	$tunnels = &$config['wireguard']['tunnel'];
65

    
66
	$used_ifs = array();
67
	foreach ($tunnels as $tunnel) {
68
		$used_ifs[] = $tunnel['name'];
69
	}
70

    
71
	for ($ifnum = 0; $ifnum < 32768; $ifnum++) {
72
		$want_if = "wg{$ifnum}";
73
		if (!in_array($want_if, $used_ifs)) {
74
			return $want_if;
75
		}
76
	}
77
	return -1;
78
}
79

    
80
// Validate the user's input and return error messages if not acceptable
81
function wg_validate_post($pconfig) {
82
	$input_errors = array();
83

    
84
	// Check the addresses
85
	$addrs = explode(",", $pconfig['interface']['address']);
86

    
87
	foreach ($addrs as $addr) {
88
		$addr = trim($addr);
89

    
90
		if (!is_subnet($addr)) {
91
			$input_errors[] = sprintf(gettext(
92
			    '%1$s is not a valid CIDR address'), $addr);
93
		}
94

    
95
		$iflist = get_configured_interface_list_by_realif();
96

    
97
		$skip = '';
98
		if (!empty($iflist[$pconfig['name']])) {
99
			if (!isset($pconfig['enabled']) ||
100
			    ($pconfig['enabled'] != 'yes')) {
101
				$input_errors[] = gettext('Cannot disable a WireGuard instance while it is assigned as an interface.');
102
			}
103
			$skip = $iflist[$pconfig['name']];
104
		}
105

    
106
		$a = explode("/", $addr);
107
		$conflicts = where_is_ipaddr_configured($a[0], $skip, true,
108
		    true, $a[1]);
109

    
110
		if (!empty($conflicts)) {
111
			foreach ($conflicts as $conflict) {
112
				$input_errors[] = sprintf(gettext(
113
				    '%1$s is already configured on this ' .
114
				    'firewall: %2$s (%3$s)'), $addr,
115
					strtoupper($conflict['if']),
116
					$conflict['ip_or_subnet']);
117
			}
118
		}
119
	}
120

    
121
	// Check listen port
122
	$lport = $pconfig['interface']['listenport'];
123
	if (!empty($lport) && (!ctype_digit($lport) || !is_port($lport))) {
124
		$input_errors[] = gettext("Invalid interface listen port.");
125
	}
126

    
127
	// Check keys
128
	if (empty($pconfig['interface']['privatekey'])) {
129
		$input_errors[] = gettext("Private key must be specified");
130
	}
131

    
132
	// Now the peers
133
	if (isset($pconfig['peers']['wgpeer'])) {
134
		$idx = 0;
135
		foreach ($pconfig['peers']['wgpeer'] as $peer) {
136
			$input_errors = array_merge($input_errors,
137
			    wg_validate_peer($idx, $peer));
138
			$idx++;
139
		}
140
	}
141

    
142
	return $input_errors;
143
}
144

    
145
// Valildate a peer
146
function wg_validate_peer($idx, $peer) {
147
	$input_errors = array();
148

    
149
	// Check remote port
150
	$rport = $peer['port'];
151
	if (!empty($rport) && (!ctype_digit($rport) || !is_port($rport))) {
152
		$input_errors[] = sprintf(gettext(
153
		    'Peer %1$s: Invalid remote port. (%2$s)'), $idx, $rport);
154
	}
155

    
156
	// Check key
157
	if (empty($peer['publickey'])) {
158
		$input_errors[] = sprintf(gettext(
159
		    'Peer %1$s: A public key must be specified'), $idx);
160
	}
161

    
162
	// Endpoint
163
	$ep = trim($peer['endpoint']);
164
	if (!empty($ep) && !is_hostname($ep) && !is_ipaddr($ep)) {
165
		$input_errors[] = sprintf(gettext(
166
		    'Peer %1$s: Endpoint must be a valid IPv4 or IPv6 ' .
167
		    'adress or hostname.'), $idx);
168
	}
169

    
170
	// Allowed IPs
171
	if (!empty($peer['allowedips'])) {
172
		foreach (explode(",", $peer['allowedips']) as $ip) {
173
			if (!is_subnet(trim($ip))) {
174
				$input_errors[] = sprintf(gettext(
175
				    'Peer %1$s: Address %2$s is not a valid ' .
176
				    'IPv4 or IPv6 CIDR subnet aadress.'),
177
					$idx, $ip);
178
			}
179
		}
180
	}
181

    
182
	return $input_errors;
183
}
184

    
185
// Setup all WireGuard tunnels
186
function wg_configure() {
187
	global $config;
188

    
189
	if (!is_array($config["wireguard"]["tunnel"])) {
190
		return;
191
	}
192

    
193
	if (platform_booting()) {
194
		echo gettext("Configuring WireGuard Tunnels... ");
195
	}
196

    
197
	wg_create_config_files();
198

    
199
	foreach ($config["wireguard"]["tunnel"] as $tunnel) {
200
		if (isset($tunnel['enabled']) && $tunnel['enabled'] == 'yes') {
201
			wg_configure_if($tunnel['name']);
202
		} else {
203
			wg_destroy_if($tunnel['name']);
204
		}
205
	}
206

    
207
	if (platform_booting()) {
208
		echo gettext("done.") . "\n";
209
	}
210
}
211

    
212
// Setup WireGuard tunnel
213
function wg_configure_if($wg_ifname) {
214
	global $config, $g;
215

    
216
	if (!is_array($config["wireguard"]["tunnel"])) {
217
		return;
218
	}
219

    
220
	unset($tunnel);
221
	foreach ($config["wireguard"]["tunnel"] as $tun) {
222
		if (isset($tun['enabled']) && $tun['enabled'] == 'yes' &&
223
		    $tun['name'] == $wg_ifname) {
224
			$tunnel = $tun;
225
			break;
226
		}
227
	}
228

    
229
	if (!is_array($tunnel)) {
230
		return;
231
	}
232

    
233
	wg_destroy_if($tunnel['name']);
234
	//$if = pfSense_interface_create($tunnel['name']);
235
	$conf_path = $g["wg_conf_path"] . "/" . $tunnel['name'] . ".conf";
236
	/* XXX - workaround while pfSense-module is fixed */
237
	mwexec("/sbin/ifconfig " . escapeshellarg($tunnel['name']) . " create");
238
	mwexec("/usr/local/bin/wg setconf " . escapeshellarg($tunnel['name']) .
239
	    " " . escapeshellarg($conf_path));
240
	$ip4_first = true;
241
	$ip6_first = true;
242
	foreach (explode(",", $tunnel['interface']['address']) as $addr) {
243
		if (strstr($addr, "/") == false) {
244
			continue;
245
		}
246
		list($ip, $mask) = explode("/", trim($addr));
247
		if (is_ipaddrv4($ip)) {
248
			mwexec("/sbin/ifconfig " .
249
			    escapeshellarg($tunnel['name']) . " inet " .
250
			    escapeshellarg($ip) . " netmask " .
251
			    escapeshellarg(gen_subnet_mask($mask)) .
252
			    ($ip4_first ? "" : " alias"));
253
			if ($ip4_first) {
254
				file_put_contents("/tmp/{$tunnel['name']}_router", wg_find_tunnel_gw($tunnel, $addr));
255
			}
256
			$ip4_first = false;
257
		} elseif (is_ipaddrv6($ip)) {
258
			mwexec("/sbin/ifconfig " .
259
			    escapeshellarg($tunnel['name']) . " inet6 " .
260
			    escapeshellarg($ip) . " netmask " .
261
			    escapeshellarg(gen_subnet_mask($mask)) .
262
			    ($ip6_first ? "" : " alias"));
263
			if ($ip6_first) {
264
				file_put_contents("/tmp/{$tunnel['name']}_routerv6", wg_find_tunnel_gw($tunnel, $addr));
265
			}
266
			$ip6_first = false;
267
		}
268
	}
269

    
270
	system_routing_configure(
271
	    convert_real_interface_to_friendly_interface_name($wg_ifname));
272

    
273
	if (is_array($tunnel['peers']) && is_array($tunnel['peers']['wgpeer']) &&
274
	    count($tunnel['peers']['wgpeer']) > 0) {
275
		foreach ($tunnel['peers']['wgpeer'] as $peer) {
276
			if (!empty($peer['allowedips'])) {
277
				foreach (explode(",", $peer['allowedips']) as $ip) {
278
					$ip = trim($ip);
279
					if (empty($ip) ||
280
					    ($ip == '0.0.0.0/0') ||
281
					    ($ip == '::/0')) {
282
						continue;
283
					}
284
					route_add_or_change($ip, $tunnel['name'], $tunnel['name']);
285
				}
286
			}
287
		}
288
	}
289
}
290

    
291
// Remove WireGuard tunnel
292
function wg_destroy_if($wg_ifname) {
293
	return (pfSense_interface_destroy($wg_ifname));
294
}
295

    
296
// Generate private key
297
function genKeyPair($json = false) {
298
	global $wgbinpath;
299

    
300
	$privkey = exec("{$wgbinpath} genkey");
301
	$pubkey = genPubKey($privkey);
302

    
303
	$res = array('privkey' => $privkey, 'pubkey' => $pubkey);
304
	return $json ? json_encode($res) : $res;
305
}
306

    
307
// Compose the public key from a provided private key
308
function genPubKey($privkey) {
309
	global $wgbinpath;
310

    
311
	return (exec("echo {$privkey} | {$wgbinpath} pubkey"));
312
}
313

    
314
function deleteTunnel($tunidx) {
315
	global $config, $g;
316

    
317
	if ($config['wireguard']['tunnel'][$tunidx]) {
318
		unset($ifname);
319
		unset($conf_path);
320
		if (isset($config['wireguard']['tunnel'][$tunidx]['name'])) {
321
			$ifname =
322
			    $config['wireguard']['tunnel'][$tunidx]['name'];
323
		}
324
		if (isset($ifname)) {
325
			$conf_path = $g["wg_conf_path"] . "/" . $ifname .
326
			    ".conf";
327
		}
328
		// Delete the tunnel configuration entry
329
		unset($config['wireguard']['tunnel'][$tunidx]);
330
		write_config("WireGuard tunnel {$index} updated.");
331

    
332
		// Delete the wg?.conf file
333
		if (isset($conf_path) && is_file($conf_path)) {
334
			unlink($conf_path);
335
		}
336

    
337
		// Destroy the deleted tunnel
338
		wg_destroy_if($ifname);
339
	}
340
}
341

    
342
// Write new tunnel values to the configuration system
343
function wg_do_post($post, $json = false) {
344
	global $config;
345

    
346
	init_config_arr(array('wireguard', 'tunnel'));
347

    
348
	$input_errors = array();
349

    
350
	$index = $post['index'];
351

    
352
	$pconfig = &$config['wireguard']['tunnel'][$index];
353

    
354
	if (empty($pconfig['name'])) {
355
		$pconfig['name'] = next_wg_if();
356
	}
357
	$pconfig['enabled'] = empty($post['enabled']) ? 'no':'yes';
358
	$pconfig['descr'] = $post['descr'];
359

    
360
	// Interface section
361
	$pconfig['interface']['address'] = $post['address'];
362
	$pconfig['interface']['listenport'] = $post['listenport'];
363
	$pconfig['interface']['privatekey'] = $post['privatekey'];
364
	$pconfig['interface']['publickey'] = genPubKey($post['privatekey']);
365

    
366
	if (isset($post['endpoint0'])) {
367
		// Peers section
368
		$pconfig['peers'] = array();
369
		$pconfig['peers']['wgpeer'] = array();
370

    
371
		$idx = 0;
372

    
373
		for (;;) {
374
			if (!$post['publickeyp'.$idx] &&
375
			    !$post['endpoint'.$idx] &&
376
			    !$post['allowedips'.$idx] &&
377
			    !$post['descr'.$idx] &&
378
			    !$post['persistentkeepalive'.$idx]) {
379
				break;
380
			}
381

    
382
			$peer = array();
383
			$peer['publickey'] = $post['publickeyp' . $idx];
384
			$peer['endpoint'] = $post['endpoint' . $idx];
385
			$peer['allowedips'] = $post['allowedips' . $idx];
386
			$peer['descr'] = $post['descp' . $idx];
387
			$peer['persistentkeepalive'] =
388
			    $post['persistentkeepalive' . $idx];
389
			$peer['presharedkey'] = $post['presharedkey' . $idx];
390
			$peer['port'] = $post['port' . $idx];
391
			$peer['peerwgaddr'] = $post['peerwgaddr' . $idx];
392

    
393
			$pconfig['peers']['wgpeer'][] = $peer;
394
			$idx++;
395
		}
396
	} else {
397
		unset($pconfig['peers']);
398
	}
399

    
400
	$input_errors = wg_validate_post($pconfig);
401

    
402
	if (!$input_errors) {
403
		$config['wireguard']['tunnel'][$index] = $pconfig;
404
		write_config("WireGuard tunnel {$index} updated.");
405
	}
406

    
407
	return(array('input_errors' => $input_errors, 'pconfig' => $pconfig));
408
}
409

    
410
/*
411
 * Read the WireGuard configurations from config.xml and create a number of
412
 * *.conf files for wg-quick to read
413
 */
414
function wg_create_config_files($clean = true) {
415
	global $config, $g;
416

    
417
	if ($config["wireguard"]["tunnel"]) {
418
		$wg_tunnels = $config["wireguard"]["tunnel"];
419
		$cfgpath = $g["wg_conf_path"];
420

    
421
		if (!file_exists($cfgpath)) {
422
			mkdir($cfgpath, 0700, true);
423
		} else {
424
			chmod($cfgpath, 0700);
425
		}
426

    
427
		if ($clean) {
428
			delete_wg_configs();
429
		}
430

    
431
		foreach ($wg_tunnels as $tunnel) {
432
			if (!empty($tunnel['enabled']) &&
433
			    $tunnel['enabled'] == 'yes') {
434
				make_wg_conf($tunnel);
435
			}
436
		}
437
	}
438
}
439

    
440
// Write each file
441
function make_wg_conf($tunnel) {
442
	global $g;
443

    
444
	$txt = "# This WireGuard config file has been created automatically. " .
445
	    "Do not edit!\n";
446

    
447
	$txt .= "# Description: {$tunnel['descr']}\n\n";
448

    
449
	// Process Interfaces section
450
	$txt .= "[Interface]\n";
451

    
452
	if (!empty($tunnel["interface"]["privatekey"])) {
453
		$txt .= "PrivateKey = {$tunnel["interface"]["privatekey"]}\n";
454
	}
455

    
456
	if (!empty($tunnel["interface"]["listenport"])) {
457
		$txt .= "ListenPort = {$tunnel["interface"]["listenport"]}\n";
458
	}
459

    
460
	$txt .= "\n";
461

    
462
	// Process peers section
463
	if (is_array($tunnel['peers']) && is_array($tunnel['peers']['wgpeer']) &&
464
	    count($tunnel['peers']['wgpeer']) > 0) {
465
		foreach ($tunnel['peers']['wgpeer'] as $peer) {
466
			$txt .= "# Peer: {$peer['descr']}\n";
467
			$txt .= "[Peer]\n";
468

    
469
			if (!empty($peer["publickey"])) {
470
				$txt .= "PublicKey = {$peer['publickey']}\n";
471
			}
472

    
473
			if (!empty($peer["endpoint"])) {
474
				$txt .= "EndPoint = ";
475
				$txt .= is_ipaddrv6($peer["endpoint"]) ? "[{$peer['endpoint']}]" : $peer['endpoint'];
476
				$txt .= ":" . ((empty($peer["port"])) ? '51820' : $peer["port"]) . "\n";
477
			}
478

    
479
			if (!empty($peer["allowedips"])) {
480
				$txt .= "AllowedIPs = {$peer['allowedips']}\n";
481
			}
482

    
483
			if (!empty($peer["persistentkeepalive"])) {
484
				$txt .= "PersistentKeepalive = " .
485
				    "{$peer['persistentkeepalive']}\n";
486
			}
487

    
488
			if (!empty($peer["presharedkey"])) {
489
				$txt .= "PresharedKey = " .
490
				    "{$peer['presharedkey']}\n";
491
			}
492

    
493
			$txt .= "\n";
494
		}
495
	}
496

    
497
	file_put_contents($g["wg_conf_path"] . "/" . $tunnel['name'] . ".conf",
498
	    $txt);
499
	chmod($g["wg_conf_path"] . "/" . $tunnel['name'] . ".conf", 0600);
500
}
501

    
502
// Remove all wg config files from the conf directory
503
function delete_wg_configs() {
504
	global $g;
505

    
506
	unlink_if_exists($g["wg_conf_path"] . "/*.conf");
507
}
508

    
509
/* Check if at least one tunnel is enabled */
510
function is_wg_enabled() {
511
	global $config;
512

    
513
	init_config_arr(array('wireguard', 'tunnel'));
514

    
515
	foreach ($config['wireguard']['tunnel'] as $tunnel) {
516
		if (empty($tunnel['enabled'])) {
517
			continue;
518
		}
519
		return true;
520
	}
521

    
522
	return false;
523
}
524

    
525
/* Return WireGuard tunnel networks for a given address family */
526
function wg_get_tunnel_networks($family = 'both') {
527
	global $config;
528
	$wg_tunnel_networks = array();
529
	init_config_arr(array('wireguard', 'tunnel'));
530
	if (is_wg_enabled()) {
531
		foreach ($config['wireguard']['tunnel'] as $wg) {
532
			if (empty($wg['enabled']) ||
533
			    empty($wg['interface']['address'])) {
534
				continue;
535
			}
536
			foreach(explode(',', $wg['interface']['address']) as $wga) {
537
				list($wgnet, $wgmask) = explode('/', trim($wga));
538
				if ((is_ipaddrv6($wgnet) && ($family == 'ipv4')) ||
539
				    (is_ipaddrv4($wgnet) && ($family == 'ipv6'))) {
540
					continue;
541
				}
542
				$network = gen_subnet($wgnet, $wgmask);
543
				$wg_tunnel_networks[] = "{$network}/{$wgmask}";
544
			}
545
		}
546
	}
547
	return $wg_tunnel_networks;
548
}
549

    
550
/* Locate a viable remote gateway address for a WireGuard tunnel
551
 * Fall back to using the tunnel address itself.
552
 * https://redmine.pfsense.org/issues/11300 */
553
function wg_find_tunnel_gw($tunnel, $addr) {
554
	list($ip, $mask) = explode("/", trim($addr));
555
	/* Loop through peers looking for a viable remote gateway address */
556
	if (is_array($tunnel['peers']) &&
557
	    is_array($tunnel['peers']['wgpeer']) &&
558
	    count($tunnel['peers']['wgpeer']) > 0) {
559
		foreach ($tunnel['peers']['wgpeer'] as $peer) {
560
			/* If this peer has no configured Peer WireGuard Address, skip it. */
561
			if (empty($peer['peerwgaddr'])) {
562
				continue;
563
			}
564
			/* Check each Peer WireGuard Address entry */
565
			foreach (explode(',', $peer['peerwgaddr']) as $pwga) {
566
				/* Ensure the address family of this entry matches the one we're seeking */
567
				if (is_v4($ip) !== is_v4($pwga)) {
568
					continue;
569
				}
570
				/* If there is a subnet mask, ditch it. */
571
				list($pip, $pmask) = explode('/', trim($pwga));
572
				/* Check that this address is in the desired subnet */
573
				if (ip_in_subnet($pip, trim($addr))) {
574
					/* We found a good candidate, return it */
575
					return $pip;
576
				}
577
			}
578
		}
579
	}
580
	/* If no viable candidate is found, return the tunnel address */
581
	return $ip;
582
}
(12-12/12)