net/wireguard - offer CARP vhid tracking support, closes https://github.com/opnsense/plugins/issues/3579

When the the selected vhid is in BACKUP or INIT mode, the wireguard interface in question will be set to "down", in which case communication stops and the new master may take over. The advantage of this strategy is that switching is relatively quick as only the interface flag need to be changed.
This commit is contained in:
Ad Schellevis 2023-09-26 20:55:17 +02:00
parent 0ce2b67e52
commit 9787d50806
8 changed files with 81 additions and 7 deletions

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= wireguard
PLUGIN_VERSION= 2.1
PLUGIN_VERSION= 2.2
PLUGIN_COMMENT= WireGuard VPN service kernel implementation
PLUGIN_DEPENDS= wireguard-kmod
PLUGIN_CONFLICTS= wireguard-go

View file

@ -16,6 +16,10 @@ WWW: https://www.wireguard.com/
Changelog
---------
2.2
* add vhid (carp) tracking support
2.1
* Only reload when interface configuration did not change

View file

@ -0,0 +1,3 @@
#!/bin/sh
configctl -dq wireguard configure

View file

@ -59,6 +59,13 @@
<allownew>true</allownew>
<help>List of addresses to configure on the tunnel adapter. Please use CIDR notation like 10.0.0.1/24.</help>
</field>
<field>
<id>server.carp_depend_on</id>
<label>Depend on (carp)</label>
<type>dropdown</type>
<help>The carp VHID to depend on, when this virtual address is not in master state,
the instance will be shutdown.</help>
</field>
<field>
<id>server.peers</id>
<label>Peers</label>

View file

@ -59,6 +59,11 @@
<gateway type="NetworkField">
<Required>N</Required>
</gateway>
<carp_depend_on type="VirtualIPField">
<type>carp</type>
<Required>N</Required>
<key>mvc</key>
</carp_depend_on>
<peers type="ModelRelationField">
<Model>
<template>

View file

@ -82,6 +82,7 @@
<tr>
<th data-column-id="if" data-type="string" data-width="8em">{{ lang._('Interface') }}</th>
<th data-column-id="type" data-type="string" data-width="8em" data-visible="false">{{ lang._('Type') }}</th>
<th data-column-id="status" data-type="string" data-width="8em" >{{ lang._('Status') }}</th>
<th data-column-id="public-key" data-type="string" data-identifier="true">{{ lang._('Public key') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="endpoint" data-type="string">{{ lang._('Port / Endpoint') }}</th>

View file

@ -31,10 +31,35 @@ require_once('script/load_phalcon.php');
require_once('util.inc');
require_once('interfaces.inc');
/**
* collect carp status per vhid
*/
function get_vhid_status()
{
$vhids = [];
$uuids = [];
foreach ((new OPNsense\Interfaces\Vip())->vip->iterateItems() as $id => $item) {
if ($item->mode == 'carp') {
$uuids[(string)$item->vhid] = $id;
}
}
foreach (legacy_interfaces_details() as $ifdata) {
if (!empty($ifdata['carp'])) {
foreach ($ifdata['carp'] as $data) {
if (isset($uuids[$data['vhid']])) {
$vhids[$uuids[$data['vhid']]] = $data['status'];
}
}
}
}
return $vhids;
}
/**
* mimic wg-quick behaviour, but bound to our config
*/
function wg_start($server, $fhandle)
function wg_start($server, $fhandle, $ifcfgflag='up')
{
if (!does_interface_exist($server->interface)) {
mwexecf('/sbin/ifconfig wg create name %s', [$server->interface]);
@ -49,7 +74,7 @@ function wg_start($server, $fhandle)
if (!empty((string)$server->mtu)) {
mwexecf('/sbin/ifconfig %s mtu %s', [$server->interface, $server->mtu]);
}
mwexecf('/sbin/ifconfig %s up', [$server->interface]);
mwexecf('/sbin/ifconfig %s %s', [$server->interface, $ifcfgflag]);
if (empty((string)$server->disableroutes)) {
/**
@ -161,6 +186,8 @@ if (isset($opts['h']) || empty($args) || !in_array($args[0], ['start', 'stop', '
$server_devs = [];
if (!empty((string)(new OPNsense\Wireguard\General())->enabled)) {
$ifdetails = legacy_interfaces_details();
$vhids = get_vhid_status();
foreach ((new OPNsense\Wireguard\Server())->servers->server->iterateItems() as $key => $node) {
if (empty((string)$node->enabled)) {
continue;
@ -168,6 +195,19 @@ if (isset($opts['h']) || empty($args) || !in_array($args[0], ['start', 'stop', '
if ($server_id != null && $key != $server_id) {
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[(string)$node->carp_depend_on]) &&
$vhids[(string)$node->carp_depend_on] != 'MASTER'
) {
$carp_if_flag = 'down';
}
$server_devs[] = (string)$node->interface;
$statHandle = fopen($node->statFilename, "a+");
if (flock($statHandle, LOCK_EX)) {
@ -176,16 +216,16 @@ if (isset($opts['h']) || empty($args) || !in_array($args[0], ['start', 'stop', '
wg_stop($node);
break;
case 'start':
wg_start($node, $statHandle);
wg_start($node, $statHandle, $carp_if_flag);
break;
case 'restart':
wg_stop($node);
wg_start($node, $statHandle);
wg_start($node, $statHandle, $carp_if_flag);
break;
case 'configure':
if (
@md5_file($node->cnfFilename) != get_stat_hash($statHandle)['file'] ||
!does_interface_exist((string)$node->interface)
!isset($ifdetails[(string)$node->interface])
) {
if (get_stat_hash($statHandle)['interface'] != wg_reconfigure_hash($node)) {
// Fluent reloading not supported for this instance, make sure the user is informed
@ -196,7 +236,13 @@ if (isset($opts['h']) || empty($args) || !in_array($args[0], ['start', 'stop', '
);
wg_stop($node);
}
wg_start($node, $statHandle);
wg_start($node, $statHandle, $carp_if_flag);
} else {
// when triggered via a CARP event, check our interface status [UP|DOWN]
$tmp = in_array('up', $ifdetails[(string)$node->interface]['flags']) ? 'up' : 'down';
if ($tmp != $carp_if_flag) {
mwexecf('/sbin/ifconfig %s %s', [$node->interface, $carp_if_flag]);
}
}
break;
}

View file

@ -28,6 +28,13 @@
import subprocess
import ujson
interfaces = {}
for line in subprocess.run(['/sbin/ifconfig'], capture_output=True, text=True).stdout.split("\n"):
if not line.startswith('\t') and line.find('<') > -1:
ifname = line.split(':')[0]
interfaces[ifname] = 'up' if 'UP' in line.split('<')[1].split('>')[0].split(',') else 'down'
sp = subprocess.run(['/usr/bin/wg', 'show', 'all', 'dump'], capture_output=True, text=True)
result = {'records': []}
if sp.returncode == 0:
@ -44,6 +51,7 @@ if sp.returncode == 0:
record['fwmark'] = parts[4]
# convenience, copy listen-port to endpoint
record['endpoint'] = parts[3]
record['status'] = interfaces.get(record['if'], 'down')
elif len(parts) == 9:
record['type'] = 'peer'
record['public-key'] = parts[1]