Skip to content

Commit aba8beb

Browse files
shwetd19pandafynemesifier
authored
[fix] Fix VPN-client template switch bug with same VPN server #973
Closes #973. --------- Co-authored-by: Gagan Deep <[email protected]> Co-authored-by: Federico Capoano <[email protected]>
1 parent c39c916 commit aba8beb

File tree

2 files changed

+127
-22
lines changed

2 files changed

+127
-22
lines changed

openwisp_controller/config/base/config.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs):
256256

257257
if action == 'post_clear':
258258
if instance.is_deactivating_or_deactivated():
259-
# If the device is deactivated or in the process of deactivatiing, then
259+
# If the device is deactivated or in the process of deactivating, then
260260
# delete all vpn clients and return.
261261
instance.vpnclient_set.all().delete()
262262
return
@@ -265,37 +265,35 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs):
265265
# coming from signal
266266
if isinstance(pk_set, set):
267267
template_model = cls.get_template_model()
268-
# Ordering the queryset here doesn't affect the functionality
269-
# since pk_set is a set. Though ordering the queryset is required
270-
# for tests.
271268
templates = template_model.objects.filter(pk__in=list(pk_set)).order_by(
272269
'created'
273270
)
274271
# coming from admin ModelForm
275272
else:
276273
templates = pk_set
277-
# delete VPN clients which have been cleared
278-
# by sortedm2m and have not been added back
279-
if action == 'post_add':
280-
vpn_list = instance.templates.filter(type='vpn').values_list('vpn')
281-
instance.vpnclient_set.exclude(vpn__in=vpn_list).delete()
282-
# when adding or removing specific templates
274+
275+
# Get current VPNs in use by any template
276+
current_vpns = set(
277+
instance.templates.filter(type='vpn').values_list('vpn_id', flat=True)
278+
)
279+
280+
# Handle template actions
283281
for template in templates.filter(type='vpn'):
284282
if action == 'post_add':
285-
if vpn_client_model.objects.filter(
283+
# Create VPN client if needed
284+
if not vpn_client_model.objects.filter(
286285
config=instance, vpn=template.vpn
287286
).exists():
288-
continue
289-
client = vpn_client_model(
290-
config=instance,
291-
vpn=template.vpn,
292-
auto_cert=template.auto_cert,
293-
)
294-
client.full_clean()
295-
client.save()
296-
elif action == 'post_remove':
297-
for client in instance.vpnclient_set.filter(vpn=template.vpn):
298-
client.delete()
287+
client = vpn_client_model(
288+
config=instance,
289+
vpn=template.vpn,
290+
auto_cert=template.auto_cert,
291+
)
292+
client.full_clean()
293+
client.save()
294+
295+
# Clean up any VPN clients that aren't associated with current templates
296+
instance.vpnclient_set.exclude(vpn_id__in=current_vpns).delete()
299297

300298
@classmethod
301299
def clean_templates_org(cls, action, instance, pk_set, raw_data=None, **kwargs):

openwisp_controller/config/tests/test_admin.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,6 +2047,113 @@ def test_device_changelist_activate_deactivate_admin_action_security(
20472047
)
20482048
self.assertEqual(mocked_deactivate.call_count, 1)
20492049

2050+
def test_vpn_template_switch(self):
2051+
"""
2052+
Test switching between two VPN templates that use the same VPN server
2053+
Verifies that:
2054+
1. Only one VpnClient exists at a time
2055+
2. VPN config variables are correctly resolved
2056+
3. Switching back and forth works properly
2057+
"""
2058+
vpn = self._create_vpn()
2059+
template1 = self._create_template(
2060+
name='vpn-test-1',
2061+
type='vpn',
2062+
vpn=vpn,
2063+
config={},
2064+
auto_cert=True,
2065+
)
2066+
template1.config['openvpn'][0]['dev'] = 'tun0'
2067+
template1.full_clean()
2068+
template1.save()
2069+
template2 = self._create_template(
2070+
name='vpn-test-2',
2071+
type='vpn',
2072+
vpn=vpn,
2073+
config={},
2074+
auto_cert=True,
2075+
)
2076+
template2.config['openvpn'][0]['dev'] = 'tun1'
2077+
template2.full_clean()
2078+
template2.save()
2079+
2080+
# Add device with default template (template1)
2081+
path = reverse(f'admin:{self.app_label}_device_add')
2082+
params = self._get_device_params(org=self._get_org())
2083+
response = self.client.post(path, data=params, follow=True)
2084+
self.assertEqual(response.status_code, 200)
2085+
config = Config.objects.get(device__name=params['name'])
2086+
2087+
# Add template1 to the device
2088+
path = reverse(f'admin:{self.app_label}_device_change', args=[config.device_id])
2089+
params.update(
2090+
{
2091+
'config-0-id': str(config.pk),
2092+
'config-0-device': str(config.device_id),
2093+
'config-0-templates': str(template1.pk),
2094+
'config-INITIAL_FORMS': 1,
2095+
'_continue': True,
2096+
}
2097+
)
2098+
response = self.client.post(path, data=params, follow=True)
2099+
self.assertEqual(response.status_code, 200)
2100+
config.refresh_from_db()
2101+
2102+
# Ensure all works as expected
2103+
self.assertEqual(config.templates.count(), 1)
2104+
self.assertEqual(config.vpnclient_set.count(), 1)
2105+
self.assertEqual(
2106+
config.backend_instance.config['openvpn'][0]['cert'],
2107+
f'/etc/x509/client-{vpn.pk.hex}.pem',
2108+
)
2109+
self.assertEqual(
2110+
config.backend_instance.config['openvpn'][0]['dev'],
2111+
'tun0',
2112+
)
2113+
2114+
with self.subTest('Switch device to template2'):
2115+
path = reverse(
2116+
f'admin:{self.app_label}_device_change', args=[config.device_id]
2117+
)
2118+
params.update(
2119+
{
2120+
'config-0-templates': str(template2.pk),
2121+
}
2122+
)
2123+
response = self.client.post(path, data=params, follow=True)
2124+
self.assertEqual(response.status_code, 200)
2125+
config.refresh_from_db()
2126+
del config.backend_instance
2127+
self.assertEqual(
2128+
config.backend_instance.config['openvpn'][0]['cert'],
2129+
f'/etc/x509/client-{vpn.pk.hex}.pem',
2130+
)
2131+
self.assertEqual(
2132+
config.backend_instance.config['openvpn'][0]['dev'],
2133+
'tun1',
2134+
)
2135+
self.assertEqual(config.vpnclient_set.count(), 1)
2136+
2137+
with self.subTest('Switch device back to template1'):
2138+
params.update(
2139+
{
2140+
'config-0-templates': str(template1.pk),
2141+
}
2142+
)
2143+
response = self.client.post(path, data=params, follow=True)
2144+
self.assertEqual(response.status_code, 200)
2145+
config.refresh_from_db()
2146+
del config.backend_instance
2147+
self.assertEqual(
2148+
config.backend_instance.config['openvpn'][0]['cert'],
2149+
f'/etc/x509/client-{vpn.pk.hex}.pem',
2150+
)
2151+
self.assertEqual(
2152+
config.backend_instance.config['openvpn'][0]['dev'],
2153+
'tun0',
2154+
)
2155+
self.assertEqual(config.vpnclient_set.count(), 1)
2156+
20502157

20512158
class TestTransactionAdmin(
20522159
CreateConfigTemplateMixin,

0 commit comments

Comments
 (0)