Peer list with details

This commit is contained in:
Eduardo Silva 2024-02-17 11:53:51 -03:00
parent 0a14192444
commit cfcabed244
13 changed files with 207 additions and 8 deletions

0
api/__init__.py Normal file
View File

3
api/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
api/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

View File

3
api/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
api/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

55
api/views.py Normal file
View File

@ -0,0 +1,55 @@
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
import subprocess
@login_required
@require_http_methods(["GET"])
def wireguard_status(request):
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 ' '",
}
output = {}
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
else:
continue
if interface not in output:
output[interface] = {}
if peer not in output[interface]:
output[interface][peer] = {
'allowed-ips': [],
'latest-handshakes': '',
'transfer': {'tx': 0, 'rx': 0},
'endpoints': '',
}
if key == 'allowed-ips':
output[interface][peer]['allowed-ips'].append(value)
elif key == 'transfer':
tx, rx = value.split()[-2:]
output[interface][peer]['transfer'] = {'tx': int(tx), 'rx': int(rx)}
elif key == 'endpoints':
output[interface][peer]['endpoints'] = value
else:
output[interface][peer][key] = value
return JsonResponse(output)

View File

@ -192,7 +192,7 @@
<footer class="main-footer"> <footer class="main-footer">
wireguard-webadmin wireguard-webadmin
<div class="float-right d-none d-sm-inline-block"> <div class="float-right d-none d-sm-inline-block">
<b>Version</b> 0.8.1 beta <b>Version</b> 0.8.3 beta
</div> </div>
</footer> </footer>

View File

@ -7,10 +7,11 @@
<p>If you encounter any issues or have suggestions, please open an issue on GitHub so I can review it.</p> <p>If you encounter any issues or have suggestions, please open an issue on GitHub so I can review it.</p>
<h2>TODO list</h2> <h2>TODO list</h2>
<ul> <ul>
<li>The peers page does not yet display data from WireGuard such as the last handshake and data transfer.</li>
<li>The verification of allowed IPs against the output of wg show has not yet been implemented. This will help detect configuration errors for crypto routing.</li>
<li>The DNS server provided to the peer is still hardcoded.</li> <li>The DNS server provided to the peer is still hardcoded.</li>
<li>AllowedIPs on client configuration side.</li> <li>AllowedIPs on client configuration side.</li>
<li>Make Peer's last handshake permanent</li>
<li>Setting for refresh interval in Peer list</li>
<li>wireguard_webadmin Update notification</li>
</ul> </ul>

View File

@ -19,8 +19,8 @@
<div class="tab-pane fade show active" id="custom-content-below-home" role="tabpanel" aria-labelledby="custom-content-below-home-tab"> <div class="tab-pane fade show active" id="custom-content-below-home" role="tabpanel" aria-labelledby="custom-content-below-home-tab">
<div class="row"> <div class="row">
{% for peer in peer_list %} {% for peer in peer_list %}
<div class="col-md-6"> <div class="col-md-6" id="peer-{{ peer.public_key }}">
<div class="callout callout-success"> <div class="callout">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<h5> <h5>
{% if peer.name %} {% if peer.name %}
@ -34,6 +34,30 @@
<a href="/peer/manage/?peer={{ peer.uuid }}"><i class="far fa-edit"></i></a></span> <a href="/peer/manage/?peer={{ peer.uuid }}"><i class="far fa-edit"></i></a></span>
</div> </div>
{% comment %}This needs to be improved{% endcomment %} {% comment %}This needs to be improved{% endcomment %}
<p>
<b>Transfer:</b> <span id="peer-transfer-{{ peer.public_key }}"></span><br>
<b>Latest Handshake:</b> <span id="peer-latest-handshake-{{ peer.public_key }}"></span><br>
<b>Endpoints:</b> <span id="peer-endpoints-{{ peer.public_key }}"></span><br>
<b>Allowed IPs: </b><span id="peer-allowed-ips-{{ peer.public_key }}">
{% for address in peer.peerallowedip_set.all %}{% if address.priority == 0 %}
{% if address.missing_from_wireguard %}
<a href='#' class='bg-warning' title="This address does not appear in the wg show command output, likely indicating that another peer has an IP overlapping this network or that the configuration file is outdated.">{{ address }}</a>
{% else %}
{{ address }}
{% endif %}
{% endif %}{% endfor %}
{% for address in peer.peerallowedip_set.all %}{% if address.priority >= 1 %}
{% if address.missing_from_wireguard %}
<a href='#' class='bg-warning' title="This address does not appear in the wg show command output, likely indicating that another peer has an IP overlapping this network or that the configuration file is outdated.">{{ address }}</a>
{% else %}
{{ address }}
{% endif %}
{% endif %}{% endfor %}
</span>
</p>
{% comment %}
<p>{% for address in peer.peerallowedip_set.all %}{% if address.priority == 0 %} <p>{% for address in peer.peerallowedip_set.all %}{% if address.priority == 0 %}
{% if address.missing_from_wireguard %} {% if address.missing_from_wireguard %}
<a href='#' class='bg-warning' title="This address does not appear in the wg show command output, likely indicating that another peer has an IP overlapping this network or that the configuration file is outdated.">{{ address }}</a> <a href='#' class='bg-warning' title="This address does not appear in the wg show command output, likely indicating that another peer has an IP overlapping this network or that the configuration file is outdated.">{{ address }}</a>
@ -48,7 +72,10 @@
{% else %} {% else %}
{{ address }} {{ address }}
{% endif %} {% endif %}
{% endif %}{% endfor %}</p> {% endif %}{% endfor %}
</p>
{% endcomment %}
</div> </div>
@ -95,10 +122,105 @@
{% block custom_page_scripts %} {% block custom_page_scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const fetchWireguardStatus = async () => {
try {
const response = await fetch('/api/wireguard_status/');
const data = await response.json();
updateUI(data);
} catch (error) {
console.error('Error fetching Wireguard status:', error);
}
};
fetchWireguardStatus();
setInterval(fetchWireguardStatus, 30000);
});
const updateUI = (data) => {
for (const [interfaceName, peers] of Object.entries(data)) {
for (const [peerId, peerInfo] of Object.entries(peers)) {
const peerDiv = document.getElementById(`peer-${peerId}`);
if (peerDiv) {
updatePeerInfo(peerDiv, peerId, peerInfo);
updateCalloutClass(peerDiv, peerInfo['latest-handshakes']);
}
}
}
};
const updatePeerInfo = (peerDiv, peerId, peerInfo) => {
const escapedPeerId = peerId.replace(/([!"#$%&'()*+,.\/:;<=>?@[\]^`{|}~])/g, '\\$1');
const transfer = peerDiv.querySelector(`#peer-transfer-${escapedPeerId}`);
const latestHandshake = peerDiv.querySelector(`#peer-latest-handshake-${escapedPeerId}`);
const endpoints = peerDiv.querySelector(`#peer-endpoints-${escapedPeerId}`);
const allowedIps = peerDiv.querySelector(`#peer-allowed-ips-${escapedPeerId}`);
transfer.textContent = `${convertBytes(peerInfo.transfer.tx)} TX, ${convertBytes(peerInfo.transfer.rx)} RX`;
latestHandshake.textContent = `${peerInfo['latest-handshakes'] !== '0' ? new Date(parseInt(peerInfo['latest-handshakes']) * 1000).toLocaleString() : '0'}`;
endpoints.textContent = `${peerInfo.endpoints}`;
checkAllowedIps(allowedIps, peerInfo['allowed-ips']);
};
const convertBytes = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const checkAllowedIps = (allowedIpsElement, allowedIpsApiResponse) => {
const apiIps = allowedIpsApiResponse[0].split(' ');
const htmlIpsText = allowedIpsElement.textContent.trim();
const htmlIpsArray = htmlIpsText.match(/\b(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}\b/g);
allowedIpsElement.innerHTML = '';
htmlIpsArray.forEach((ip, index, array) => {
const ipSpan = document.createElement('span');
ipSpan.textContent = ip;
allowedIpsElement.appendChild(ipSpan);
if (!apiIps.includes(ip)) {
ipSpan.style.color = 'red';
ipSpan.style.textDecoration = 'underline';
ipSpan.title = 'This address does not appear in the wg show command output, likely indicating that another peer has an IP overlapping this network or that the configuration file is outdated.';
}
if (index < array.length - 1) {
allowedIpsElement.appendChild(document.createTextNode(', '));
}
});
};
const updateCalloutClass = (peerDiv, latestHandshake) => {
const calloutDiv = peerDiv.querySelector('.callout');
calloutDiv.classList.remove('callout-success', 'callout-info', 'callout-warning', 'callout-danger');
const handshakeAge = Date.now() / 1000 - parseInt(latestHandshake);
if (latestHandshake === '0') {
calloutDiv.classList.add('callout-danger');
} else if (handshakeAge < 600) {
calloutDiv.classList.add('callout-success');
} else if (handshakeAge < 1800) {
calloutDiv.classList.add('callout-info');
} else if (handshakeAge < 21600) {
calloutDiv.classList.add('callout-warning');
}
calloutDiv.style.transition = 'all 5s';
};
</script>
<script> <script>
function openImageLightbox(url) { function openImageLightbox(url) {
window.open(url, 'Image', 'width=500,height=500,toolbar=0,location=0,menubar=0'); window.open(url, 'Image', 'width=500,height=500,toolbar=0,location=0,menubar=0');
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -30,6 +30,9 @@ class PeerAllowedIPForm(forms.ModelForm):
priority = cleaned_data.get('priority') priority = cleaned_data.get('priority')
allowed_ip = cleaned_data.get('allowed_ip') allowed_ip = cleaned_data.get('allowed_ip')
netmask = cleaned_data.get('netmask') netmask = cleaned_data.get('netmask')
if allowed_ip is None:
raise forms.ValidationError("Please provide a valid IP address.")
wireguard_network = ipaddress.ip_network(f"{self.current_peer.wireguard_instance.address}/{self.current_peer.wireguard_instance.netmask}", strict=False) wireguard_network = ipaddress.ip_network(f"{self.current_peer.wireguard_instance.address}/{self.current_peer.wireguard_instance.netmask}", strict=False)
if priority == 0: if priority == 0:

View File

@ -119,7 +119,7 @@ def view_wireguard_peer_manage(request):
if current_peer.name: if current_peer.name:
page_title += current_peer.name page_title += current_peer.name
else: else:
page_title += current_peer.public_key page_title += current_peer.public_key[:16] + ("..." if len(current_peer.public_key) > 16 else "")
if request.method == 'POST': if request.method == 'POST':
form = PeerForm(request.POST, instance=current_peer) form = PeerForm(request.POST, instance=current_peer)
if form.is_valid(): if form.is_valid():
@ -157,7 +157,7 @@ def view_manage_ip_address(request):
if current_peer.name: if current_peer.name:
page_title += current_peer.name page_title += current_peer.name
else: else:
page_title += current_peer.public_key page_title += current_peer.public_key[:10] + ("..." if len(current_peer.public_key) > 16 else "")
if request.GET.get('action') == 'delete': if request.GET.get('action') == 'delete':
if request.GET.get('confirmation') == 'delete': if request.GET.get('confirmation') == 'delete':
current_ip.delete() current_ip.delete()

View File

@ -22,6 +22,7 @@ from console.views import view_console
from user_manager.views import view_user_list, view_manage_user from user_manager.views import view_user_list, view_manage_user
from accounts.views import view_create_first_user, view_login, view_logout from accounts.views import view_create_first_user, view_login, view_logout
from wireguard_tools.views import export_wireguard_configs, download_config_or_qrcode, restart_wireguard_interfaces from wireguard_tools.views import export_wireguard_configs, download_config_or_qrcode, restart_wireguard_interfaces
from api.views import wireguard_status
urlpatterns = [ urlpatterns = [
@ -41,4 +42,6 @@ urlpatterns = [
path('accounts/create_first_user/', view_create_first_user, name='create_first_user'), path('accounts/create_first_user/', view_create_first_user, name='create_first_user'),
path('accounts/login/', view_login, name='login'), path('accounts/login/', view_login, name='login'),
path('accounts/logout/', view_logout, name='logout'), path('accounts/logout/', view_logout, name='logout'),
path('api/wireguard_status/', wireguard_status, name='api_wireguard_status'),
] ]