Preserve independent NordVPN enrollment selectors

This commit is contained in:
ms2k 2026-08-06 21:45:54 +09:00
commit 2890153002
8 changed files with 182 additions and 90 deletions

View file

@ -51,9 +51,16 @@ function nordvpn_firewall(Plugin $firewall)
return; return;
} }
$interfaces = nordvpn_values((string)$settings->general->interfaces); $enrollments = [];
$source = (string)$settings->general->source ?: 'any'; $interfaces = [];
$destination = (string)$settings->general->destination ?: 'any'; foreach ($settings->enrollments->enrollment->iterateItems() as $enrollment) {
if ((string)$enrollment->enabled !== '1') {
continue;
}
$enrollments[] = $enrollment;
$interfaces = array_merge($interfaces, nordvpn_values((string)$enrollment->interfaces));
}
$interfaces = array_values(array_unique($interfaces));
$directGateway = (string)$settings->general->direct_gateway; $directGateway = (string)$settings->general->direct_gateway;
$status = (new StatusStore())->read(); $status = (new StatusStore())->read();
$available = !empty($status['available']); $available = !empty($status['available']);
@ -85,13 +92,16 @@ function nordvpn_firewall(Plugin $firewall)
$firewall->registerFilterRule(190000, $rule); $firewall->registerFilterRule(190000, $rule);
} }
foreach ($interfaces as $interface) { foreach ($enrollments as $enrollment) {
$source = (string)$enrollment->source ?: 'any';
$destination = (string)$enrollment->destination ?: 'any';
foreach (nordvpn_values((string)$enrollment->interfaces) as $interface) {
$rule = nordvpn_filter_rule( $rule = nordvpn_filter_rule(
$interface, $interface,
$source, $source,
$destination, $destination,
(string)$settings->general->source_not === '1', (string)$enrollment->source_not === '1',
(string)$settings->general->destination_not === '1' (string)$enrollment->destination_not === '1'
); );
$rule['descr'] = 'NordVPN egress enrollment'; $rule['descr'] = 'NordVPN egress enrollment';
if ($available) { if ($available) {
@ -106,17 +116,17 @@ function nordvpn_firewall(Plugin $firewall)
} }
$firewall->registerFilterRule(190100, $rule); $firewall->registerFilterRule(190100, $rule);
} }
$firewall->registerSNatRule(40, [ $firewall->registerSNatRule(40, [
'interface' => ManagedConfiguration::INTERFACE, 'interface' => ManagedConfiguration::INTERFACE,
'ipprotocol' => 'inet', 'ipprotocol' => 'inet',
'from' => $source, 'from' => $source,
'from_not' => (string)$settings->general->source_not === '1', 'from_not' => (string)$enrollment->source_not === '1',
'to' => $destination, 'to' => $destination,
'to_not' => (string)$settings->general->destination_not === '1', 'to_not' => (string)$enrollment->destination_not === '1',
'descr' => 'NordVPN managed outbound NAT', 'descr' => 'NordVPN managed outbound NAT',
]); ]);
} }
}
function nordvpn_filter_rule(string $interface, string $source, string $destination, bool $sourceNot, bool $destinationNot) function nordvpn_filter_rule(string $interface, string $source, string $destination, bool $sourceNot, bool $destinationNot)
{ {

View file

@ -68,10 +68,17 @@ class ServiceController extends ApiControllerBase
public function statusAction(): array public function statusAction(): array
{ {
$settings = new Settings(); $settings = new Settings();
$warning = (new EnrollmentInspector())->includesPrivateDestinations( $inspector = new EnrollmentInspector();
(string)$settings->general->destination, $warning = false;
(string)$settings->general->destination_not === '1' foreach ($settings->enrollments->enrollment->iterateItems() as $enrollment) {
); if ((string)$enrollment->enabled === '1' && $inspector->includesPrivateDestinations(
(string)$enrollment->destination,
(string)$enrollment->destination_not === '1'
)) {
$warning = true;
break;
}
}
return ['status' => 'ok', 'tunnel' => (new StatusStore())->read(), 'rfc1918_warning' => $warning]; return ['status' => 'ok', 'tunnel' => (new StatusStore())->read(), 'rfc1918_warning' => $warning];
} }

View file

@ -13,6 +13,36 @@ class SettingsController extends ApiMutableModelControllerBase
protected static $internalModelName = 'settings'; protected static $internalModelName = 'settings';
protected static $internalModelClass = 'OPNsense\NordVPN\Settings'; protected static $internalModelClass = 'OPNsense\NordVPN\Settings';
public function searchEnrollmentAction()
{
return $this->searchBase('enrollments.enrollment');
}
public function getEnrollmentAction($uuid = null)
{
return $this->getBase('enrollment', 'enrollments.enrollment', $uuid);
}
public function addEnrollmentAction()
{
return $this->addBase('enrollment', 'enrollments.enrollment');
}
public function setEnrollmentAction($uuid)
{
return $this->setBase('enrollment', 'enrollments.enrollment', $uuid);
}
public function delEnrollmentAction($uuid)
{
return $this->delBase('enrollments.enrollment', $uuid);
}
public function toggleEnrollmentAction($uuid, $enabled = null)
{
return $this->toggleBase('enrollments.enrollment', $uuid, $enabled);
}
public function reconfigureAction() public function reconfigureAction()
{ {
if (!$this->request->isPost()) { if (!$this->request->isPost()) {

View file

@ -10,6 +10,8 @@ class IndexController extends \OPNsense\Base\IndexController
public function indexAction() public function indexAction()
{ {
$this->view->settingsForm = $this->getForm('settings'); $this->view->settingsForm = $this->getForm('settings');
$this->view->enrollmentForm = $this->getForm('dialogEditEnrollment');
$this->view->enrollmentGrid = $this->getFormGrid('dialogEditEnrollment');
$this->view->pick('OPNsense/NordVPN/index'); $this->view->pick('OPNsense/NordVPN/index');
} }
} }

View file

@ -0,0 +1,48 @@
<form>
<field>
<id>enrollment.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<grid_view>
<width>100</width>
<type>boolean</type>
<formatter>rowtoggle</formatter>
</grid_view>
</field>
<field>
<id>enrollment.interfaces</id>
<label>Interfaces</label>
<type>select_multiple</type>
<grid_view>
<width>200</width>
</grid_view>
</field>
<field>
<id>enrollment.source</id>
<label>Source</label>
<type>select_multiple</type>
<style>tokenize</style>
<grid_view>
<width>200</width>
</grid_view>
</field>
<field>
<id>enrollment.source_not</id>
<label>Invert source</label>
<type>checkbox</type>
</field>
<field>
<id>enrollment.destination</id>
<label>Destination</label>
<type>select_multiple</type>
<style>tokenize</style>
<grid_view>
<width>200</width>
</grid_view>
</field>
<field>
<id>enrollment.destination_not</id>
<label>Invert destination</label>
<type>checkbox</type>
</field>
</form>

View file

@ -10,37 +10,6 @@
<style>tokenize</style> <style>tokenize</style>
<help>Leave empty to permit every country returned by NordVPN recommendations.</help> <help>Leave empty to permit every country returned by NordVPN recommendations.</help>
</field> </field>
<field>
<type>header</type>
<label>NordVPN egress enrollment</label>
</field>
<field>
<id>settings.general.interfaces</id>
<label>Interfaces</label>
<type>select_multiple</type>
</field>
<field>
<id>settings.general.source</id>
<label>Source</label>
<type>select_multiple</type>
<style>tokenize</style>
</field>
<field>
<id>settings.general.source_not</id>
<label>Invert source</label>
<type>checkbox</type>
</field>
<field>
<id>settings.general.destination</id>
<label>Destination</label>
<type>select_multiple</type>
<style>tokenize</style>
</field>
<field>
<id>settings.general.destination_not</id>
<label>Invert destination</label>
<type>checkbox</type>
</field>
<field> <field>
<type>header</type> <type>header</type>
<label>Failure and direct egress</label> <label>Failure and direct egress</label>

View file

@ -5,7 +5,31 @@
<items> <items>
<general> <general>
<countries type="CSVListField"/> <countries type="CSVListField"/>
<direct_gateway type="JsonKeyValueStoreField">
<ConfigdPopulateAct>interface gateways list -g</ConfigdPopulateAct>
<ValidationMessage>Specify a valid IPv4 gateway.</ValidationMessage>
</direct_gateway>
<failure_policy type="OptionField">
<Default>fail_closed</Default>
<Required>Y</Required>
<OptionValues>
<fail_closed>Fail closed</fail_closed>
<fail_open>Fail open</fail_open>
</OptionValues>
</failure_policy>
<exempt_wireguard type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</exempt_wireguard>
</general>
<enrollments>
<enrollment type="ArrayField">
<enabled type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</enabled>
<interfaces type="InterfaceField"> <interfaces type="InterfaceField">
<Required>Y</Required>
<Multiple>Y</Multiple> <Multiple>Y</Multiple>
<Filters> <Filters>
<if>/^(?!lo0$).*/</if> <if>/^(?!lo0$).*/</if>
@ -29,23 +53,8 @@
<Default>0</Default> <Default>0</Default>
<Required>Y</Required> <Required>Y</Required>
</destination_not> </destination_not>
<direct_gateway type="JsonKeyValueStoreField"> </enrollment>
<ConfigdPopulateAct>interface gateways list -g</ConfigdPopulateAct> </enrollments>
<ValidationMessage>Specify a valid IPv4 gateway.</ValidationMessage>
</direct_gateway>
<failure_policy type="OptionField">
<Default>fail_closed</Default>
<Required>Y</Required>
<OptionValues>
<fail_closed>Fail closed</fail_closed>
<fail_open>Fail open</fail_open>
</OptionValues>
</failure_policy>
<exempt_wireguard type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</exempt_wireguard>
</general>
<managed> <managed>
<instance_uuid type="TextField"/> <instance_uuid type="TextField"/>
<peer_uuid type="TextField"/> <peer_uuid type="TextField"/>

View file

@ -1,5 +1,15 @@
<script> <script>
$(document).ready(function() { $(document).ready(function() {
$('#{{ enrollmentGrid['table_id'] }}').UIBootgrid({
search: '/api/nordvpn/settings/searchEnrollment/',
get: '/api/nordvpn/settings/getEnrollment/',
set: '/api/nordvpn/settings/setEnrollment/',
add: '/api/nordvpn/settings/addEnrollment/',
del: '/api/nordvpn/settings/delEnrollment/',
toggle: '/api/nordvpn/settings/toggleEnrollment/',
options: {selection: false, multiSelect: false}
});
function loadSettings() { function loadSettings() {
ajaxCall('/api/nordvpn/service/countries', {}, function(data) { ajaxCall('/api/nordvpn/service/countries', {}, function(data) {
if (data.status === 'ok') { if (data.status === 'ok') {
@ -76,6 +86,11 @@
{{ lang._('NordVPN egress enrollment can include an RFC 1918 destination address.') }} {{ lang._('NordVPN egress enrollment can include an RFC 1918 destination address.') }}
</div> </div>
<div class="content-box">
<h2>{{ lang._('NordVPN egress enrollment') }}</h2>
{{ partial('layout_partials/base_bootgrid_table', enrollmentGrid) }}
</div>
<div class="content-box"> <div class="content-box">
{{ partial('layout_partials/base_form', ['fields': settingsForm, 'id': 'frm_NordVPNSettings']) }} {{ partial('layout_partials/base_form', ['fields': settingsForm, 'id': 'frm_NordVPNSettings']) }}
<hr/> <hr/>
@ -84,6 +99,8 @@
</button> </button>
</div> </div>
{{ partial('layout_partials/base_dialog', ['fields': enrollmentForm, 'id': enrollmentGrid['edit_dialog_id'], 'label': lang._('Edit enrollment')]) }}
<div class="content-box"> <div class="content-box">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">