feat(wireguard-dashboard,webapp): Add setup wizard, admin sessions, and blocking stats

WireGuard Dashboard v0.7.0:
- Add zone-based setup wizard with 4-step flow
- Add tunnel presets (road-warrior, site-to-site, iot-tunnel)
- Add zone presets (home-user, remote-worker, mobile, iot, guest, server)
- Add interface control (up/down/restart)
- Add peer ping functionality
- Add bandwidth rates monitoring
- Comprehensive wizard CSS styles

SecuBox Webapp v1.5.0:
- Add admin sessions list showing authenticated LuCI users with IP source
- Add blocking statistics (today's bans, blocked attempts, top scenario, unique IPs)
- Integrate stats from CrowdSec decisions and alerts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-21 15:40:46 +01:00
parent a1d66157fc
commit 5e29599682
10 changed files with 2386 additions and 15 deletions

View File

@ -8,7 +8,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wireguard-dashboard
PKG_VERSION:=0.5.0
PKG_VERSION:=0.7.0
PKG_RELEASE:=1
PKG_ARCH:=all

View File

@ -11,11 +11,69 @@ return view.extend({
pollInterval: 5,
pollActive: true,
selectedInterface: 'all',
peerDescriptions: {},
bandwidthRates: {},
load: function() {
return api.getAllData();
},
// Interface control actions
handleInterfaceAction: function(iface, action) {
var self = this;
ui.showModal(_('Interface Control'), [
E('p', { 'class': 'spinning' }, _('Executing %s on %s...').format(action, iface))
]);
api.interfaceControl(iface, action).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', result.message || _('Action completed')), 'info');
} else {
ui.addNotification(null, E('p', result.error || _('Action failed')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
},
// Ping peer
handlePingPeer: function(peerIp, peerName) {
var self = this;
if (!peerIp || peerIp === '(none)') {
ui.addNotification(null, E('p', _('No endpoint IP available for this peer')), 'warning');
return;
}
// Extract IP from endpoint (remove port)
var ip = peerIp.split(':')[0];
ui.showModal(_('Ping Peer'), [
E('p', { 'class': 'spinning' }, _('Pinging %s (%s)...').format(peerName, ip))
]);
api.pingPeer(ip).then(function(result) {
ui.hideModal();
if (result.reachable) {
ui.addNotification(null, E('p', _('Peer %s is reachable (RTT: %s ms)').format(peerName, result.rtt_ms)), 'info');
} else {
ui.addNotification(null, E('p', _('Peer %s is not reachable').format(peerName)), 'warning');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Ping failed: %s').format(err.message || err)), 'error');
});
},
// Get peer display name
getPeerName: function(peer) {
if (this.peerDescriptions && this.peerDescriptions[peer.public_key]) {
return this.peerDescriptions[peer.public_key];
}
return 'Peer ' + peer.short_key;
},
// Interface tab filtering
setInterfaceFilter: function(ifaceName) {
this.selectedInterface = ifaceName;
@ -184,7 +242,10 @@ return view.extend({
var status = data.status || {};
var interfaces = (data.interfaces || {}).interfaces || [];
var peers = (data.peers || {}).peers || [];
// Store peer descriptions
this.peerDescriptions = data.descriptions || {};
var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length;
var view = E('div', { 'class': 'wireguard-dashboard' }, [
@ -323,6 +384,25 @@ return view.extend({
E('div', { 'class': 'wg-interface-detail-value wg-interface-traffic' },
'↓' + api.formatBytes(iface.rx_bytes) + ' / ↑' + api.formatBytes(iface.tx_bytes))
])
]),
// Interface control buttons
E('div', { 'class': 'wg-interface-controls' }, [
iface.state === 'up' ?
E('button', {
'class': 'wg-btn wg-btn-sm wg-btn-warning',
'click': L.bind(self.handleInterfaceAction, self, iface.name, 'down'),
'title': _('Bring interface down')
}, '⏹ Stop') :
E('button', {
'class': 'wg-btn wg-btn-sm wg-btn-success',
'click': L.bind(self.handleInterfaceAction, self, iface.name, 'up'),
'title': _('Bring interface up')
}, '▶ Start'),
E('button', {
'class': 'wg-btn wg-btn-sm',
'click': L.bind(self.handleInterfaceAction, self, iface.name, 'restart'),
'title': _('Restart interface')
}, '🔄 Restart')
])
]);
})
@ -347,12 +427,13 @@ return view.extend({
E('div', { 'class': 'wg-card-body' }, [
E('div', { 'class': 'wg-peer-grid' },
peers.slice(0, 6).map(function(peer) {
var peerName = self.getPeerName(peer);
return E('div', { 'class': 'wg-peer-card ' + (peer.status === 'active' ? 'active' : ''), 'data-peer': peer.public_key, 'data-interface': peer.interface || '' }, [
E('div', { 'class': 'wg-peer-header' }, [
E('div', { 'class': 'wg-peer-info' }, [
E('div', { 'class': 'wg-peer-icon' }, peer.status === 'active' ? '✅' : '👤'),
E('div', {}, [
E('p', { 'class': 'wg-peer-name' }, 'Peer ' + peer.short_key),
E('p', { 'class': 'wg-peer-name' }, peerName),
E('p', { 'class': 'wg-peer-key' }, api.shortenKey(peer.public_key, 16))
])
]),
@ -383,7 +464,16 @@ return view.extend({
E('div', { 'class': 'wg-peer-traffic-value tx' }, api.formatBytes(peer.tx_bytes)),
E('div', { 'class': 'wg-peer-traffic-label' }, 'Sent')
])
])
]),
// Peer action buttons
peer.endpoint && peer.endpoint !== '(none)' ?
E('div', { 'class': 'wg-peer-actions' }, [
E('button', {
'class': 'wg-btn wg-btn-xs',
'click': L.bind(self.handlePingPeer, self, peer.endpoint, peerName),
'title': _('Ping peer')
}, '📡 Ping')
]) : ''
]);
})
)

View File

@ -18,13 +18,13 @@ var callStatus = rpc.declare({
var callGetPeers = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'get_peers',
method: 'peers',
expect: { peers: [] }
});
var callGetInterfaces = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'get_interfaces',
method: 'interfaces',
expect: { interfaces: [] }
});
@ -74,6 +74,32 @@ var callGetTraffic = rpc.declare({
expect: { }
});
var callInterfaceControl = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'interface_control',
params: ['interface', 'action'],
expect: { success: false }
});
var callPeerDescriptions = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'peer_descriptions',
expect: { descriptions: {} }
});
var callBandwidthRates = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'bandwidth_rates',
expect: { rates: [] }
});
var callPingPeer = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'ping_peer',
params: ['ip'],
expect: { reachable: false }
});
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
@ -112,6 +138,15 @@ function formatHandshake(seconds) {
return Math.floor(seconds / 86400) + 'd ago';
}
function formatRate(bytesPerSec) {
if (!bytesPerSec || bytesPerSec === 0) return '0 B/s';
var k = 1024;
var sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
var i = Math.floor(Math.log(bytesPerSec) / Math.log(k));
if (i >= sizes.length) i = sizes.length - 1;
return parseFloat((bytesPerSec / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
return baseclass.extend({
getStatus: callStatus,
getPeers: callGetPeers,
@ -123,11 +158,16 @@ return baseclass.extend({
removePeer: callRemovePeer,
generateConfig: callGenerateConfig,
generateQR: callGenerateQR,
interfaceControl: callInterfaceControl,
getPeerDescriptions: callPeerDescriptions,
getBandwidthRates: callBandwidthRates,
pingPeer: callPingPeer,
formatBytes: formatBytes,
formatLastHandshake: formatLastHandshake,
getPeerStatusClass: getPeerStatusClass,
shortenKey: shortenKey,
formatHandshake: formatHandshake,
formatRate: formatRate,
// Aggregate function for overview page
getAllData: function() {
@ -135,13 +175,32 @@ return baseclass.extend({
callStatus(),
callGetPeers(),
callGetInterfaces(),
callGetTraffic()
callGetTraffic(),
callPeerDescriptions()
]).then(function(results) {
return {
status: results[0] || {},
peers: results[1] || { peers: [] },
interfaces: results[2] || { interfaces: [] },
traffic: results[3] || {}
traffic: results[3] || {},
descriptions: (results[4] || {}).descriptions || {}
};
});
},
// Get data with bandwidth rates for real-time monitoring
getMonitoringData: function() {
return Promise.all([
callStatus(),
callGetPeers(),
callBandwidthRates(),
callPeerDescriptions()
]).then(function(results) {
return {
status: results[0] || {},
peers: results[1] || { peers: [] },
rates: (results[2] || {}).rates || [],
descriptions: (results[3] || {}).descriptions || {}
};
});
}

View File

@ -816,6 +816,62 @@
font-size: 12px;
}
.wg-btn-xs {
padding: 4px 8px;
font-size: 11px;
gap: 4px;
}
.wg-btn-success {
background: linear-gradient(135deg, var(--wg-accent-green), #059669);
border: none;
color: white;
}
.wg-btn-success:hover {
box-shadow: 0 0 20px rgba(16, 185, 129, 0.4);
}
.wg-btn-warning {
background: linear-gradient(135deg, var(--wg-accent-yellow), #d97706);
border: none;
color: white;
}
.wg-btn-warning:hover {
box-shadow: 0 0 20px rgba(245, 158, 11, 0.4);
}
.wg-btn-danger {
background: linear-gradient(135deg, var(--wg-accent-red), #dc2626);
border: none;
color: white;
}
.wg-btn-danger:hover {
box-shadow: 0 0 20px rgba(239, 68, 68, 0.4);
}
/* Interface controls */
.wg-interface-controls {
display: flex;
gap: 8px;
padding-top: 12px;
margin-top: 12px;
border-top: 1px solid var(--wg-border);
justify-content: flex-end;
}
/* Peer actions */
.wg-peer-actions {
display: flex;
gap: 6px;
padding-top: 10px;
margin-top: 10px;
border-top: 1px solid var(--wg-border);
justify-content: center;
}
/* Auto-refresh control */
.wg-refresh-control {
display: flex;
@ -1140,3 +1196,528 @@
.wg-chart-legend-dot.tx {
background: var(--wg-accent-blue);
}
/* ========== Wizard Styles ========== */
/* Tunnel type selection */
.wg-tunnel-types {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin: 20px 0;
}
.wg-tunnel-type-card {
background: var(--wg-bg-tertiary);
border: 2px solid var(--wg-border);
border-radius: var(--wg-radius-lg);
padding: 24px;
cursor: pointer;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.wg-tunnel-type-card:hover {
border-color: var(--wg-accent-cyan);
transform: translateY(-2px);
}
.wg-tunnel-type-card.selected {
border-color: var(--wg-accent-green);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), transparent);
}
.wg-tunnel-type-card.selected::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--wg-accent-green);
}
.wg-tunnel-type-icon {
font-size: 40px;
margin-bottom: 12px;
}
.wg-tunnel-type-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.wg-tunnel-type-desc {
font-size: 13px;
color: var(--wg-text-secondary);
line-height: 1.5;
}
.wg-tunnel-type-features {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
}
.wg-tunnel-type-feature {
font-size: 10px;
padding: 4px 8px;
background: var(--wg-bg-primary);
border-radius: 10px;
color: var(--wg-text-muted);
}
/* Zone preset cards */
.wg-zone-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
margin: 20px 0;
}
.wg-zone-card {
background: var(--wg-bg-tertiary);
border: 2px solid var(--wg-border);
border-radius: var(--wg-radius-lg);
padding: 20px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.wg-zone-card:hover {
border-color: var(--wg-accent-cyan);
transform: translateY(-2px);
}
.wg-zone-card.selected {
border-color: var(--zone-color, var(--wg-accent-green));
background: linear-gradient(135deg, rgba(var(--zone-color-rgb, 16, 185, 129), 0.1), transparent);
}
.wg-zone-icon {
font-size: 36px;
margin-bottom: 10px;
}
.wg-zone-name {
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
}
.wg-zone-desc {
font-size: 11px;
color: var(--wg-text-muted);
line-height: 1.4;
}
/* Wizard form */
.wg-wizard-form {
max-width: 600px;
margin: 0 auto;
}
.wg-wizard-section {
margin-bottom: 24px;
}
.wg-wizard-section-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.wg-wizard-section-title::before {
content: '';
width: 4px;
height: 16px;
background: var(--wg-tunnel-gradient);
border-radius: 2px;
}
/* Input styles */
.wg-input {
width: 100%;
padding: 12px 14px;
background: var(--wg-bg-primary);
border: 1px solid var(--wg-border);
border-radius: var(--wg-radius);
color: var(--wg-text-primary);
font-family: var(--wg-font-mono);
font-size: 13px;
transition: all 0.2s;
}
.wg-input:focus {
outline: none;
border-color: var(--wg-accent-cyan);
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15);
}
.wg-input::placeholder {
color: var(--wg-text-muted);
}
.wg-select {
width: 100%;
padding: 12px 14px;
background: var(--wg-bg-primary);
border: 1px solid var(--wg-border);
border-radius: var(--wg-radius);
color: var(--wg-text-primary);
font-size: 13px;
cursor: pointer;
}
.wg-select:focus {
outline: none;
border-color: var(--wg-accent-cyan);
}
.wg-checkbox-group {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: var(--wg-bg-tertiary);
border-radius: var(--wg-radius);
cursor: pointer;
}
.wg-checkbox-group:hover {
background: var(--wg-bg-secondary);
}
.wg-checkbox {
width: 18px;
height: 18px;
accent-color: var(--wg-accent-cyan);
}
/* Peer list in wizard */
.wg-peer-list {
display: flex;
flex-direction: column;
gap: 12px;
margin: 16px 0;
}
.wg-peer-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: var(--wg-bg-tertiary);
border: 1px solid var(--wg-border);
border-radius: var(--wg-radius);
}
.wg-peer-list-info {
display: flex;
align-items: center;
gap: 12px;
}
.wg-peer-list-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: var(--wg-bg-primary);
}
.wg-peer-list-details h4 {
font-size: 14px;
font-weight: 600;
margin: 0 0 4px 0;
}
.wg-peer-list-details p {
font-size: 11px;
color: var(--wg-text-muted);
margin: 0;
font-family: var(--wg-font-mono);
}
.wg-peer-list-actions {
display: flex;
gap: 8px;
}
/* QR Code Modal */
.wg-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fade-in 0.2s ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.wg-modal {
background: var(--wg-bg-secondary);
border: 1px solid var(--wg-border);
border-radius: var(--wg-radius-lg);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
animation: modal-slide 0.3s ease-out;
}
@keyframes modal-slide {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.wg-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--wg-border);
}
.wg-modal-title {
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.wg-modal-close {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--wg-text-muted);
font-size: 24px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
}
.wg-modal-close:hover {
background: var(--wg-bg-tertiary);
color: var(--wg-text-primary);
}
.wg-modal-body {
padding: 24px;
}
.wg-modal-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 16px 20px;
border-top: 1px solid var(--wg-border);
}
/* QR Code display */
.wg-qr-container {
text-align: center;
margin: 20px 0;
}
.wg-qr-code {
display: inline-block;
padding: 16px;
background: white;
border-radius: var(--wg-radius-lg);
}
.wg-qr-code img,
.wg-qr-code svg {
display: block;
max-width: 200px;
height: auto;
}
.wg-qr-hint {
font-size: 12px;
color: var(--wg-text-muted);
margin-top: 12px;
}
/* Config preview */
.wg-config-preview {
background: var(--wg-bg-primary);
border: 1px solid var(--wg-border);
border-radius: var(--wg-radius);
padding: 16px;
font-family: var(--wg-font-mono);
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
/* Wizard navigation */
.wg-wizard-nav {
display: flex;
justify-content: space-between;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--wg-border);
}
/* Success state */
.wg-success-state {
text-align: center;
padding: 40px 20px;
}
.wg-success-icon {
font-size: 64px;
margin-bottom: 20px;
animation: bounce-in 0.5s ease-out;
}
@keyframes bounce-in {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.wg-success-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 12px;
color: var(--wg-accent-green);
}
.wg-success-desc {
font-size: 14px;
color: var(--wg-text-secondary);
margin-bottom: 24px;
}
/* Loading state */
.wg-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
}
.wg-loading-spinner {
width: 48px;
height: 48px;
border: 3px solid var(--wg-border);
border-top-color: var(--wg-accent-cyan);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.wg-loading-text {
margin-top: 16px;
font-size: 14px;
color: var(--wg-text-secondary);
}
/* Info box */
.wg-info-box {
display: flex;
gap: 12px;
padding: 14px 16px;
background: rgba(6, 182, 212, 0.1);
border: 1px solid rgba(6, 182, 212, 0.3);
border-radius: var(--wg-radius);
margin: 16px 0;
}
.wg-info-box-icon {
font-size: 20px;
flex-shrink: 0;
}
.wg-info-box-content {
font-size: 13px;
color: var(--wg-text-secondary);
line-height: 1.5;
}
.wg-info-box.warning {
background: rgba(245, 158, 11, 0.1);
border-color: rgba(245, 158, 11, 0.3);
}
/* Copy button */
.wg-copy-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--wg-bg-tertiary);
border: 1px solid var(--wg-border);
border-radius: 6px;
color: var(--wg-text-secondary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.wg-copy-btn:hover {
border-color: var(--wg-accent-cyan);
color: var(--wg-text-primary);
}
.wg-copy-btn.copied {
border-color: var(--wg-accent-green);
color: var(--wg-accent-green);
}
/* Endpoint detection */
.wg-endpoint-detect {
display: flex;
gap: 10px;
align-items: center;
}
.wg-endpoint-detect .wg-input {
flex: 1;
}
.wg-endpoint-status {
font-size: 11px;
color: var(--wg-text-muted);
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
}

View File

@ -752,10 +752,148 @@ ping_peer() {
json_dump
}
# Interface up/down control
interface_control() {
read input
json_load "$input"
json_get_var iface interface
json_get_var action action
json_init
if [ -z "$iface" ] || [ -z "$action" ]; then
json_add_boolean "success" 0
json_add_string "error" "Missing interface or action"
json_dump
return
fi
# Validate action
case "$action" in
up|down|restart)
;;
*)
json_add_boolean "success" 0
json_add_string "error" "Invalid action. Use: up, down, restart"
json_dump
return
;;
esac
# Check if interface exists
if ! uci -q get network.$iface >/dev/null; then
json_add_boolean "success" 0
json_add_string "error" "Interface not found: $iface"
json_dump
return
fi
# Execute action
case "$action" in
up)
ifup "$iface" 2>/dev/null
;;
down)
ifdown "$iface" 2>/dev/null
;;
restart)
ifdown "$iface" 2>/dev/null
sleep 1
ifup "$iface" 2>/dev/null
;;
esac
local rc=$?
if [ $rc -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Interface $iface $action completed"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to $action interface $iface"
fi
json_dump
}
# Get peer descriptions from UCI
get_peer_descriptions() {
json_init
json_add_object "descriptions"
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
for iface in $interfaces; do
local sections=$(uci show network 2>/dev/null | grep "=wireguard_$iface$" | cut -d'.' -f2 | cut -d'=' -f1)
for section in $sections; do
local pubkey=$(uci -q get network.$section.public_key)
local desc=$(uci -q get network.$section.description)
if [ -n "$pubkey" ] && [ -n "$desc" ]; then
json_add_string "$pubkey" "$desc"
fi
done
done
json_close_object
json_dump
}
# Get current bandwidth rates (requires previous call to calculate delta)
get_bandwidth_rates() {
json_init
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
local now=$(date +%s)
json_add_array "rates"
for iface in $interfaces; do
local rx_bytes=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0)
local tx_bytes=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0)
# Get previous values from temp file
local prev_file="/tmp/wg_rate_$iface"
local prev_rx=0
local prev_tx=0
local prev_time=0
if [ -f "$prev_file" ]; then
read prev_rx prev_tx prev_time < "$prev_file"
fi
# Calculate rates
local rx_rate=0
local tx_rate=0
local time_diff=$((now - prev_time))
if [ $time_diff -gt 0 ] && [ $prev_time -gt 0 ]; then
rx_rate=$(( (rx_bytes - prev_rx) / time_diff ))
tx_rate=$(( (tx_bytes - prev_tx) / time_diff ))
# Ensure non-negative rates
[ $rx_rate -lt 0 ] && rx_rate=0
[ $tx_rate -lt 0 ] && tx_rate=0
fi
# Save current values
echo "$rx_bytes $tx_bytes $now" > "$prev_file"
json_add_object
json_add_string "interface" "$iface"
json_add_int "rx_rate" "$rx_rate"
json_add_int "tx_rate" "$tx_rate"
json_add_int "rx_total" "$rx_bytes"
json_add_int "tx_total" "$tx_bytes"
json_close_object
done
json_close_array
json_add_int "timestamp" "$now"
json_dump
}
# Main dispatcher
case "$1" in
list)
echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"}}'
echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{}}'
;;
call)
case "$2" in
@ -798,6 +936,15 @@ case "$1" in
ping_peer)
ping_peer
;;
interface_control)
interface_control
;;
peer_descriptions)
get_peer_descriptions
;;
bandwidth_rates)
get_bandwidth_rates
;;
*)
echo '{"error": "Unknown method"}'
;;

View File

@ -9,6 +9,14 @@
"acl": ["luci-app-wireguard-dashboard"]
}
},
"admin/secubox/network/wireguard/wizard": {
"title": "Setup Wizard",
"order": 5,
"action": {
"type": "view",
"path": "wireguard-dashboard/wizard"
}
},
"admin/secubox/network/wireguard/overview": {
"title": "Overview",
"order": 10,

View File

@ -5,10 +5,14 @@
"ubus": {
"luci.wireguard-dashboard": [
"status",
"get_interfaces",
"get_peers",
"interfaces",
"peers",
"config",
"traffic"
"traffic",
"peer_descriptions",
"bandwidth_rates",
"bandwidth_history",
"endpoint_info"
],
"system": [ "info", "board" ],
"file": [ "read", "stat", "exec" ]
@ -26,10 +30,12 @@
"add_peer",
"remove_peer",
"generate_config",
"generate_qr"
"generate_qr",
"interface_control",
"ping_peer"
]
},
"uci": [ "wireguard-dashboard" ]
"uci": [ "wireguard-dashboard", "network" ]
}
}
}

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-webapp
PKG_VERSION:=1.4.1
PKG_VERSION:=1.5.0
PKG_RELEASE:=1
PKG_LICENSE:=MIT
PKG_MAINTAINER:=CyberMind.FR <contact@cybermind.fr>

View File

@ -2553,6 +2553,65 @@
</div>
</div>
<!-- Third Row - Admin Sessions -->
<div class="cards-grid-2" style="margin-top: 1.5rem;">
<!-- Admin Sessions -->
<div class="card">
<div class="card-header">
<div class="card-title">
<i data-lucide="users" style="width: 18px; height: 18px; color: var(--rose);"></i>
Sessions Admin LuCI
</div>
<div class="card-badge" id="adminSessionsCount">0 sessions</div>
</div>
<div class="card-body" style="overflow-x: auto; max-height: 200px; overflow-y: auto;">
<table class="interfaces-table">
<thead>
<tr>
<th>Utilisateur</th>
<th>IP Source</th>
<th>Expiration</th>
<th>Statut</th>
</tr>
</thead>
<tbody id="adminSessionsBody">
<!-- Sessions dynamically loaded -->
</tbody>
</table>
</div>
</div>
<!-- Firewall Stats Overview -->
<div class="card">
<div class="card-header">
<div class="card-title">
<i data-lucide="shield-ban" style="width: 18px; height: 18px; color: var(--amber);"></i>
Statistiques Blocage
</div>
</div>
<div class="card-body">
<div class="stats-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="stat-item">
<div class="stat-value" id="totalBansToday">--</div>
<div class="stat-label">Bans Aujourd'hui</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalBlockedAttempts">--</div>
<div class="stat-label">Tentatives Bloquées</div>
</div>
<div class="stat-item">
<div class="stat-value" id="topScenario">--</div>
<div class="stat-label">Top Scénario</div>
</div>
<div class="stat-item">
<div class="stat-value" id="uniqueIPs">--</div>
<div class="stat-label">IPs Uniques</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card" style="margin-top: 1.5rem;">
<div class="card-header">
@ -3691,6 +3750,16 @@
CROWDSEC.getAlerts()
]);
updateCrowdSecMetrics(decisions, bouncers, alerts);
// Update firewall blocking statistics
if (currentTab === 'overview') {
updateBlockingStats(decisions, alerts);
}
}
// Load admin sessions on overview tab
if (currentTab === 'overview') {
loadAdminSessions().catch(e => console.warn('Admin sessions error:', e));
}
// Get logs only when on overview or logs tab
@ -4028,6 +4097,181 @@
lucide.createIcons();
}
// =====================================================
// Admin Sessions & Blocking Statistics
// =====================================================
async function loadAdminSessions() {
const tbody = document.getElementById('adminSessionsBody');
const countBadge = document.getElementById('adminSessionsCount');
try {
// Get LuCI sessions by listing session files
const result = await UBUS.execCommand('/bin/sh', ['-c',
'for f in /tmp/luci-sessions/*; do [ -f "$f" ] && echo "$(basename $f)|$(cat $f 2>/dev/null | jsonfilter -e @.data.username 2>/dev/null)|$(cat $f 2>/dev/null | jsonfilter -e @.data.localtime 2>/dev/null)|$(stat -c %Y $f 2>/dev/null)"; done 2>/dev/null || ls -la /tmp/luci-sessions/ 2>/dev/null'
]);
let sessions = [];
if (result && result.stdout) {
const lines = result.stdout.trim().split('\n').filter(l => l && l.includes('|'));
sessions = lines.map(line => {
const parts = line.split('|');
return {
id: parts[0] || 'unknown',
username: parts[1] || 'root',
ip: parts[2] || 'local',
mtime: parseInt(parts[3]) || 0
};
}).filter(s => s.id !== 'unknown');
}
// Also try to get session via ubus session.list (requires admin)
try {
const ubusResult = await UBUS.call('session', 'list', {});
if (ubusResult && typeof ubusResult === 'object') {
// Add current session info
const currentSession = {
id: STATE.sessionId ? STATE.sessionId.substring(0, 8) : 'current',
username: ubusResult.username || 'root',
ip: window.location.hostname,
mtime: Math.floor(Date.now() / 1000),
expires: ubusResult.expires || 300,
current: true
};
// Add if not already in list
if (!sessions.find(s => s.current)) {
sessions.unshift(currentSession);
}
}
} catch (e) {
// Session.list might not be available
}
// Update badge
countBadge.textContent = `${sessions.length} session${sessions.length > 1 ? 's' : ''}`;
if (sessions.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="4" style="text-align: center; color: var(--text-muted);">
Aucune session active
</td>
</tr>
`;
return;
}
const now = Math.floor(Date.now() / 1000);
tbody.innerHTML = sessions.map(session => {
const age = now - session.mtime;
const isActive = age < 3600; // Active if less than 1 hour old
const expireText = session.expires ?
`${Math.floor(session.expires / 60)} min` :
(age < 60 ? 'maintenant' : `${Math.floor(age / 60)} min`);
return `
<tr>
<td>
<div style="display: flex; align-items: center; gap: 6px;">
<i data-lucide="user" style="width: 14px; height: 14px; color: var(--cyan);"></i>
${escapeHtml(session.username)}
${session.current ? '<span style="font-size: 0.65rem; background: var(--cyan); color: var(--bg); padding: 1px 4px; border-radius: 3px;">vous</span>' : ''}
</div>
</td>
<td>
<code style="font-size: 0.7rem; background: var(--surface); padding: 2px 6px; border-radius: 3px;">
${escapeHtml(session.ip)}
</code>
</td>
<td style="font-size: 0.75rem;">${expireText}</td>
<td>
<span class="status-badge ${isActive ? 'online' : 'offline'}" style="font-size: 0.65rem;">
${isActive ? 'Actif' : 'Expiré'}
</span>
</td>
</tr>
`;
}).join('');
lucide.createIcons();
} catch (e) {
console.warn('Error loading admin sessions:', e);
tbody.innerHTML = `
<tr>
<td colspan="4" style="text-align: center; color: var(--text-muted);">
Impossible de charger les sessions
</td>
</tr>
`;
}
}
function updateBlockingStats(decisions, alerts) {
const totalBansEl = document.getElementById('totalBansToday');
const blockedEl = document.getElementById('totalBlockedAttempts');
const topScenarioEl = document.getElementById('topScenario');
const uniqueIPsEl = document.getElementById('uniqueIPs');
// Calculate stats from decisions and alerts
const decisionsList = Array.isArray(decisions) ? decisions : [];
const alertsList = Array.isArray(alerts) ? alerts : [];
// Today's date
const today = new Date();
today.setHours(0, 0, 0, 0);
// Count today's bans
const todayBans = decisionsList.filter(d => {
if (d.created_at) {
const created = new Date(d.created_at);
return created >= today;
}
return true; // Count all if no date
}).length;
// Total blocked attempts (sum of events from alerts)
const totalBlocked = alertsList.reduce((sum, alert) => {
return sum + (alert.events_count || 1);
}, 0);
// Find top scenario
const scenarioCounts = {};
alertsList.forEach(alert => {
const scenario = alert.scenario || 'unknown';
scenarioCounts[scenario] = (scenarioCounts[scenario] || 0) + 1;
});
let topScenario = '--';
let maxCount = 0;
Object.entries(scenarioCounts).forEach(([scenario, count]) => {
if (count > maxCount) {
maxCount = count;
// Extract short name from scenario (e.g., "crowdsecurity/ssh-bf" -> "ssh-bf")
const parts = scenario.split('/');
topScenario = parts[parts.length - 1] || scenario;
}
});
// Count unique IPs
const uniqueIPs = new Set();
decisionsList.forEach(d => {
if (d.value) uniqueIPs.add(d.value);
});
alertsList.forEach(a => {
if (a.source && a.source.ip) uniqueIPs.add(a.source.ip);
});
// Update UI
totalBansEl.textContent = todayBans;
blockedEl.textContent = totalBlocked || '--';
topScenarioEl.textContent = topScenario.length > 10 ? topScenario.substring(0, 10) + '...' : topScenario;
topScenarioEl.title = topScenario; // Full name on hover
uniqueIPsEl.textContent = uniqueIPs.size;
}
async function loadSystemLogs() {
const container = document.getElementById('logsContainer');