%PDF- %PDF-
Direktori : /backups/router/usr/local/opnsense/scripts/Wireguard/ |
Current File : //backups/router/usr/local/opnsense/scripts/Wireguard/wg-service-control.php |
#!/usr/local/bin/php <?php /* * Copyright (C) 2023 Deciso B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ require_once('script/load_phalcon.php'); require_once('util.inc'); require_once('config.inc'); require_once('interfaces.inc'); require_once('system.inc'); /** * collect carp status per vhid */ function get_vhid_status() { $vhids = []; foreach ((new OPNsense\Interfaces\Vip())->vip->iterateItems() as $id => $item) { if ($item->mode == 'carp') { $vhids[$id] = ['status' => 'DISABLED', 'vhid' => (string)$item->vhid]; } } foreach (legacy_interfaces_details() as $ifdata) { if (!empty($ifdata['carp'])) { foreach ($ifdata['carp'] as $data) { foreach ($vhids as $id => &$item) { if ($item['vhid'] == $data['vhid']) { $item['status'] = $data['status']; } } } } } return $vhids; } /** * mimic wg-quick behaviour, but bound to our config */ function wg_start($server, $fhandle, $ifcfgflag = 'up', $reload = false) { if (!does_interface_exist($server->interface)) { mwexecf('/sbin/ifconfig wg create name %s', [$server->interface]); mwexecf('/sbin/ifconfig %s group wireguard', [$server->interface]); $reload = true; } mwexecf('/usr/bin/wg syncconf %s %s', [$server->interface, $server->cnfFilename]); /* The tunneladdress can be empty, so array_filter without callback filters empty strings out. */ foreach (array_filter(explode(',', (string)$server->tunneladdress)) as $alias) { $proto = strpos($alias, ':') === false ? "inet" : "inet6"; mwexecf('/sbin/ifconfig %s %s %s alias', [$server->interface, $proto, $alias]); } if (!empty((string)$server->mtu)) { mwexecf('/sbin/ifconfig %s mtu %s', [$server->interface, $server->mtu]); } if (empty((string)$server->disableroutes)) { /** * Add routes for all configured peers, wg-quick seems to parse 'wg show wgX allowed-ips' for this, * but this should logically congtain the same networks. * * XXX: For some reason these routes look a bit off, not very well integrated into OPNsense. * In the long run it might make sense to have some sort of pluggable model facility * where these (and maybe other) static routes hook into. **/ $peers = explode(',', $server->peers); $routes_to_add = $routes_to_skip = ['inet' => [], 'inet6' => []]; /* calculate subnets to skip because these are automatically attached by instance address */ foreach (array_filter(explode(',', (string)$server->tunneladdress)) as $alias) { $ipproto = strpos($alias, ':') === false ? 'inet' : 'inet6'; $alias = explode('/', $alias); $alias = ($ipproto == 'inet' ? gen_subnet($alias[0], $alias[1]) : gen_subnetv6($alias[0], $alias[1])) . "/{$alias[1]}"; $routes_to_skip[$ipproto][] = $alias; } foreach ((new OPNsense\Wireguard\Client())->clients->client->iterateItems() as $key => $client) { if (empty((string)$client->enabled) || !in_array($key, $peers)) { continue; } foreach (explode(',', (string)$client->tunneladdress) as $address) { $ipproto = strpos($address, ":") === false ? "inet" : "inet6"; $address = explode('/', $address); $address = ($ipproto == 'inet' ? gen_subnet($address[0], $address[1]) : gen_subnetv6($address[0], $address[1])) . "/{$address[1]}"; /* wg-quick seems to prevent /0 being routed and translates this automatically */ if (str_ends_with(trim($address), '/0')) { if ($ipproto == 'inet') { array_push($routes_to_add[$ipproto], '0.0.0.0/1', '128.0.0.0/1'); } else { array_push($routes_to_add[$ipproto], '::/1', '8000::/1'); } } elseif (!in_array($address, $routes_to_skip[$ipproto])) { $routes_to_add[$ipproto][] = $address; } } } foreach ($routes_to_add as $ipproto => $routes) { foreach (array_unique($routes) as $route) { mwexecf('/sbin/route -q -n add -%s %s -interface %s', [$ipproto, $route, $server->interface]); } } } elseif (!empty((string)$server->gateway)) { /* Only bind the gateway ip to the tunnel */ $ipprefix = strpos($server->gateway, ":") === false ? "-4" : "-6"; mwexecf('/sbin/route -q -n add %s %s -iface %s', [$ipprefix, $server->gateway, $server->interface]); } if ($reload) { interfaces_restart_by_device(false, [(string)$server->interface]); } mwexecf('/sbin/ifconfig %s %s', [$server->interface, $ifcfgflag]); // flush checksum to ease change detection fseek($fhandle, 0); ftruncate($fhandle, 0); fwrite($fhandle, @md5_file($server->cnfFilename) . "|" . wg_reconfigure_hash($server)); syslog(LOG_NOTICE, "wireguard instance {$server->name} ({$server->interface}) started"); } /** * stop wireguard tunnel, kill the device, the routes should drop automatically. */ function wg_stop($server) { if (does_interface_exist($server->interface)) { legacy_interface_destroy($server->interface); } syslog(LOG_NOTICE, "wireguard instance {$server->name} ({$server->interface}) stopped"); } /** * Calculate a hash which determines if we are able to reconfigure without a restart of the tunnel. * We currently assume if something changed on the interface or peer routes are being pushed, it's safer to * restart then reload. */ function wg_reconfigure_hash($server) { if (empty((string)$server->disableroutes)) { return md5(uniqid('', true)); // random hash, should always reconfigure } return md5( sprintf( '%s|%s|%s', $server->tunneladdress, $server->mtu, $server->gateway ) ); } /** * The stat hash file answers two questions, [1] has anything changed, which is answered using an md5 hash of the * configuration file. The second question, if something has changed, is it safe to only reload the configuration. * This is answered by wg_reconfigure_hash() for the instance in question. */ function get_stat_hash($fhandle) { fseek($fhandle, 0); $payload = stream_get_contents($fhandle) ?? ''; $parts = explode('|', $payload); return [ 'file' => $parts[0] ?? '', 'interface' => $parts[1] ?? '' ]; } $opts = getopt('ah', [], $optind); $args = array_slice($argv, $optind); /* setup syslog logging */ openlog("wireguard", LOG_ODELAY, LOG_AUTH); if (isset($opts['h']) || empty($args) || !in_array($args[0], ['start', 'stop', 'restart', 'configure'])) { echo "Usage: wg-service-control.php [-a] [-h] [stop|start|restart|configure] [uuid|vhid]\n\n"; echo "\t-a all instances\n"; } elseif (isset($opts['a']) || !empty($args[1])) { // either a server id (uuid) or a vhid could be offered $server_id = $vhid = null; if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $args[1] ?? '') == 1) { $server_id = $args[1]; } elseif (!empty($args[1])) { $vhid = explode('@', $args[1])[0]; } $action = $args[0]; $server_devs = []; if (!empty((string)(new OPNsense\Wireguard\General())->enabled)) { $vhids = get_vhid_status(); foreach ((new OPNsense\Wireguard\Server())->servers->server->iterateItems() as $key => $node) { $carp_depend_on = (string)$node->carp_depend_on; if (empty((string)$node->enabled)) { continue; } elseif ($server_id != null && $key != $server_id) { continue; } elseif ($vhid != null && (!empty($vhids[$carp_depend_on]) && $vhids[$carp_depend_on]['vhid'] != $vhid)) { continue; } /** * CARP may influence the interface status (up or down). * In order to fluently switch between roles, one should only have to change the interface flag in this * case, which means we can still reconfigure an interface in the usual way and just omit sending traffic * when in BACKUP or INIT mode. */ $carp_if_flag = 'up'; if (!empty($vhids[$carp_depend_on]) && $vhids[$carp_depend_on]['status'] != 'MASTER') { $carp_if_flag = 'down'; } $server_devs[] = (string)$node->interface; $statHandle = fopen($node->statFilename, 'a+e'); if (flock($statHandle, LOCK_EX)) { $ifdetails = legacy_interfaces_details((string)$node->interface); switch ($action) { case 'stop': wg_stop($node); break; case 'start': wg_start($node, $statHandle, $carp_if_flag); break; case 'restart': wg_stop($node); wg_start($node, $statHandle, $carp_if_flag); break; case 'configure': $ifstatus = '-'; if (!empty($ifdetails[(string)$node->interface])) { $ifstatus = in_array('up', $ifdetails[(string)$node->interface]['flags']) ? 'up' : 'down'; } if (!empty($carp_depend_on) && !empty($vhid)) { // CARP event traceability when a vhid is being passed syslog( LOG_NOTICE, sprintf( "Wireguard configure event instance %s (%s) vhid: %s carp: %s interface: %s", $node->name, $node->interface, $vhid, !empty($vhids[$carp_depend_on]) ? $vhids[$carp_depend_on]['status'] : '-', $ifstatus ) ); } if ( @md5_file($node->cnfFilename) != get_stat_hash($statHandle)['file'] || empty($ifdetails[(string)$node->interface]) ) { $reload = false; if (get_stat_hash($statHandle)['interface'] != wg_reconfigure_hash($node)) { // Fluent reloading not supported for this instance, make sure the user is informed syslog( LOG_NOTICE, "wireguard instance {$node->name} ({$node->interface}) " . "can not reconfigure without stopping it first." ); /* * Scrub interface, although dropping and recreating is more clean, there are * side affects in doing so. Dropping the addresses should drop the associated * routes and force a full reload (also of attached interface). */ interfaces_addresses_flush((string)$node->interface, 4, $ifdetails); interfaces_addresses_flush((string)$node->interface, 6, $ifdetails); $reload = true; } wg_start($node, $statHandle, $carp_if_flag, $reload); /* when triggered via a CARP event, check our interface status [UP|DOWN] */ } elseif ($ifstatus != $carp_if_flag) { syslog( LOG_NOTICE, "wireguard instance {$node->name} ({$node->interface}) " . "switching to " . strtoupper($carp_if_flag) ); mwexecf('/sbin/ifconfig %s %s', [$node->interface, $carp_if_flag]); } break; } flock($statHandle, LOCK_UN); } fclose($statHandle); } } /** * When -a is specified, cleanup up old or disabled instances (files and interfaces) */ if ($server_id == null && $vhid == null) { foreach (glob('/usr/local/etc/wireguard/wg*') as $filename) { $this_dev = explode('.', basename($filename))[0]; if (!in_array($this_dev, $server_devs)) { @unlink($filename); if (does_interface_exist($this_dev)) { legacy_interface_destroy($this_dev); } } } } if (count($server_devs) && $action == 'restart') { /* XXX required for filter/NAT rules, as interface was recreated, rules might not match anymore */ configd_run('filter reload'); } }