diff --git a/api/admin.py b/api/admin.py index 8c38f3f..db95fdf 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,3 +1,10 @@ from django.contrib import admin +from .models import WireguardStatusCache + + # Register your models here. +class WireguardStatusCacheAdmin(admin.ModelAdmin): + list_display = ('cache_type', 'processing_time_ms', 'created', 'updated') + readonly_fields = ('uuid', 'created', 'updated') +admin.site.register(WireguardStatusCache, WireguardStatusCacheAdmin) \ No newline at end of file diff --git a/api/migrations/0002_alter_wireguardstatuscache_uuid.py b/api/migrations/0002_alter_wireguardstatuscache_uuid.py new file mode 100644 index 0000000..68d7394 --- /dev/null +++ b/api/migrations/0002_alter_wireguardstatuscache_uuid.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.9 on 2026-01-07 13:28 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='wireguardstatuscache', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ] diff --git a/api/models.py b/api/models.py index fe2a150..591d8ff 100644 --- a/api/models.py +++ b/api/models.py @@ -1,10 +1,13 @@ +import uuid + from django.db import models + class WireguardStatusCache(models.Model): cache_type = models.CharField(choices=(('master', 'Master'), ('cluster', 'Cluster')), max_length=16) data = models.JSONField() processing_time_ms = models.PositiveIntegerField() - uuid = models.UUIDField(unique=True) + uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) \ No newline at end of file diff --git a/api/views.py b/api/views.py index 98e8141..d84c86d 100644 --- a/api/views.py +++ b/api/views.py @@ -2,6 +2,7 @@ import base64 import datetime import os import subprocess +import time import uuid import pytz @@ -17,6 +18,7 @@ from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.views.decorators.http import require_http_methods +from api.models import WireguardStatusCache from user_manager.models import AuthenticationToken, UserAcl from vpn_invite.models import InviteSettings, PeerInvite from wgwadmlibrary.tools import create_peer_invite, get_peer_invite_data, send_email, user_allowed_peers, \ @@ -208,12 +210,153 @@ def api_instance_info(request): } return JsonResponse(data) + +def func_process_wireguard_status(): + # Query WireGuard status from the system and construct the data dictionary + commands = { + 'latest-handshakes': "wg show all latest-handshakes | expand | tr -s ' '", + 'allowed-ips': "wg show all allowed-ips | expand | tr -s ' '", + 'transfer': "wg show all transfer | expand | tr -s ' '", + 'endpoints': "wg show all endpoints | expand | tr -s ' '", + } + + data = {} + + for key, command in commands.items(): + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + stdout, stderr = process.communicate() + + if process.returncode != 0: + return JsonResponse({'error': stderr}, status=400) + + current_interface = None + for line in stdout.strip().split('\n'): + parts = line.split() + if len(parts) >= 3: + interface, peer, value = parts[0], parts[1], " ".join(parts[2:]) + current_interface = interface + elif len(parts) == 2 and current_interface: + peer, value = parts + else: + continue + + if interface not in data: + data[interface] = {} + + if peer not in data[interface]: + data[interface][peer] = { + 'allowed-ips': [], + 'latest-handshakes': '', + 'transfer': {'tx': 0, 'rx': 0}, + 'endpoints': '', + } + + if key == 'allowed-ips': + data[interface][peer]['allowed-ips'].append(value) + elif key == 'transfer': + rx, tx = value.split()[-2:] + data[interface][peer]['transfer'] = {'tx': int(tx), 'rx': int(rx)} + elif key == 'endpoints': + data[interface][peer]['endpoints'] = value + else: + data[interface][peer][key] = value + + return data + + +def func_apply_enhanced_filter(data: dict, user_acl: UserAcl): + # Remove peers and instances that are not allowed for the user + if user_acl.enable_enhanced_filter: + pass + else: + pass + return data + + +def func_get_wireguard_status(): + if settings.WIREGUARD_STATUS_CACHE_ENABLED: + cache_entry = WireguardStatusCache.objects.filter(cache_type='master').order_by('-created').first() + if cache_entry: + data = cache_entry.data + data['cache_information'] = { + 'processing_time_ms': cache_entry.processing_time_ms, + 'created': cache_entry.created.isoformat(), + 'cache_type': cache_entry.cache_type, + 'cache_hit': True, + 'cache_enabled': True, + 'cache_uuid': str(cache_entry.uuid), + } + data['status'] = 'success' + data['message'] = 'WireGuard status retrieved from cache' + else: + data = { + 'status': 'error', + 'message': 'No cache entry found', + 'cache_information': { + 'cache_hit': False, + 'cache_enabled': True, + } + } + else: + data = func_process_wireguard_status() + data['cache_information'] = { + 'cache_hit': False, + 'cache_enabled': False, + } + data['status'] = 'success' + data['message'] = 'WireGuard status retrieved without cache' + return data + + +def cron_refresh_wireguard_status_cache(request): + data = {'status': 'success'} + WireguardStatusCache.objects.filter(created__lt=timezone.now() - timezone.timedelta(seconds=settings.WIREGUARD_STATUS_CACHE_MAX_AGE)).delete() + + if not settings.WIREGUARD_STATUS_CACHE_ENABLED: + return JsonResponse(data) + start_time = time.monotonic() + data = func_process_wireguard_status() + end_time = time.monotonic() + processing_time_ms = int((end_time - start_time) * 1000) + WireguardStatusCache.objects.create(data=data, processing_time_ms=processing_time_ms, cache_type='master') + return JsonResponse(data) + + @require_http_methods(["GET"]) def wireguard_status(request): user_acl = None enhanced_filter = False filter_peer_list = [] + if request.user.is_authenticated: + user_acl = get_object_or_404(UserAcl, user=request.user) + if user_acl.enable_enhanced_filter and user_acl.peer_groups.count() > 0: + enhanced_filter = True + elif request.GET.get('key'): + api_key = get_api_key('api') + if api_key and api_key == request.GET.get('key'): + pass + else: + return HttpResponseForbidden() + elif request.GET.get('rrdkey'): + api_key = get_api_key('rrdkey') + if api_key and api_key == request.GET.get('rrdkey'): + pass + else: + return HttpResponseForbidden() + else: + return HttpResponseForbidden() + + data = func_get_wireguard_status() + return JsonResponse(data) + + +@require_http_methods(["GET"]) +def legacy_wireguard_status(request): + user_acl = None + enhanced_filter = False + filter_peer_list = [] + if request.user.is_authenticated: user_acl = get_object_or_404(UserAcl, user=request.user) if user_acl.enable_enhanced_filter and user_acl.peer_groups.count() > 0: diff --git a/cron/cron_tasks b/cron/cron_tasks index 4b1b39d..2d0834d 100644 --- a/cron/cron_tasks +++ b/cron/cron_tasks @@ -1,2 +1,3 @@ * * * * * root /usr/bin/curl -s http://wireguard-webadmin:8000/api/cron_check_updates/ >> /var/log/cron.log 2>&1 */10 * * * * root /usr/bin/curl -s http://wireguard-webadmin:8000/api/cron_update_peer_latest_handshake/ >> /var/log/cron.log 2>&1 +* * * * * root /usr/bin/curl -s http://wireguard-webadmin:8000/api/cron_refresh_wireguard_status_cache/ >> /var/log/cron.log 2>&1 diff --git a/wireguard_webadmin/settings.py b/wireguard_webadmin/settings.py index 36d20be..922f9ce 100644 --- a/wireguard_webadmin/settings.py +++ b/wireguard_webadmin/settings.py @@ -155,6 +155,8 @@ STATICFILES_DIRS = [ BASE_DIR / "static_files", ] +WIREGUARD_STATUS_CACHE_ENABLED = True +WIREGUARD_STATUS_CACHE_MAX_AGE = 600 # Default primary key field type # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DNS_CONFIG_FILE = '/etc/dnsmasq/wireguard_webadmin_dns.conf' diff --git a/wireguard_webadmin/urls.py b/wireguard_webadmin/urls.py index 31d6a6b..a02da79 100644 --- a/wireguard_webadmin/urls.py +++ b/wireguard_webadmin/urls.py @@ -20,7 +20,7 @@ from django.urls import path from accounts.views import view_create_first_user, view_login, view_logout from api.views import api_instance_info, api_peer_invite, api_peer_list, cron_check_updates, \ cron_update_peer_latest_handshake, peer_info, routerfleet_authenticate_session, routerfleet_get_user_token, \ - wireguard_status + wireguard_status, cron_refresh_wireguard_status_cache from cluster.cluster_api import api_cluster_status, api_get_worker_config_files, api_get_worker_dnsmasq_config, \ api_worker_ping from cluster.views import cluster_main, cluster_settings, worker_manage @@ -75,6 +75,7 @@ urlpatterns = [ path('api/instance_info/', api_instance_info, name='api_instance_info'), path('api/peer_info/', peer_info, name='api_peer_info'), path('api/peer_invite/', api_peer_invite, name='api_peer_invite'), + path('api/cron_refresh_wireguard_status_cache/', cron_refresh_wireguard_status_cache, name='cron_refresh_wireguard_status_cache'), path('api/cron_check_updates/', cron_check_updates, name='cron_check_updates'), path('api/cron_update_peer_latest_handshake/', cron_update_peer_latest_handshake, name='cron_update_peer_latest_handshake'), path('api/cluster/status/', api_cluster_status, name='api_cluster_status'),