diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/api.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/api.js index 8494c43c..c3f680e7 100644 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/api.js +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/api.js @@ -41,11 +41,6 @@ var callParental = rpc.declare({ expect: { } }); -var callPortal = rpc.declare({ - object: 'luci.client-guardian', - method: 'portal', - expect: { } -}); var callAlerts = rpc.declare({ object: 'luci.client-guardian', @@ -95,13 +90,6 @@ var callUpdateZone = rpc.declare({ expect: { success: false } }); -var callUpdatePortal = rpc.declare({ - object: 'luci.client-guardian', - method: 'update_portal', - params: ['title', 'subtitle', 'accent_color'], - expect: { success: false } -}); - var callSendTestAlert = rpc.declare({ object: 'luci.client-guardian', method: 'send_test_alert', @@ -109,13 +97,6 @@ var callSendTestAlert = rpc.declare({ expect: { success: false } }); -// Nodogsplash Captive Portal Methods -var callListSessions = rpc.declare({ - object: 'luci.client-guardian', - method: 'list_sessions', - expect: { sessions: [] } -}); - var callGetPolicy = rpc.declare({ object: 'luci.client-guardian', method: 'get_policy', @@ -125,21 +106,13 @@ var callGetPolicy = rpc.declare({ var callSetPolicy = rpc.declare({ object: 'luci.client-guardian', method: 'set_policy', - params: ['policy', 'portal_enabled', 'auto_approve', 'session_timeout'], + params: ['policy', 'auto_approve', 'session_timeout'], expect: { success: false } }); -var callAuthorizeClient = rpc.declare({ +var callSyncZones = rpc.declare({ object: 'luci.client-guardian', - method: 'authorize_client', - params: ['mac', 'ip'], - expect: { success: false } -}); - -var callDeauthorizeClient = rpc.declare({ - object: 'luci.client-guardian', - method: 'deauthorize_client', - params: ['mac', 'ip'], + method: 'sync_zones', expect: { success: false } }); @@ -165,6 +138,42 @@ function formatBytes(bytes) { return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i]; } +function getDeviceIcon(hostname, mac) { + hostname = (hostname || '').toLowerCase(); + mac = (mac || '').toLowerCase(); + + // Mobile devices + if (hostname.match(/android|iphone|ipad|mobile|phone|samsung|xiaomi|huawei/)) + return '📱'; + + // Computers + if (hostname.match(/pc|laptop|desktop|macbook|imac|windows|linux|ubuntu/)) + return '💻'; + + // IoT devices + if (hostname.match(/camera|bulb|switch|sensor|thermostat|doorbell|lock/)) + return '📷'; + + // Smart TV / Media + if (hostname.match(/tv|roku|chromecast|firestick|appletv|media/)) + return '📺'; + + // Gaming + if (hostname.match(/playstation|xbox|nintendo|switch|steam/)) + return '🎮'; + + // Network equipment + if (hostname.match(/router|switch|ap|access[-_]?point|bridge/)) + return '🌐'; + + // Printers + if (hostname.match(/printer|print|hp-|canon-|epson-/)) + return '🖨️'; + + // Default + return '🔌'; +} + return baseclass.extend({ // Core methods getStatus: callStatus, @@ -172,7 +181,6 @@ return baseclass.extend({ getClient: callGetClient, getZones: callZones, getParental: callParental, - getPortal: callPortal, getAlerts: callAlerts, getLogs: callLogs, @@ -184,18 +192,14 @@ return baseclass.extend({ // Configuration updateZone: callUpdateZone, - updatePortal: callUpdatePortal, sendTestAlert: callSendTestAlert, - - // Nodogsplash Captive Portal - listSessions: callListSessions, + syncZones: callSyncZones, getPolicy: callGetPolicy, setPolicy: callSetPolicy, - authorizeClient: callAuthorizeClient, - deauthorizeClient: callDeauthorizeClient, // Utility functions formatMac: formatMac, formatDuration: formatDuration, - formatBytes: formatBytes + formatBytes: formatBytes, + getDeviceIcon: getDeviceIcon }); diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/dashboard.css b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/dashboard.css index dedcfb43..ef01291e 100644 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/dashboard.css +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/dashboard.css @@ -9,41 +9,50 @@ --cg-bg-tertiary: #251a1a; --cg-border: #3d2828; --cg-border-light: #4a3333; - + --cg-text-primary: #fafafa; --cg-text-secondary: #b8a8a8; --cg-text-muted: #8a7575; - - --cg-accent-red: #ef4444; + + /* SecuBox Brand Colors (Indigo/Purple) */ + --cg-primary: #6366f1; + --cg-primary-end: #8b5cf6; + + /* Accent Colors */ --cg-accent-orange: #f97316; --cg-accent-amber: #f59e0b; --cg-accent-green: #22c55e; --cg-accent-blue: #3b82f6; --cg-accent-purple: #8b5cf6; --cg-accent-cyan: #06b6d4; - - --cg-danger: #dc2626; + + /* State Colors */ + --cg-danger: #ef4444; --cg-warning: #f59e0b; --cg-success: #16a34a; --cg-info: #0284c7; - - --cg-gradient: linear-gradient(135deg, #ef4444, #dc2626, #b91c1c); - --cg-gradient-soft: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.1)); - + + /* Primary Gradients (SecuBox Indigo/Purple) */ + --cg-gradient: linear-gradient(135deg, #6366f1, #8b5cf6); + --cg-gradient-soft: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.1)); + + /* Zone Colors (Keep as-is) */ --cg-zone-private: #22c55e; --cg-zone-iot: #f59e0b; --cg-zone-kids: #06b6d4; --cg-zone-guest: #8b5cf6; --cg-zone-quarantine: #ef4444; --cg-zone-blocked: #6b7280; - + + /* Typography */ --cg-font-mono: 'JetBrains Mono', 'Fira Code', monospace; --cg-font-sans: 'Inter', -apple-system, sans-serif; - + + /* Layout */ --cg-radius: 8px; --cg-radius-lg: 12px; --cg-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); - --cg-shadow-glow: 0 0 30px rgba(239, 68, 68, 0.3); + --cg-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.3); } /* Base */ @@ -164,14 +173,14 @@ } .cg-status-badge.approved { - background: rgba(34, 197, 94, 0.15); - color: var(--cg-accent-green); - border: 1px solid rgba(34, 197, 94, 0.3); + background: rgba(99, 102, 241, 0.15); + color: var(--cg-primary); + border: 1px solid rgba(99, 102, 241, 0.3); } .cg-status-badge.quarantine { background: rgba(239, 68, 68, 0.15); - color: var(--cg-accent-red); + color: var(--cg-danger); border: 1px solid rgba(239, 68, 68, 0.3); } @@ -259,8 +268,8 @@ } .cg-client-item:hover { - border-color: var(--cg-accent-red); - background: rgba(239, 68, 68, 0.05); + border-color: var(--cg-primary); + background: rgba(99, 102, 241, 0.05); } .cg-client-item.online { @@ -273,7 +282,7 @@ } .cg-client-item.quarantine { - border-left: 3px solid var(--cg-accent-red); + border-left: 3px solid var(--cg-danger); background: rgba(239, 68, 68, 0.08); } @@ -373,13 +382,37 @@ .cg-client-action:hover { background: var(--cg-bg-tertiary); - border-color: var(--cg-accent-red); + border-color: var(--cg-primary); } .cg-client-action.approve:hover { border-color: var(--cg-accent-green); background: rgba(34, 197, 94, 0.1); } .cg-client-action.ban:hover { border-color: var(--cg-danger); background: rgba(220, 38, 38, 0.1); } .cg-client-action.edit:hover { border-color: var(--cg-accent-blue); background: rgba(59, 130, 246, 0.1); } +/* Threat Indicators */ +.cg-threat-badge { + display: inline-flex; + align-items: center; + justify-content: center; + animation: pulse-threat 2s ease-in-out infinite; +} + +@keyframes pulse-threat { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +.cg-client-item .cg-threat-badge:hover { + animation: none; + transform: scale(1.2); +} + /* Zone Cards */ .cg-zones-grid { display: grid; @@ -515,7 +548,7 @@ transition: background 0.3s; } -.cg-toggle-switch.active { background: var(--cg-accent-red); } +.cg-toggle-switch.active { background: var(--cg-primary); } .cg-toggle-switch::after { content: ''; @@ -554,7 +587,7 @@ .cg-select:focus, .cg-textarea:focus { outline: none; - border-color: var(--cg-accent-red); + border-color: var(--cg-primary); } .cg-textarea { min-height: 100px; resize: vertical; } @@ -584,7 +617,7 @@ transition: all 0.2s; } -.cg-btn:hover { border-color: var(--cg-accent-red); } +.cg-btn:hover { border-color: var(--cg-primary); } .cg-btn-primary { background: var(--cg-gradient); @@ -618,7 +651,7 @@ padding: 16px; background: var(--cg-bg-tertiary); border-radius: var(--cg-radius); - border-left: 4px solid var(--cg-accent-red); + border-left: 4px solid var(--cg-danger); margin-bottom: 16px; } @@ -647,8 +680,8 @@ .cg-schedule-day.active { background: rgba(239, 68, 68, 0.2); - border-color: var(--cg-accent-red); - color: var(--cg-accent-red); + border-color: var(--cg-danger); + color: var(--cg-danger); } .cg-schedule-day-name { font-size: 11px; font-weight: 600; } @@ -729,7 +762,7 @@ .cg-log-level.info { background: rgba(59, 130, 246, 0.15); color: var(--cg-accent-blue); } .cg-log-level.warning { background: rgba(245, 158, 11, 0.15); color: var(--cg-accent-amber); } -.cg-log-level.error { background: rgba(239, 68, 68, 0.15); color: var(--cg-accent-red); } +.cg-log-level.error { background: rgba(239, 68, 68, 0.15); color: var(--cg-danger); } .cg-log-message { flex: 1; color: var(--cg-text-secondary); } @@ -761,10 +794,428 @@ .client-guardian-dashboard ::-webkit-scrollbar-thumb { background: var(--cg-border); border-radius: 4px; } .client-guardian-dashboard ::-webkit-scrollbar-thumb:hover { background: var(--cg-text-muted); } +/* Loading States */ +.cg-status-badge.loading { + opacity: 0.6; +} + +.cg-status-badge.loading .cg-status-dot { + animation: spin 1s linear infinite; +} + +/* Smooth Transitions */ +.cg-stat-value { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.cg-client-item { + transition: all 0.3s ease; +} + +.cg-stat-card.updated { + animation: flash 0.5s ease-in-out; +} + /* Animations */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes flash { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; background: rgba(99, 102, 241, 0.1); } +} + @keyframes client-pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } 50% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } } .cg-client-item.quarantine { animation: client-pulse 2s infinite; } + +/* Wizard Styles */ +.cg-wizard { + max-width: 1400px; + margin: 0 auto; + padding: 24px; +} + +.cg-wizard-header { + text-align: center; + margin-bottom: 48px; +} + +.cg-wizard-icon { + font-size: 72px; + margin-bottom: 16px; + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +.cg-wizard-title { + font-size: 32px; + font-weight: 700; + color: var(--cg-text-primary); + margin: 0 0 8px 0; + background: var(--cg-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.cg-wizard-subtitle { + font-size: 16px; + color: var(--cg-text-secondary); + max-width: 600px; + margin: 0 auto; +} + +.cg-profiles-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); + gap: 24px; + margin-bottom: 32px; +} + +.cg-profile-card { + background: var(--cg-bg-secondary); + border: 2px solid var(--cg-border); + border-radius: 16px; + padding: 24px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.cg-profile-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: var(--cg-gradient); + opacity: 0; + transition: opacity 0.3s; +} + +.cg-profile-card:hover::before { + opacity: 1; +} + +.cg-profile-card:hover { + transform: translateY(-4px); + border-color: var(--cg-primary); + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15); +} + +.cg-profile-icon { + font-size: 48px; + text-align: center; + margin-bottom: 16px; +} + +.cg-profile-name { + font-size: 20px; + font-weight: 600; + color: var(--cg-text-primary); + text-align: center; + margin-bottom: 8px; +} + +.cg-profile-desc { + font-size: 14px; + color: var(--cg-text-secondary); + text-align: center; + margin-bottom: 16px; + min-height: 40px; +} + +.cg-profile-zones { + background: var(--cg-bg-tertiary); + padding: 12px; + border-radius: 8px; + margin-bottom: 16px; +} + +.cg-profile-zones strong { + display: block; + margin-bottom: 8px; + font-size: 12px; + text-transform: uppercase; + color: var(--cg-text-muted); +} + +.cg-profile-zone-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.cg-profile-zone-badge { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + color: white; +} + +.cg-profile-more { + font-size: 12px; + color: var(--cg-text-muted); + font-style: italic; +} + +.cg-profile-btn { + width: 100%; + justify-content: center; +} + +.cg-wizard-footer { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 8px; + padding: 16px 20px; +} + +.cg-wizard-note { + color: var(--cg-text-secondary); + font-size: 14px; +} + +.cg-modal-profile { + padding: 16px; +} + +.cg-modal-profile h3 { + text-align: center; + color: var(--cg-primary); + margin-bottom: 8px; +} + +.cg-modal-profile p { + text-align: center; + color: var(--cg-text-secondary); + margin-bottom: 16px; +} + +.cg-modal-profile ul { + list-style: none; + padding: 0; +} + +.cg-modal-profile li { + padding: 8px 0; + border-bottom: 1px solid var(--cg-border); +} + +.cg-modal-profile li:last-child { + border-bottom: none; +} + +.spinner { + border: 4px solid rgba(99, 102, 241, 0.1); + border-top: 4px solid var(--cg-primary); + border-radius: 50%; + width: 48px; + height: 48px; + animation: spin 1s linear infinite; + margin: 0 auto; +} + +/* Debug Interface Styles */ +.cg-debug-controls { + display: flex; + gap: 8px; +} + +.cg-btn-sm { + padding: 6px 12px; + font-size: 13px; +} + +.cg-debug-status-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; +} + +.cg-debug-status-item { + display: flex; + flex-direction: column; + gap: 8px; +} + +.cg-debug-status-label { + font-size: 12px; + text-transform: uppercase; + color: var(--cg-text-muted); + font-weight: 600; +} + +.cg-debug-status-value { + font-size: 14px; + color: var(--cg-text-primary); + display: flex; + align-items: center; + gap: 8px; +} + +.cg-input-sm { + padding: 6px 10px; + font-size: 13px; +} + +.cg-system-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 12px; +} + +.cg-info-item { + display: flex; + padding: 8px 0; + border-bottom: 1px solid var(--cg-border); +} + +.cg-info-label { + font-weight: 600; + color: var(--cg-text-secondary); + min-width: 140px; +} + +.cg-info-value { + color: var(--cg-text-primary); + word-break: break-word; +} + +.cg-log-container { + max-height: 600px; + overflow-y: auto; + border: 1px solid var(--cg-border); + border-radius: 8px; + background: var(--cg-bg-tertiary); +} + +.cg-log-entry { + padding: 12px; + border-bottom: 1px solid var(--cg-border); + font-family: var(--cg-font-mono); + font-size: 13px; + transition: background 0.2s; +} + +.cg-log-entry:hover { + background: rgba(99, 102, 241, 0.05); +} + +.cg-log-entry:last-child { + border-bottom: none; +} + +.cg-log-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.cg-log-icon { + font-size: 16px; +} + +.cg-log-level { + font-weight: 700; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; +} + +.cg-log-error .cg-log-level { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + +.cg-log-warn .cg-log-level { + background: rgba(245, 158, 11, 0.15); + color: #f59e0b; +} + +.cg-log-info .cg-log-level { + background: rgba(59, 130, 246, 0.15); + color: #3b82f6; +} + +.cg-log-debug .cg-log-level { + background: rgba(139, 92, 246, 0.15); + color: #8b5cf6; +} + +.cg-log-trace .cg-log-level { + background: rgba(107, 114, 128, 0.15); + color: #6b7280; +} + +.cg-log-time { + color: var(--cg-text-muted); + font-size: 11px; + margin-left: auto; +} + +.cg-log-message { + color: var(--cg-text-primary); + margin-bottom: 4px; + line-height: 1.5; +} + +.cg-log-details { + margin-top: 8px; +} + +.cg-log-details summary { + cursor: pointer; + color: var(--cg-primary); + font-size: 12px; + user-select: none; +} + +.cg-log-details summary:hover { + text-decoration: underline; +} + +.cg-log-data { + margin-top: 8px; + padding: 12px; + background: var(--cg-bg-primary); + border-radius: 6px; + overflow-x: auto; + font-size: 12px; + color: var(--cg-text-secondary); + line-height: 1.6; +} + +/* Scrollbar styling for log container */ +.cg-log-container::-webkit-scrollbar { + width: 8px; +} + +.cg-log-container::-webkit-scrollbar-track { + background: var(--cg-bg-secondary); + border-radius: 4px; +} + +.cg-log-container::-webkit-scrollbar-thumb { + background: var(--cg-border); + border-radius: 4px; +} + +.cg-log-container::-webkit-scrollbar-thumb:hover { + background: var(--cg-primary); +} diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/captive.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/captive.js deleted file mode 100644 index fc2c9f0d..00000000 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/captive.js +++ /dev/null @@ -1,257 +0,0 @@ -'use strict'; -'require view'; -'require secubox-theme/theme as Theme'; -'require dom'; -'require poll'; -'require ui'; -'require client-guardian/api as API'; - -return view.extend({ - load: function() { - return Promise.all([ - API.listSessions(), - API.getPolicy(), - API.getStatus() - ]); - }, - - render: function(data) { - var sessions = data[0] || {}; - var policy = data[1] || {}; - var status = data[2] || {}; - - var sessionList = sessions.sessions || []; - var nds = sessions.nodogsplash || {}; - - var view = E('div', { 'class': 'cbi-map' }, [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('h2', {}, _('Captive Portal Management')), - - // Nodogsplash Status Card - E('div', { 'class': 'cbi-section', 'style': 'background: ' + (nds.running ? '#d4edda' : '#f8d7da') + '; border-left: 4px solid ' + (nds.running ? '#28a745' : '#dc3545') + '; padding: 1em; margin-bottom: 1em;' }, [ - E('h3', { 'style': 'margin-top: 0;' }, _('Nodogsplash Status')), - E('div', { 'style': 'display: flex; gap: 2em; align-items: center;' }, [ - E('div', {}, [ - E('strong', {}, _('Service:')), - ' ', - E('span', { 'class': 'badge', 'style': 'background: ' + (nds.running ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' }, - nds.running ? _('RUNNING') : _('STOPPED')) - ]), - E('div', {}, [ - E('strong', {}, _('Active Sessions:')), - ' ', - E('span', { 'style': 'font-size: 1.5em; color: #0088cc; font-weight: bold;' }, sessionList.length.toString()) - ]), - E('div', {}, [ - E('strong', {}, _('Default Policy:')), - ' ', - E('span', { 'class': 'badge', 'style': 'background: #0088cc; color: white; padding: 0.25em 0.6em; border-radius: 3px;' }, - policy.default_policy || 'captive') - ]) - ]), - !nds.running ? E('p', { 'style': 'margin: 1em 0 0 0; color: #856404; background: #fff3cd; padding: 0.75em; border-radius: 4px;' }, [ - E('strong', {}, _('Note:')), - ' ', - _('Nodogsplash is not running. Start the service to enable captive portal functionality.') - ]) : null - ]), - - // Active Sessions Table - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [ - E('h3', { 'style': 'margin: 0;' }, _('Active Portal Sessions')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleRefresh, this) - }, _('Refresh')) - ]), - - E('div', { 'class': 'table-wrapper' }, [ - E('table', { 'class': 'table', 'id': 'sessions-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, _('MAC Address')), - E('th', {}, _('IP Address')), - E('th', {}, _('Hostname')), - E('th', {}, _('Duration')), - E('th', { 'style': 'text-align: right;' }, _('Downloaded')), - E('th', { 'style': 'text-align: right;' }, _('Uploaded')), - E('th', {}, _('State')), - E('th', { 'class': 'cbi-section-actions' }, _('Actions')) - ]) - ]), - E('tbody', { 'id': 'sessions-tbody' }, - this.renderSessionRows(sessionList) - ) - ]) - ]) - ]), - - // Help Section - E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 2em;' }, [ - E('h3', {}, _('Captive Portal Information')), - E('p', {}, _('The captive portal intercepts new clients and requires them to authenticate before accessing the network. Sessions are managed by nodogsplash.')), - E('ul', {}, [ - E('li', {}, _('Active sessions show clients currently authenticated through the portal')), - E('li', {}, _('Use "Deauthorize" to end a session and force re-authentication')), - E('li', {}, _('Configure portal settings in the Portal tab')), - E('li', {}, _('Change default policy in Settings to control portal behavior')) - ]) - ]) - ]); - - // Setup auto-refresh - poll.add(L.bind(function() { - return API.listSessions().then(L.bind(function(refreshData) { - var tbody = document.getElementById('sessions-tbody'); - if (tbody) { - var refreshedSessions = refreshData.sessions || []; - dom.content(tbody, this.renderSessionRows(refreshedSessions)); - } - }, this)); - }, this), 5); - - return view; - }, - - renderSessionRows: function(sessions) { - if (!sessions || sessions.length === 0) { - return E('tr', {}, [ - E('td', { 'colspan': 8, 'style': 'text-align: center; padding: 2em; color: #999;' }, - _('No active captive portal sessions')) - ]); - } - - return sessions.map(L.bind(function(session) { - return E('tr', {}, [ - E('td', {}, [ - E('code', { 'style': 'font-size: 0.9em;' }, session.mac || 'N/A') - ]), - E('td', {}, session.ip || 'N/A'), - E('td', {}, session.hostname || 'Unknown'), - E('td', {}, this.formatDuration(session.duration || 0)), - E('td', { 'style': 'text-align: right; font-family: monospace;' }, - this.formatBytes(session.downloaded || 0)), - E('td', { 'style': 'text-align: right; font-family: monospace;' }, - this.formatBytes(session.uploaded || 0)), - E('td', {}, [ - E('span', { - 'class': 'badge', - 'style': 'background: #28a745; color: white; padding: 0.25em 0.6em; border-radius: 3px;' - }, session.state || 'authenticated') - ]), - E('td', { 'class': 'cbi-section-actions' }, [ - E('button', { - 'class': 'cbi-button cbi-button-negative cbi-button-remove', - 'click': L.bind(function(ev) { - this.handleDeauth(ev, session.mac, session.ip, session.hostname); - }, this) - }, _('Deauthorize')) - ]) - ]); - }, this)); - }, - - formatDuration: function(seconds) { - if (!seconds || seconds === 0) return '0s'; - - var hours = Math.floor(seconds / 3600); - var minutes = Math.floor((seconds % 3600) / 60); - var secs = seconds % 60; - - var parts = []; - if (hours > 0) parts.push(hours + 'h'); - if (minutes > 0) parts.push(minutes + 'm'); - if (secs > 0 || parts.length === 0) parts.push(secs + 's'); - - return parts.join(' '); - }, - - formatBytes: function(bytes) { - if (!bytes || bytes === 0) return '0 B'; - - var units = ['B', 'KB', 'MB', 'GB']; - var i = Math.floor(Math.log(bytes) / Math.log(1024)); - i = Math.min(i, units.length - 1); - - return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i]; - }, - - handleDeauth: function(ev, mac, ip, hostname) { - var btn = ev.target; - - ui.showModal(_('Deauthorize Client'), [ - E('p', {}, _('Are you sure you want to deauthorize this client?')), - E('div', { 'style': 'background: #f8f9fa; padding: 1em; margin: 1em 0; border-radius: 4px;' }, [ - E('div', {}, [E('strong', {}, _('Hostname:')), ' ', hostname || 'Unknown']), - E('div', {}, [E('strong', {}, _('MAC:')), ' ', E('code', {}, mac)]), - E('div', {}, [E('strong', {}, _('IP:')), ' ', ip || 'N/A']) - ]), - E('p', {}, _('The client will be immediately disconnected and must re-authenticate through the captive portal.')), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - ' ', - E('button', { - 'class': 'btn cbi-button-negative', - 'click': L.bind(function() { - ui.hideModal(); - this.deauthorize(mac, ip, btn); - }, this) - }, _('Deauthorize')) - ]) - ]); - }, - - deauthorize: function(mac, ip, btn) { - btn.disabled = true; - btn.textContent = _('Deauthorizing...'); - - API.deauthorizeClient(mac, ip).then(L.bind(function(result) { - if (result.success) { - ui.addNotification(null, - E('p', _('Client %s has been deauthorized').format(mac)), - 'info' - ); - - // Refresh the table - this.handleRefresh(); - } else { - ui.addNotification(null, - E('p', _('Failed to deauthorize client: %s').format(result.error || 'Unknown error')), - 'error' - ); - btn.disabled = false; - btn.textContent = _('Deauthorize'); - } - }, this)).catch(function(err) { - ui.addNotification(null, - E('p', _('Error: %s').format(err.message || err)), - 'error' - ); - btn.disabled = false; - btn.textContent = _('Deauthorize'); - }); - }, - - handleRefresh: function() { - poll.start(); - - return Promise.all([ - API.listSessions(), - API.getPolicy() - ]).then(L.bind(function(data) { - var tbody = document.getElementById('sessions-tbody'); - if (tbody) { - var sessions = data[0].sessions || []; - dom.content(tbody, this.renderSessionRows(sessions)); - } - }, this)); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/clients.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/clients.js index 7b7c02d9..12864f0e 100644 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/clients.js +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/clients.js @@ -15,8 +15,8 @@ return view.extend({ }, render: function(data) { - var clients = data[0].clients || []; - var zones = data[1].zones || []; + var clients = Array.isArray(data[0]) ? data[0] : (data[0].clients || []); + var zones = Array.isArray(data[1]) ? data[1] : (data[1].zones || []); var self = this; var view = E('div', { 'class': 'client-guardian-dashboard' }, [ @@ -188,15 +188,16 @@ return view.extend({ ]), E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [ E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')), - E('button', { 'class': 'cg-btn cg-btn-success', 'click': function() { + E('button', { 'class': 'cg-btn cg-btn-success', 'click': L.bind(function() { var name = document.getElementById('approve-name').value; var zone = document.getElementById('approve-zone').value; var notes = document.getElementById('approve-notes').value; - api.approveClient(mac, name, zone, notes).then(function() { + api.approveClient(mac, name, zone, notes).then(L.bind(function() { ui.hideModal(); - window.location.reload(); - }); - }}, _('Approuver')) + ui.addNotification(null, E('p', _('Client approved successfully')), 'success'); + this.handleRefresh(); + }, this)); + }, this)}, _('Approuver')) ]) ]); }, @@ -229,7 +230,7 @@ return view.extend({ ]), E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [ E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')), - E('button', { 'class': 'cg-btn cg-btn-primary', 'click': function() { + E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() { api.updateClient( client.section, document.getElementById('edit-name').value, @@ -237,11 +238,12 @@ return view.extend({ document.getElementById('edit-notes').value, parseInt(document.getElementById('edit-quota').value) || 0, document.getElementById('edit-ip').value - ).then(function() { + ).then(L.bind(function() { ui.hideModal(); - window.location.reload(); - }); - }}, _('Enregistrer')) + ui.addNotification(null, E('p', _('Client updated successfully')), 'success'); + this.handleRefresh(); + }, this)); + }, this)}, _('Enregistrer')) ]) ]); }, @@ -258,26 +260,39 @@ return view.extend({ ]), E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [ E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')), - E('button', { 'class': 'cg-btn cg-btn-danger', 'click': function() { + E('button', { 'class': 'cg-btn cg-btn-danger', 'click': L.bind(function() { var reason = document.getElementById('ban-reason').value || 'Manual ban'; - api.banClient(mac, reason).then(function() { + api.banClient(mac, reason).then(L.bind(function() { ui.hideModal(); - window.location.reload(); - }); - }}, _('Bannir')) + ui.addNotification(null, E('p', _('Client banned successfully')), 'info'); + this.handleRefresh(); + }, this)); + }, this)}, _('Bannir')) ]) ]); }, handleUnban: function(ev) { var mac = ev.currentTarget.dataset.mac; - api.quarantineClient(mac).then(function() { - window.location.reload(); - }); + api.quarantineClient(mac).then(L.bind(function() { + ui.addNotification(null, E('p', _('Client unbanned successfully')), 'success'); + this.handleRefresh(); + }, this)); }, handleRefresh: function() { - window.location.reload(); + return Promise.all([ + api.getClients(), + api.getZones() + ]).then(L.bind(function(data) { + var container = document.querySelector('.client-guardian-dashboard'); + if (container) { + var newView = this.render(data); + dom.content(container.parentNode, newView); + } + }, this)).catch(function(err) { + console.error('Failed to refresh clients list:', err); + }); }, handleSaveApply: null, diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/overview.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/overview.js index 8e6d635b..c38383b2 100644 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/overview.js +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/overview.js @@ -12,14 +12,15 @@ return view.extend({ return Promise.all([ api.getStatus(), api.getClients(), - api.getZones() + api.getZones(), + uci.load('client-guardian') ]); }, render: function(data) { var status = data[0]; - var clients = data[1].clients || []; - var zones = data[2].zones || []; + var clients = Array.isArray(data[1]) ? data[1] : (data[1].clients || []); + var zones = Array.isArray(data[2]) ? data[2] : (data[2].zones || []); var onlineClients = clients.filter(function(c) { return c.online; }); var approvedClients = clients.filter(function(c) { return c.status === 'approved'; }); @@ -51,8 +52,8 @@ return view.extend({ this.renderStatCard('✅', approvedClients.length, 'Approuvés'), this.renderStatCard('⏳', quarantineClients.length, 'Quarantaine'), this.renderStatCard('🚫', bannedClients.length, 'Bannis'), - this.renderStatCard('🌐', zones.length, 'Zones'), - this.renderStatCard('🔔', status.alerts_today || 0, 'Alertes Aujourd\'hui') + this.renderStatCard('⚠️', clients.filter(function(c) { return c.has_threats; }).length, 'Menaces Actives'), + this.renderStatCard('🌐', zones.length, 'Zones') ]), // Recent Clients Card @@ -88,6 +89,16 @@ return view.extend({ ]) : E('div') ]); + // Setup auto-refresh polling based on UCI settings + var autoRefresh = uci.get('client-guardian', 'config', 'auto_refresh'); + var refreshInterval = parseInt(uci.get('client-guardian', 'config', 'refresh_interval') || '10'); + + if (autoRefresh === '1') { + poll.add(L.bind(function() { + return this.handleRefresh(); + }, this), refreshInterval); + } + return view; }, @@ -114,11 +125,19 @@ return view.extend({ E('div', { 'class': 'cg-client-info' }, [ E('div', { 'class': 'cg-client-name' }, [ client.online ? E('span', { 'class': 'online-indicator' }) : E('span'), - client.name || client.hostname || 'Unknown' + client.name || client.hostname || 'Unknown', + client.has_threats ? E('span', { + 'class': 'cg-threat-badge', + 'title': (client.threat_count || 0) + ' menace(s) active(s), score de risque: ' + (client.risk_score || 0), + 'style': 'margin-left: 8px; color: #ef4444; font-size: 16px; cursor: help;' + }, '⚠️') : E('span') ]), E('div', { 'class': 'cg-client-meta' }, [ E('span', {}, client.mac), - E('span', {}, client.ip || 'N/A') + E('span', {}, client.ip || 'N/A'), + client.has_threats ? E('span', { + 'style': 'color: #ef4444; font-weight: 500; margin-left: 8px;' + }, 'Risque: ' + (client.risk_score || 0) + '%') : E('span') ]) ]), E('span', { 'class': 'cg-client-zone ' + zoneClass }, client.zone || 'unknown'), @@ -176,13 +195,14 @@ return view.extend({ }, _('Annuler')), E('button', { 'class': 'cg-btn cg-btn-success', - 'click': function() { + 'click': L.bind(function() { var zone = document.getElementById('approve-zone').value; - api.approveClient(mac, '', zone, '').then(function() { + api.approveClient(mac, '', zone, '').then(L.bind(function() { ui.hideModal(); - window.location.reload(); - }); - } + ui.addNotification(null, E('p', _('Client approved successfully')), 'success'); + this.handleRefresh(); + }, this)); + }, this) }, _('Approuver')) ]) ]); @@ -201,17 +221,52 @@ return view.extend({ }, _('Annuler')), E('button', { 'class': 'cg-btn cg-btn-danger', - 'click': function() { - api.banClient(mac, 'Manual ban').then(function() { + 'click': L.bind(function() { + api.banClient(mac, 'Manual ban').then(L.bind(function() { ui.hideModal(); - window.location.reload(); - }); - } + ui.addNotification(null, E('p', _('Client banned successfully')), 'info'); + this.handleRefresh(); + }, this)); + }, this) }, _('Bannir')) ]) ]); }, + handleRefresh: function() { + return Promise.all([ + api.getStatus(), + api.getClients(), + api.getZones() + ]).then(L.bind(function(data) { + // Update dashboard without full page reload + var container = document.querySelector('.client-guardian-dashboard'); + if (container) { + // Show loading indicator + var statusBadge = document.querySelector('.cg-status-badge'); + if (statusBadge) { + statusBadge.classList.add('loading'); + } + + // Reconstruct data array (status, clients, zones, uci already loaded) + var newView = this.render(data); + dom.content(container.parentNode, newView); + + // Remove loading indicator + if (statusBadge) { + statusBadge.classList.remove('loading'); + } + } + }, this)).catch(function(err) { + console.error('Failed to refresh Client Guardian dashboard:', err); + }); + }, + + handleLeave: function() { + // Stop polling when leaving the view to prevent memory leaks + poll.stop(); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/portal.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/portal.js deleted file mode 100644 index e21aec01..00000000 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/portal.js +++ /dev/null @@ -1,215 +0,0 @@ -'use strict'; -'require view'; -'require secubox-theme/theme as Theme'; -'require dom'; -'require ui'; -'require client-guardian.api as api'; - -return view.extend({ - load: function() { - return api.getPortal(); - }, - - render: function(data) { - var portal = data; - var self = this; - - return E('div', { 'class': 'client-guardian-dashboard' }, [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('client-guardian/dashboard.css') }), - - E('div', { 'class': 'cg-header' }, [ - E('div', { 'class': 'cg-logo' }, [ - E('div', { 'class': 'cg-logo-icon' }, '🚪'), - E('div', { 'class': 'cg-logo-text' }, 'Portail Captif') - ]), - E('div', { 'class': 'cg-status-badge ' + (portal.enabled ? 'approved' : 'offline') }, [ - E('span', { 'class': 'cg-status-dot' }), - portal.enabled ? 'Actif' : 'Inactif' - ]) - ]), - - // Configuration - E('div', { 'class': 'cg-card' }, [ - E('div', { 'class': 'cg-card-header' }, [ - E('div', { 'class': 'cg-card-title' }, [ - E('span', { 'class': 'cg-card-title-icon' }, '⚙️'), - 'Configuration' - ]) - ]), - E('div', { 'class': 'cg-card-body' }, [ - E('div', { 'class': 'cg-toggle' }, [ - E('div', { 'class': 'cg-toggle-info' }, [ - E('span', { 'class': 'cg-toggle-icon' }, '🚪'), - E('div', {}, [ - E('div', { 'class': 'cg-toggle-label' }, 'Portail Captif'), - E('div', { 'class': 'cg-toggle-desc' }, 'Activer pour les zones Guest et Quarantine') - ]) - ]), - E('div', { - 'class': 'cg-toggle-switch' + (portal.enabled ? ' active' : ''), - 'id': 'toggle-portal', - 'click': function() { this.classList.toggle('active'); } - }) - ]), - E('div', { 'class': 'cg-toggle' }, [ - E('div', { 'class': 'cg-toggle-info' }, [ - E('span', { 'class': 'cg-toggle-icon' }, '📝'), - E('div', {}, [ - E('div', { 'class': 'cg-toggle-label' }, 'Conditions d\'utilisation'), - E('div', { 'class': 'cg-toggle-desc' }, 'Exiger l\'acceptation des CGU') - ]) - ]), - E('div', { - 'class': 'cg-toggle-switch' + (portal.require_terms ? ' active' : ''), - 'id': 'toggle-terms', - 'click': function() { this.classList.toggle('active'); } - }) - ]), - E('div', { 'class': 'cg-toggle' }, [ - E('div', { 'class': 'cg-toggle-info' }, [ - E('span', { 'class': 'cg-toggle-icon' }, '📧'), - E('div', {}, [ - E('div', { 'class': 'cg-toggle-label' }, 'Inscription'), - E('div', { 'class': 'cg-toggle-desc' }, 'Permettre l\'auto-inscription (avec approbation)') - ]) - ]), - E('div', { - 'class': 'cg-toggle-switch' + (portal.allow_registration ? ' active' : ''), - 'id': 'toggle-registration', - 'click': function() { this.classList.toggle('active'); } - }) - ]), - - E('div', { 'class': 'cg-form-group', 'style': 'margin-top: 20px' }, [ - E('label', { 'class': 'cg-form-label' }, 'Titre du Portail'), - E('input', { - 'type': 'text', - 'id': 'portal-title', - 'class': 'cg-input', - 'value': portal.title || 'Bienvenue sur le Réseau' - }) - ]), - E('div', { 'class': 'cg-form-group' }, [ - E('label', { 'class': 'cg-form-label' }, 'Sous-titre'), - E('input', { - 'type': 'text', - 'id': 'portal-subtitle', - 'class': 'cg-input', - 'value': portal.subtitle || 'Veuillez vous identifier pour accéder à Internet' - }) - ]), - E('div', { 'class': 'cg-form-group' }, [ - E('label', { 'class': 'cg-form-label' }, 'Méthode d\'authentification'), - E('select', { 'id': 'portal-auth', 'class': 'cg-input' }, [ - E('option', { 'value': 'password', 'selected': portal.auth_method === 'password' }, 'Mot de passe unique'), - E('option', { 'value': 'voucher', 'selected': portal.auth_method === 'voucher' }, 'Codes voucher'), - E('option', { 'value': 'click', 'selected': portal.auth_method === 'click' }, 'Click-through (acceptation CGU)') - ]) - ]), - E('div', { 'class': 'cg-form-group' }, [ - E('label', { 'class': 'cg-form-label' }, 'Mot de passe Invité'), - E('input', { - 'type': 'text', - 'id': 'portal-password', - 'class': 'cg-input', - 'value': portal.guest_password || 'guest2024' - }) - ]), - E('div', { 'class': 'cg-form-group' }, [ - E('label', { 'class': 'cg-form-label' }, 'Couleur d\'accent'), - E('input', { - 'type': 'color', - 'id': 'portal-color', - 'class': 'cg-input', - 'style': 'width: 80px; height: 40px; padding: 4px', - 'value': portal.accent_color || '#ef4444' - }) - ]), - - E('div', { 'class': 'cg-btn-group' }, [ - E('button', { - 'class': 'cg-btn cg-btn-primary', - 'click': L.bind(this.handleSavePortal, this) - }, [ - E('span', {}, '💾'), - ' Enregistrer' - ]) - ]) - ]) - ]), - - // Portal Preview - E('div', { 'class': 'cg-card' }, [ - E('div', { 'class': 'cg-card-header' }, [ - E('div', { 'class': 'cg-card-title' }, [ - E('span', { 'class': 'cg-card-title-icon' }, '👁️'), - 'Aperçu du Portail' - ]) - ]), - E('div', { 'class': 'cg-card-body' }, [ - E('div', { 'class': 'cg-portal-preview', 'id': 'portal-preview' }, [ - E('div', { - 'class': 'cg-portal-preview-logo', - 'style': 'background: ' + (portal.accent_color || '#ef4444') - }, '🛡️'), - E('div', { 'class': 'cg-portal-preview-title', 'id': 'preview-title' }, - portal.title || 'Bienvenue sur le Réseau' - ), - E('div', { 'class': 'cg-portal-preview-subtitle', 'id': 'preview-subtitle' }, - portal.subtitle || 'Veuillez vous identifier pour accéder à Internet' - ), - E('input', { - 'type': 'password', - 'class': 'cg-portal-preview-input', - 'placeholder': 'Mot de passe invité' - }), - E('button', { - 'class': 'cg-portal-preview-btn', - 'id': 'preview-btn', - 'style': 'background: ' + (portal.accent_color || '#ef4444') - }, 'Se Connecter') - ]) - ]) - ]), - - // Active Sessions - E('div', { 'class': 'cg-card' }, [ - E('div', { 'class': 'cg-card-header' }, [ - E('div', { 'class': 'cg-card-title' }, [ - E('span', { 'class': 'cg-card-title-icon' }, '👥'), - 'Sessions Actives' - ]), - E('span', { 'class': 'cg-card-badge' }, (portal.active_sessions || 0) + ' sessions') - ]), - E('div', { 'class': 'cg-card-body' }, [ - portal.active_sessions > 0 ? - E('p', {}, 'Liste des sessions actives...') : - E('div', { 'class': 'cg-empty-state' }, [ - E('div', { 'class': 'cg-empty-state-icon' }, '🔒'), - E('div', { 'class': 'cg-empty-state-title' }, 'Aucune session active'), - E('div', { 'class': 'cg-empty-state-text' }, 'Les sessions du portail captif apparaîtront ici') - ]) - ]) - ]) - ]); - }, - - handleSavePortal: function(ev) { - var title = document.getElementById('portal-title').value; - var subtitle = document.getElementById('portal-subtitle').value; - var auth = document.getElementById('portal-auth').value; - var password = document.getElementById('portal-password').value; - var color = document.getElementById('portal-color').value; - - api.updatePortal(title, subtitle, color, auth, password).then(function(res) { - if (res.success) { - ui.addNotification(null, E('p', {}, _('Configuration du portail enregistrée')), 'success'); - } - }); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/settings.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/settings.js index 79e452c6..4383105f 100644 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/settings.js +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/settings.js @@ -22,7 +22,7 @@ return view.extend({ var m, s, o; m = new form.Map('client-guardian', _('Client Guardian Settings'), - _('Configure default network access policy and captive portal behavior.')); + _('Configure default network access policy and client management.')); // General Settings s = m.section(form.NamedSection, 'config', 'client-guardian', _('General Settings')); @@ -34,9 +34,9 @@ return view.extend({ o = s.option(form.ListValue, 'default_policy', _('Default Policy')); o.value('open', _('Open - Allow all clients')); - o.value('captive', _('Captive Portal - Require portal authentication')); + o.value('quarantine', _('Quarantine - Require approval')); o.value('whitelist', _('Whitelist Only - Allow only approved clients')); - o.default = 'captive'; + o.default = 'quarantine'; o.rmempty = false; o.description = _('Default behavior for new/unknown clients'); @@ -51,57 +51,6 @@ return view.extend({ o.placeholder = '86400'; o.description = _('Maximum session duration in seconds (default: 86400 = 24 hours)'); - // Captive Portal Settings - s = m.section(form.NamedSection, 'portal', 'portal', _('Captive Portal')); - - o = s.option(form.Flag, 'enabled', _('Enable Captive Portal')); - o.default = '1'; - o.rmempty = false; - o.description = _('Enable nodogsplash captive portal for guest authentication'); - - o = s.option(form.Value, 'title', _('Portal Title')); - o.default = 'Welcome'; - o.placeholder = 'Welcome'; - o.description = _('Main title displayed on the portal page'); - - o = s.option(form.Value, 'subtitle', _('Portal Subtitle')); - o.default = 'Please authenticate to access the network'; - o.placeholder = 'Please authenticate to access the network'; - o.description = _('Subtitle or welcome message'); - - o = s.option(form.ListValue, 'auth_method', _('Authentication Method')); - o.value('click', _('Click to Continue')); - o.value('password', _('Password')); - o.value('voucher', _('Voucher Code')); - o.value('email', _('Email Verification')); - o.default = 'click'; - o.description = _('Method used to authenticate users'); - - o = s.option(form.Value, 'guest_password', _('Guest Password')); - o.depends('auth_method', 'password'); - o.password = true; - o.placeholder = 'Enter password'; - o.description = _('Password required for guest access'); - - o = s.option(form.Value, 'accent_color', _('Accent Color')); - o.default = '#0088cc'; - o.placeholder = '#0088cc'; - o.datatype = 'string'; - o.description = _('Hex color code for portal branding'); - - o = s.option(form.Flag, 'require_terms', _('Require Terms Acceptance')); - o.default = '0'; - o.description = _('Require users to accept terms and conditions'); - - o = s.option(form.Flag, 'allow_registration', _('Allow Self-Registration')); - o.default = '0'; - o.description = _('Allow users to register their own devices'); - - o = s.option(form.Flag, 'registration_approval', _('Require Registration Approval')); - o.default = '1'; - o.depends('allow_registration', '1'); - o.description = _('Administrator must approve self-registered devices'); - // Advanced Settings s = m.section(form.NamedSection, 'config', 'client-guardian', _('Advanced Settings')); @@ -127,26 +76,128 @@ return view.extend({ o.default = '0'; o.description = _('Attempt to detect and block VPN connections'); + // Dashboard Reactiveness + s = m.section(form.NamedSection, 'config', 'client-guardian', _('Dashboard Reactiveness')); + + o = s.option(form.Flag, 'auto_refresh', _('Enable Auto-Refresh'), + _('Automatically refresh dashboard every few seconds')); + o.default = o.enabled; + o.rmempty = false; + + o = s.option(form.ListValue, 'refresh_interval', _('Refresh Interval'), + _('How often to poll for updates')); + o.value('5', _('Every 5 seconds')); + o.value('10', _('Every 10 seconds (recommended)')); + o.value('30', _('Every 30 seconds')); + o.value('60', _('Every 60 seconds')); + o.default = '10'; + o.depends('auto_refresh', '1'); + + // Threat Intelligence Integration + s = m.section(form.NamedSection, 'threat_policy', 'threat_policy', _('Threat Intelligence Integration')); + + o = s.option(form.Flag, 'enabled', _('Enable Threat Intelligence'), + _('Correlate clients with Security Threats Dashboard data')); + o.default = o.enabled; + o.rmempty = false; + + o = s.option(form.Value, 'auto_ban_threshold', _('Auto-Ban Threshold'), + _('Automatically ban clients with threat score above this value (0-100)')); + o.datatype = 'range(1,100)'; + o.placeholder = '80'; + o.default = '80'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'auto_quarantine_threshold', _('Auto-Quarantine Threshold'), + _('Automatically quarantine clients with threat score above this value (0-100)')); + o.datatype = 'range(1,100)'; + o.placeholder = '60'; + o.default = '60'; + o.depends('enabled', '1'); + + o = s.option(form.Value, 'threat_check_interval', _('Threat Check Interval'), + _('How often to check for threats (seconds)')); + o.datatype = 'uinteger'; + o.placeholder = '60'; + o.default = '60'; + o.depends('enabled', '1'); + + // Auto-Zoning / Auto-Parking + s = m.section(form.NamedSection, 'config', 'client-guardian', _('Auto-Zoning & Auto-Parking')); + s.description = _('Automatically assign new clients to zones based on device type, vendor, or hostname patterns.'); + + o = s.option(form.Flag, 'auto_zoning_enabled', _('Enable Auto-Zoning'), + _('Automatically assign clients to zones using matching rules')); + o.default = '1'; + o.rmempty = false; + + o = s.option(form.ListValue, 'auto_parking_zone', _('Auto-Parking Zone'), + _('Default zone for clients that don\'t match any rule')); + o.value('guest', _('Guest')); + o.value('quarantine', _('Quarantine')); + o.value('iot', _('IoT')); + o.value('lan_private', _('LAN Private')); + o.default = 'guest'; + o.depends('auto_zoning_enabled', '1'); + + o = s.option(form.Flag, 'auto_parking_approve', _('Auto-Approve Parked Clients'), + _('Automatically approve clients placed in auto-parking zone')); + o.default = '0'; + o.depends('auto_zoning_enabled', '1'); + + // Auto-Zoning Rules Section + s = m.section(form.GridSection, 'auto_zone_rule', _('Auto-Zoning Rules')); + s.anonymous = false; + s.addremove = true; + s.sortable = true; + s.description = _('Rules are evaluated in priority order. First match wins.'); + + o = s.option(form.Flag, 'enabled', _('Enabled')); + o.default = '1'; + o.editable = true; + + o = s.option(form.Value, 'name', _('Rule Name')); + o.rmempty = false; + + o = s.option(form.ListValue, 'match_type', _('Match Type')); + o.value('vendor', _('Device Vendor (OUI)')); + o.value('hostname', _('Hostname Pattern')); + o.value('mac_prefix', _('MAC Prefix')); + o.default = 'vendor'; + + o = s.option(form.Value, 'match_value', _('Match Value/Pattern')); + o.placeholder = _('e.g., Xiaomi, Apple, .*camera.*, aa:bb:cc'); + o.rmempty = false; + + o = s.option(form.ListValue, 'target_zone', _('Target Zone')); + o.value('lan_private', _('LAN Private')); + o.value('iot', _('IoT')); + o.value('kids', _('Kids')); + o.value('guest', _('Guest')); + o.value('quarantine', _('Quarantine')); + o.default = 'guest'; + + o = s.option(form.Flag, 'auto_approve', _('Auto-Approve')); + o.default = '0'; + + o = s.option(form.Value, 'priority', _('Priority')); + o.datatype = 'uinteger'; + o.placeholder = '50'; + o.default = '50'; + o.description = _('Lower numbers = higher priority'); + return m.render().then(function(rendered) { // Add policy info box at the top var infoBox = E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; border-left: 4px solid #0088cc; padding: 1em; margin-bottom: 1em;' }, [ - E('h3', { 'style': 'margin-top: 0;' }, _('Current Policy: ') + E('span', { 'style': 'color: #0088cc;' }, policy.default_policy || 'captive')), - E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1em; margin-top: 1em;' }, [ - E('div', {}, [ - E('strong', {}, _('Portal Enabled:')), - ' ', - E('span', { 'class': 'badge', 'style': 'background: ' + (policy.portal_enabled ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' }, - policy.portal_enabled ? _('Yes') : _('No')) - ]), - E('div', {}, [ - E('strong', {}, _('Session Timeout:')), - ' ', - E('span', {}, (policy.session_timeout || 86400) + ' ' + _('seconds')) - ]) - ]), + E('h3', { 'style': 'margin-top: 0;' }, _('Current Policy: ') + E('span', { 'style': 'color: #0088cc;' }, policy.default_policy || 'quarantine')), + E('div', { 'style': 'margin-top: 1em;' }, [ + E('strong', {}, _('Session Timeout:')), + ' ', + E('span', {}, (policy.session_timeout || 86400) + ' ' + _('seconds')) + ]), E('div', { 'style': 'margin-top: 1em; padding: 0.75em; background: white; border-radius: 4px;' }, [ E('strong', {}, _('Policy Descriptions:')), E('ul', { 'style': 'margin: 0.5em 0;' }, [ @@ -156,9 +207,9 @@ return view.extend({ _('All clients can access the network without authentication. Not recommended for public networks.') ]), E('li', {}, [ - E('strong', {}, _('Captive Portal:')), + E('strong', {}, _('Quarantine:')), ' ', - _('New clients are redirected to a captive portal for authentication. Recommended for guest networks.') + _('New clients are placed in quarantine and require manual approval. Recommended for secure networks.') ]), E('li', {}, [ E('strong', {}, _('Whitelist Only:')), diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/zones.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/zones.js index becb744c..94000e4c 100644 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/zones.js +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/view/client-guardian/zones.js @@ -11,7 +11,7 @@ return view.extend({ }, render: function(data) { - var zones = data.zones || []; + var zones = Array.isArray(data) ? data : (data.zones || []); var self = this; return E('div', { 'class': 'client-guardian-dashboard' }, [ @@ -22,11 +22,19 @@ return view.extend({ E('div', { 'class': 'cg-logo' }, [ E('div', { 'class': 'cg-logo-icon' }, '🌐'), E('div', { 'class': 'cg-logo-text' }, 'Zones Réseau') + ]), + E('button', { + 'class': 'cg-btn cg-btn-primary', + 'click': L.bind(this.handleSyncZones, this), + 'style': 'display: flex; align-items: center; gap: 8px;' + }, [ + E('span', {}, '🔄'), + 'Synchroniser Firewall' ]) ]), E('p', { 'style': 'color: var(--cg-text-secondary); margin-bottom: 24px' }, - 'Définissez les zones de sécurité avec leurs règles d\'accès, filtrage et limitations.' + 'Définissez les zones de sécurité avec leurs règles d\'accès, filtrage et limitations. Cliquez sur "Synchroniser Firewall" pour créer les zones dans la configuration firewall.' ), E('div', { 'class': 'cg-zones-grid' }, @@ -153,7 +161,7 @@ return view.extend({ ]) : E('span'), E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end; margin-top: 20px' }, [ E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')), - E('button', { 'class': 'cg-btn cg-btn-primary', 'click': function() { + E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() { api.updateZone( zone.id, zone.name, @@ -161,15 +169,53 @@ return view.extend({ document.getElementById('zone-filter').value, zone.time_restrictions ? document.getElementById('zone-start').value : '', zone.time_restrictions ? document.getElementById('zone-end').value : '' - ).then(function() { + ).then(L.bind(function() { ui.hideModal(); - window.location.reload(); - }); - }}, _('Enregistrer')) + ui.addNotification(null, E('p', _('Zone updated successfully')), 'success'); + this.handleRefresh(); + }, this)); + }, this)}, _('Enregistrer')) ]) ]); }, + handleSyncZones: function(ev) { + var btn = ev.currentTarget; + btn.disabled = true; + btn.innerHTML = ' Synchronisation...'; + + api.syncZones().then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, 'Zones firewall synchronisées avec succès'), 'success'); + btn.innerHTML = ' Synchronisé'; + setTimeout(function() { + btn.disabled = false; + btn.innerHTML = '🔄 Synchroniser Firewall'; + }, 2000); + } else { + ui.addNotification(null, E('p', {}, 'Erreur lors de la synchronisation'), 'error'); + btn.disabled = false; + btn.innerHTML = '🔄 Synchroniser Firewall'; + } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, 'Erreur: ' + err), 'error'); + btn.disabled = false; + btn.innerHTML = '🔄 Synchroniser Firewall'; + }); + }, + + handleRefresh: function() { + return api.getZones().then(L.bind(function(data) { + var container = document.querySelector('.client-guardian-dashboard'); + if (container) { + var newView = this.render(data); + dom.content(container.parentNode, newView); + } + }, this)).catch(function(err) { + console.error('Failed to refresh zones list:', err); + }); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-client-guardian/root/etc/config/client-guardian b/package/secubox/luci-app-client-guardian/root/etc/config/client-guardian index 50a24b2d..3200c343 100644 --- a/package/secubox/luci-app-client-guardian/root/etc/config/client-guardian +++ b/package/secubox/luci-app-client-guardian/root/etc/config/client-guardian @@ -4,10 +4,18 @@ config client-guardian 'config' option quarantine_zone 'quarantine' option scan_interval '30' option auto_approve '0' - option portal_enabled '1' - option portal_url '/cgi-bin/client-guardian-portal' - option session_timeout '3600' option log_level 'info' + # Dashboard Reactiveness + option auto_refresh '1' + option refresh_interval '10' + # Debug Mode + option debug_enabled '0' + option debug_level 'INFO' + option enable_active_scan '1' + # Auto-Zoning / Auto-Parking + option auto_zoning_enabled '1' + option auto_parking_zone 'guest' + option auto_parking_approve '0' # Alert Configuration config alerts 'alerts' @@ -76,7 +84,13 @@ config zone 'kids' option content_filter 'kids' option schedule_start '08:00' option schedule_end '21:00' - list schedule_days 'mon' 'tue' 'wed' 'thu' 'fri' 'sat' 'sun' + list schedule_days 'mon' + list schedule_days 'tue' + list schedule_days 'wed' + list schedule_days 'thu' + list schedule_days 'fri' + list schedule_days 'sat' + list schedule_days 'sun' config zone 'guest' option name 'Invités' @@ -90,8 +104,6 @@ config zone 'guest' option bandwidth_limit '25' option time_restrictions '0' option content_filter 'adult' - option session_duration '7200' - option portal_required '1' config zone 'quarantine' option name 'Quarantaine' @@ -103,8 +115,6 @@ config zone 'quarantine' option local_access '0' option inter_client '0' option bandwidth_limit '1' - option portal_required '1' - option portal_only '1' config zone 'blocked' option name 'Bloqué' @@ -120,15 +130,23 @@ config zone 'blocked' config filter 'kids_filter' option name 'Filtre Enfants' option type 'whitelist' - list categories 'education' 'kids' 'games_safe' - list blocked_categories 'adult' 'violence' 'gambling' 'drugs' 'weapons' + list categories 'education' + list categories 'kids' + list categories 'games_safe' + list blocked_categories 'adult' + list blocked_categories 'violence' + list blocked_categories 'gambling' + list blocked_categories 'drugs' + list blocked_categories 'weapons' option safe_search '1' option youtube_restricted '1' config filter 'adult_filter' option name 'Filtre Adulte' option type 'blacklist' - list blocked_categories 'malware' 'phishing' 'illegal' + list blocked_categories 'malware' + list blocked_categories 'phishing' + list blocked_categories 'illegal' option safe_search '0' config filter 'strict_filter' @@ -157,7 +175,11 @@ config schedule 'school_hours' option action 'block' option start_time '08:00' option end_time '16:00' - list days 'mon' 'tue' 'wed' 'thu' 'fri' + list days 'mon' + list days 'tue' + list days 'wed' + list days 'thu' + list days 'fri' config schedule 'night_block' option name 'Blocage Nocturne' @@ -165,31 +187,28 @@ config schedule 'night_block' option action 'block' option start_time '22:00' option end_time '07:00' - list days 'mon' 'tue' 'wed' 'thu' 'fri' 'sat' 'sun' + list days 'mon' + list days 'tue' + list days 'wed' + list days 'thu' + list days 'fri' + list days 'sat' + list days 'sun' config schedule 'weekend_limit' option name 'Limite Weekend' option enabled '0' option action 'quota' option daily_quota '180' - list days 'sat' 'sun' + list days 'sat' + list days 'sun' -# Captive Portal Configuration -config portal 'portal' +# Threat Intelligence Integration +config threat_policy 'threat_policy' option enabled '1' - option title 'Bienvenue sur le Réseau' - option subtitle 'Veuillez vous identifier pour accéder à Internet' - option logo '/luci-static/client-guardian/logo.png' - option background_color '#0f172a' - option accent_color '#ef4444' - option require_terms '1' - option terms_url '/client-guardian/terms.html' - option auth_method 'password' - option guest_password 'guest2024' - option allow_registration '1' - option registration_approval '1' - option show_bandwidth_info '1' - option custom_css '' + option auto_ban_threshold '80' + option auto_quarantine_threshold '60' + option threat_check_interval '60' # Example Known Clients config client 'client_example1' @@ -229,3 +248,84 @@ config client 'client_banned' option first_seen '2024-12-18 03:00:00' option ban_reason 'Tentative intrusion' option ban_date '2024-12-18 03:05:00' + +# Auto-Zoning Rules +# Rules are evaluated in order, first match wins + +# IoT Devices - Chinese brands +config auto_zone_rule 'rule_xiaomi' + option enabled '1' + option name 'Xiaomi Devices' + option match_type 'vendor' + option match_value 'Xiaomi' + option target_zone 'iot' + option auto_approve '0' + option priority '10' + +config auto_zone_rule 'rule_tuya' + option enabled '1' + option name 'Tuya Smart Devices' + option match_type 'vendor' + option match_value 'Tuya' + option target_zone 'iot' + option auto_approve '0' + option priority '10' + +config auto_zone_rule 'rule_tp_link' + option enabled '1' + option name 'TP-Link Smart Home' + option match_type 'vendor' + option match_value 'TP-Link' + option target_zone 'iot' + option auto_approve '0' + option priority '10' + +# Mobile devices - Kids tablets +config auto_zone_rule 'rule_kids_tablet' + option enabled '1' + option name 'Kids Tablets' + option match_type 'hostname' + option match_pattern 'tablet-.*|.*-kid.*|samsung-tab-kid' + option target_zone 'kids' + option auto_approve '1' + option priority '20' + +# Guest devices - Temporary +config auto_zone_rule 'rule_guest_android' + option enabled '1' + option name 'Guest Android Phones' + option match_type 'hostname' + option match_pattern 'android-.*|Galaxy-.*|Pixel-.*' + option target_zone 'guest' + option auto_approve '0' + option priority '30' + +config auto_zone_rule 'rule_guest_iphone' + option enabled '1' + option name 'Guest iPhones' + option match_type 'hostname' + option match_pattern 'iPhone.*|iPad.*' + option target_zone 'guest' + option auto_approve '0' + option priority '30' + +# Trusted devices - Apple ecosystem +config auto_zone_rule 'rule_apple_trusted' + option enabled '0' + option name 'Apple Devices (Trusted)' + option match_type 'vendor' + option match_value 'Apple' + option target_zone 'lan_private' + option auto_approve '1' + option priority '40' + +# IoT Cameras +config auto_zone_rule 'rule_cameras' + option enabled '1' + option name 'IP Cameras' + option match_type 'hostname' + option match_pattern '.*camera.*|.*cam.*|ipcam.*|IPCam.*' + option target_zone 'iot' + option auto_approve '0' + option priority '15' + diff --git a/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian b/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian index ab20011c..2e2287a2 100755 --- a/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian +++ b/package/secubox/luci-app-client-guardian/root/usr/libexec/rpcd/luci.client-guardian @@ -11,32 +11,193 @@ LOG_FILE="/var/log/client-guardian.log" CLIENTS_DB="/tmp/client-guardian-clients.json" ALERTS_QUEUE="/tmp/client-guardian-alerts.json" -# Logging function +# Logging function with debug support log_event() { local level="$1" local message="$2" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] [$level] $message" >> "$LOG_FILE" + + # Also log to syslog if debug enabled + local debug_enabled=$(uci -q get client-guardian.config.debug_enabled) + if [ "$debug_enabled" = "1" ]; then + logger -t client-guardian -p "daemon.$level" "$message" + fi } -# Get real-time client list from ARP and DHCP +# Debug logging function +log_debug() { + local message="$1" + local data="$2" + + local debug_enabled=$(uci -q get client-guardian.config.debug_enabled) + local debug_level=$(uci -q get client-guardian.config.debug_level || echo "INFO") + + if [ "$debug_enabled" != "1" ]; then + return + fi + + # Log based on level hierarchy: ERROR < WARN < INFO < DEBUG < TRACE + case "$debug_level" in + ERROR) return ;; # Only errors + WARN) [ "$1" != "error" ] && [ "$1" != "warn" ] && return ;; + INFO) [ "$1" != "error" ] && [ "$1" != "warn" ] && [ "$1" != "info" ] && return ;; + DEBUG) [ "$1" = "trace" ] && return ;; + TRACE) ;; # Log everything + esac + + local timestamp=$(date '+%Y-%m-%d %H:%M:%S.%N' | cut -c1-23) + local log_msg="[$timestamp] [DEBUG] $message" + + if [ -n "$data" ]; then + log_msg="$log_msg | Data: $data" + fi + + echo "$log_msg" >> "$LOG_FILE" + logger -t client-guardian-debug "$log_msg" +} + +# Active network scan to discover clients +scan_network_active() { + local subnet="$1" + local iface="$2" + + # Method 1: arping (if available) + if command -v arping >/dev/null 2>&1; then + # Scan common subnet (192.168.x.0/24) + for i in $(seq 1 254); do + arping -c 1 -w 1 -I "$iface" "${subnet%.*}.$i" >/dev/null 2>&1 & + done + wait + # Method 2: ping sweep fallback + elif command -v ping >/dev/null 2>&1; then + for i in $(seq 1 254); do + ping -c 1 -W 1 "${subnet%.*}.$i" >/dev/null 2>&1 & + done + wait + fi + + # Let ARP table populate + sleep 2 +} + +# Enhanced client detection with multiple methods get_connected_clients() { - local clients="" - - # Parse ARP table - while read ip type flags mac mask iface; do + log_debug "Starting client detection" "method=get_connected_clients" + + local clients_tmp="/tmp/cg-clients-$$" + > "$clients_tmp" + + # Active scan to populate ARP table (run in background) + local enable_scan=$(uci -q get client-guardian.config.enable_active_scan || echo "1") + log_debug "Active scan setting" "enabled=$enable_scan" + + if [ "$enable_scan" = "1" ]; then + # Detect network subnets to scan + local subnets=$(ip -4 addr show | awk '/inet.*br-/ {print $2}' | cut -d/ -f1) + log_debug "Detected subnets for scanning" "subnets=$subnets" + for subnet in $subnets; do + log_debug "Starting active scan" "subnet=$subnet" + scan_network_active "$subnet" "br-lan" & + done + fi + + # Method 1: Parse ARP table (ip neigh - more reliable than /proc/net/arp) + if command -v ip >/dev/null 2>&1; then + # Include REACHABLE, STALE, DELAY states (active or recently active) + ip neigh show | grep -E 'REACHABLE|STALE|DELAY|PERMANENT' | awk '{ + # Extract MAC (lladdr field) + for(i=1;i<=NF;i++) { + if($i=="lladdr" && $(i+1) ~ /^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/) { + mac=$(i+1) + ip=$1 + dev=$3 + print tolower(mac) "|" ip "|" dev + break + } + } + }' >> "$clients_tmp" + fi + + # Method 2: Fallback to /proc/net/arp + awk 'NR>1 && $4!="00:00:00:00:00:00" && $3!="0x0" { + print tolower($4) "|" $1 "|" $6 + }' /proc/net/arp >> "$clients_tmp" + + # Method 3: DHCP leases (authoritative for IP assignments) + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + awk '{print tolower($2) "|" $3 "|" $4 "|dhcp|" $1}' /tmp/dhcp.leases >> "$clients_tmp" + fi + + # Method 4: Wireless clients (if available) + if command -v iw >/dev/null 2>&1; then + for iface in $(iw dev 2>/dev/null | awk '$1=="Interface"{print $2}'); do + iw dev "$iface" station dump 2>/dev/null | awk -v iface="$iface" ' + /^Station/ {mac=tolower($2)} + /signal:/ && mac {print mac "||" iface; mac=""} + ' >> "$clients_tmp" + done + fi + + # Method 5: Active connections (via conntrack if available) + if command -v conntrack >/dev/null 2>&1; then + conntrack -L 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | while read ip; do + # Try to resolve MAC via ARP + local mac=$(ip neigh show "$ip" 2>/dev/null | awk '/lladdr/{print tolower($5)}' | head -1) + [ -n "$mac" ] && [ "$mac" != "00:00:00:00:00:00" ] && echo "$mac|$ip|br-lan" >> "$clients_tmp" + done + fi + + # Method 6: Parse /proc/net/arp for any entry (last resort) + cat /proc/net/arp 2>/dev/null | awk 'NR>1 && $4 ~ /^[0-9a-fA-F:]+$/ && $4 != "00:00:00:00:00:00" { + print tolower($4) "|" $1 "|" $6 + }' >> "$clients_tmp" + + # Deduplicate and merge data + sort -u -t'|' -k1,1 "$clients_tmp" | while IFS='|' read mac ip iface extra; do [ -z "$mac" ] && continue [ "$mac" = "00:00:00:00:00:00" ] && continue - + + # Skip IPv6 addresses in IP field + echo "$ip" | grep -q ':' && continue + # Get hostname from DHCP leases - local hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}') + local hostname="" + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}' | head -1) + fi + + # Try to resolve hostname via DNS reverse lookup + if [ -z "$hostname" ] && [ "$ip" != "N/A" ] && [ -n "$ip" ]; then + hostname=$(nslookup "$ip" 2>/dev/null | awk '/name =/{print $4}' | sed 's/\.$//' | head -1) + fi + [ -z "$hostname" ] && hostname="Unknown" - - # Get lease expiry - local lease_time=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $1}') - + + # Get best IP address (prefer DHCP assigned) + if [ -z "$ip" ] || [ "$ip" = "" ]; then + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + ip=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $3}' | head -1) + fi + [ -z "$ip" ] && ip="N/A" + fi + + # Get interface (prefer provided, fallback to bridge) + [ -z "$iface" ] && iface="br-lan" + + # Get lease time + local lease_time="" + if [ -f /tmp/dhcp.leases ] && [ -s /tmp/dhcp.leases ]; then + lease_time=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $1}' | head -1) + fi + echo "$mac|$ip|$hostname|$iface|$lease_time" - done < /proc/net/arp + done + + rm -f "$clients_tmp" + + # Wait for background scan to complete + wait } # Get dashboard status @@ -45,11 +206,9 @@ get_status() { local enabled=$(uci -q get client-guardian.config.enabled || echo "1") local default_policy=$(uci -q get client-guardian.config.default_policy || echo "quarantine") - local portal_enabled=$(uci -q get client-guardian.portal.enabled || echo "1") json_add_boolean "enabled" "$enabled" json_add_string "default_policy" "$default_policy" - json_add_boolean "portal_enabled" "$portal_enabled" # Count clients by status local total_known=0 @@ -110,6 +269,243 @@ count_zones() { zone_count=$((zone_count + 1)) } +# Threat Intelligence Integration +get_client_threats() { + local ip="$1" + local mac="$2" + + # Check if threat intelligence is enabled + local threat_enabled=$(uci -q get client-guardian.threat_policy.enabled) + [ "$threat_enabled" != "1" ] && return + + # Query Security Threats Dashboard via ubus + ubus call luci.secubox-security-threats get_active_threats 2>/dev/null | \ + jsonfilter -e "@.threats[@.ip='$ip']" -e "@.threats[@.mac='$mac']" 2>/dev/null +} + +enrich_client_with_threats() { + local ip="$1" + local mac="$2" + + # Get threat data + local threats=$(get_client_threats "$ip" "$mac") + + # Count threats and find max risk score + local threat_count=0 + local max_risk_score=0 + + if [ -n "$threats" ]; then + threat_count=$(echo "$threats" | jsonfilter -e '@[*].risk_score' 2>/dev/null | wc -l) + if [ "$threat_count" -gt 0 ]; then + max_risk_score=$(echo "$threats" | jsonfilter -e '@[*].risk_score' 2>/dev/null | sort -rn | head -1) + fi + fi + + # Add threat fields to JSON + json_add_int "threat_count" "${threat_count:-0}" + json_add_int "risk_score" "${max_risk_score:-0}" + json_add_boolean "has_threats" "$( [ "$threat_count" -gt 0 ] && echo 1 || echo 0 )" + + # Check for auto-actions if threats detected + if [ "$threat_count" -gt 0 ] && [ "$max_risk_score" -gt 0 ]; then + check_threat_auto_actions "$mac" "$ip" "$max_risk_score" + fi +} + +# Auto-ban/quarantine based on threat score +check_threat_auto_actions() { + local mac="$1" + local ip="$2" + local risk_score="$3" + + # Check if threat intelligence and auto-actions are enabled + local threat_enabled=$(uci -q get client-guardian.threat_policy.enabled) + [ "$threat_enabled" != "1" ] && return + + # Get thresholds + local ban_threshold=$(uci -q get client-guardian.threat_policy.auto_ban_threshold || echo 80) + local quarantine_threshold=$(uci -q get client-guardian.threat_policy.auto_quarantine_threshold || echo 60) + + # Check if client is already approved (skip auto-actions for approved clients) + local status=$(get_client_status "$mac") + [ "$status" = "approved" ] && return + + # Auto-ban high-risk clients + if [ "$risk_score" -ge "$ban_threshold" ]; then + log_event "warning" "Auto-ban client $mac (IP: $ip) - Threat score: $risk_score" + + # Create/update client entry + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + local section="" + if [ -n "$found_section" ]; then + section="$found_section" + else + section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="Auto-banned Device" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + fi + + uci set client-guardian.$section.status="banned" + uci set client-guardian.$section.zone="blocked" + uci set client-guardian.$section.ban_reason="Auto-banned: Threat score $risk_score" + uci set client-guardian.$section.ban_date="$(date '+%Y-%m-%d %H:%M:%S')" + uci commit client-guardian + + # Apply firewall block + apply_client_rules "$mac" "blocked" + return + fi + + # Auto-quarantine medium-risk clients + if [ "$risk_score" -ge "$quarantine_threshold" ]; then + log_event "warning" "Auto-quarantine client $mac (IP: $ip) - Threat score: $risk_score" + + # Create/update client entry + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + local section="" + if [ -n "$found_section" ]; then + section="$found_section" + else + section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="Auto-quarantined Device" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + fi + + uci set client-guardian.$section.status="unknown" + uci set client-guardian.$section.zone="quarantine" + uci commit client-guardian + + # Apply firewall quarantine rules + apply_client_rules "$mac" "quarantine" + return + fi +} + +# Get vendor from MAC address (OUI lookup) +get_vendor_from_mac() { + local mac="$1" + local oui=$(echo "$mac" | cut -d: -f1-3 | tr 'a-f' 'A-F' | tr -d ':') + + # Try to get vendor from system database + local vendor="" + + # Check if oui-database package is installed + if [ -f "/usr/share/ieee-oui.txt" ]; then + vendor=$(grep -i "^$oui" /usr/share/ieee-oui.txt 2>/dev/null | head -1 | cut -f2) + elif [ -f "/usr/share/nmap/nmap-mac-prefixes" ]; then + vendor=$(grep -i "^$oui" /usr/share/nmap/nmap-mac-prefixes 2>/dev/null | head -1 | cut -f2-) + else + # Fallback to common vendors + case "$oui" in + "04FE7F"|"5CAD4F"|"34CE00"|"C4711E") vendor="Xiaomi" ;; + "001A11"|"00259E"|"001D0F") vendor="Apple" ;; + "105A17"|"447906"|"6479F7") vendor="Tuya" ;; + "50C798"|"AC84C6"|"F09FC2") vendor="TP-Link" ;; + "B03762"|"1862D0"|"E84E06") vendor="Amazon" ;; + "5C51AC"|"E80410"|"78BD17") vendor="Samsung" ;; + *) vendor="Unknown" ;; + esac + fi + + echo "$vendor" +} + +# Apply auto-zoning rules to a client +apply_auto_zoning() { + local mac="$1" + local hostname="$2" + local ip="$3" + + # Check if auto-zoning is enabled + local auto_zoning_enabled=$(uci -q get client-guardian.config.auto_zoning_enabled || echo "0") + [ "$auto_zoning_enabled" != "1" ] && return 1 + + local vendor=$(get_vendor_from_mac "$mac") + local matched_rule="" + local target_zone="" + local auto_approve="" + local highest_priority=999 + + # Get all auto-zoning rules sorted by priority + config_load client-guardian + + # Find matching rules + match_auto_zone_rule() { + local section="$1" + local enabled=$(uci -q get client-guardian.$section.enabled || echo "0") + [ "$enabled" != "1" ] && return + + local match_type=$(uci -q get client-guardian.$section.match_type) + local priority=$(uci -q get client-guardian.$section.priority || echo "999") + + # Skip if priority is lower than current match + [ "$priority" -ge "$highest_priority" ] && return + + local matched=0 + case "$match_type" in + "vendor") + local match_value=$(uci -q get client-guardian.$section.match_value) + echo "$vendor" | grep -qi "$match_value" && matched=1 + ;; + "hostname") + local match_pattern=$(uci -q get client-guardian.$section.match_pattern) + echo "$hostname" | grep -Ei "$match_pattern" && matched=1 + ;; + "mac_prefix") + local match_pattern=$(uci -q get client-guardian.$section.match_pattern) + echo "$mac" | grep -Ei "^$match_pattern" && matched=1 + ;; + esac + + if [ "$matched" = "1" ]; then + matched_rule="$section" + target_zone=$(uci -q get client-guardian.$section.target_zone) + auto_approve=$(uci -q get client-guardian.$section.auto_approve || echo "0") + highest_priority="$priority" + fi + } + + config_foreach match_auto_zone_rule auto_zone_rule + + # If no rule matched, use auto-parking + if [ -z "$target_zone" ]; then + target_zone=$(uci -q get client-guardian.config.auto_parking_zone || echo "guest") + auto_approve=$(uci -q get client-guardian.config.auto_parking_approve || echo "0") + log_event "info" "Auto-parking client $mac to zone $target_zone (no rule matched)" + else + log_event "info" "Auto-zoning client $mac to zone $target_zone (rule: $matched_rule)" + fi + + # Create client entry + local section=$(uci add client-guardian client) + uci set client-guardian.$section.mac="$mac" + uci set client-guardian.$section.name="${hostname:-Unknown Device}" + uci set client-guardian.$section.zone="$target_zone" + uci set client-guardian.$section.first_seen="$(date '+%Y-%m-%d %H:%M:%S')" + uci set client-guardian.$section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" + uci set client-guardian.$section.vendor="$vendor" + + if [ "$auto_approve" = "1" ]; then + uci set client-guardian.$section.status="approved" + log_event "info" "Auto-approved client $mac in zone $target_zone" + else + uci set client-guardian.$section.status="unknown" + fi + + uci commit client-guardian + + # Apply firewall rules + apply_client_rules "$mac" "$target_zone" + + return 0 +} + # Get all clients (known + detected) get_clients() { json_init @@ -148,15 +544,42 @@ get_clients() { json_add_string "last_seen" "$(uci -q get client-guardian.$found_section.last_seen)" json_add_string "notes" "$(uci -q get client-guardian.$found_section.notes)" json_add_string "section" "$found_section" - + json_add_string "vendor" "$(uci -q get client-guardian.$found_section.vendor || echo 'Unknown')" + # Update last seen uci set client-guardian.$found_section.last_seen="$(date '+%Y-%m-%d %H:%M:%S')" else - json_add_boolean "known" 0 - json_add_string "name" "$hostname" - json_add_string "zone" "quarantine" - json_add_string "status" "unknown" - json_add_string "first_seen" "$(date '+%Y-%m-%d %H:%M:%S')" + # New client detected - apply auto-zoning if enabled + if apply_auto_zoning "$mac" "$hostname" "$ip"; then + # Auto-zoning succeeded, reload and get the new section + config_load client-guardian + config_foreach find_client_by_mac client "$mac" + + if [ -n "$found_section" ]; then + json_add_boolean "known" 1 + json_add_string "name" "$(uci -q get client-guardian.$found_section.name)" + json_add_string "zone" "$(uci -q get client-guardian.$found_section.zone)" + json_add_string "status" "$(uci -q get client-guardian.$found_section.status)" + json_add_string "first_seen" "$(uci -q get client-guardian.$found_section.first_seen)" + json_add_string "vendor" "$(uci -q get client-guardian.$found_section.vendor || echo 'Unknown')" + else + # Fallback in case auto-zoning failed + json_add_boolean "known" 0 + json_add_string "name" "$hostname" + json_add_string "zone" "quarantine" + json_add_string "status" "unknown" + json_add_string "first_seen" "$(date '+%Y-%m-%d %H:%M:%S')" + json_add_string "vendor" "$(get_vendor_from_mac "$mac")" + fi + else + # Auto-zoning disabled or failed - use default quarantine + json_add_boolean "known" 0 + json_add_string "name" "$hostname" + json_add_string "zone" "quarantine" + json_add_string "status" "unknown" + json_add_string "first_seen" "$(date '+%Y-%m-%d %H:%M:%S')" + json_add_string "vendor" "$(get_vendor_from_mac "$mac")" + fi fi # Get traffic stats if available @@ -169,7 +592,10 @@ get_clients() { fi json_add_int "rx_bytes" "$rx_bytes" json_add_int "tx_bytes" "$tx_bytes" - + + # Enrich with threat intelligence + enrich_client_with_threats "$ip" "$mac" + json_close_object found_section="" done << EOF @@ -221,6 +647,11 @@ add_offline_client() { json_add_string "section" "$section" json_add_int "rx_bytes" 0 json_add_int "tx_bytes" 0 + + # Enrich with threat intelligence + local ip="$(uci -q get client-guardian.$section.static_ip || echo 'N/A')" + enrich_client_with_threats "$ip" "$mac" + json_close_object } @@ -238,7 +669,25 @@ get_zones() { output_zone() { local section="$1" - + + # Helper to convert true/false to 1/0 + local internet_val=$(uci -q get client-guardian.$section.internet_access || echo "0") + [ "$internet_val" = "true" ] && internet_val="1" + [ "$internet_val" = "false" ] && internet_val="0" + + local local_val=$(uci -q get client-guardian.$section.local_access || echo "0") + [ "$local_val" = "true" ] && local_val="1" + [ "$local_val" = "false" ] && local_val="0" + + local inter_val=$(uci -q get client-guardian.$section.inter_client || echo "0") + [ "$inter_val" = "true" ] && inter_val="1" + [ "$inter_val" = "false" ] && inter_val="0" + + local time_val=$(uci -q get client-guardian.$section.time_restrictions || echo "0") + [ "$time_val" = "true" ] && time_val="1" + [ "$time_val" = "false" ] && time_val="0" + + json_add_object json_add_string "id" "$section" json_add_string "name" "$(uci -q get client-guardian.$section.name)" @@ -246,19 +695,18 @@ output_zone() { json_add_string "network" "$(uci -q get client-guardian.$section.network)" json_add_string "color" "$(uci -q get client-guardian.$section.color)" json_add_string "icon" "$(uci -q get client-guardian.$section.icon)" - json_add_boolean "internet_access" "$(uci -q get client-guardian.$section.internet_access || echo 0)" - json_add_boolean "local_access" "$(uci -q get client-guardian.$section.local_access || echo 0)" - json_add_boolean "inter_client" "$(uci -q get client-guardian.$section.inter_client || echo 0)" + json_add_boolean "internet_access" "$internet_val" + json_add_boolean "local_access" "$local_val" + json_add_boolean "inter_client" "$inter_val" json_add_int "bandwidth_limit" "$(uci -q get client-guardian.$section.bandwidth_limit || echo 0)" - json_add_boolean "time_restrictions" "$(uci -q get client-guardian.$section.time_restrictions || echo 0)" + json_add_boolean "time_restrictions" "$time_val" json_add_string "content_filter" "$(uci -q get client-guardian.$section.content_filter)" - json_add_boolean "portal_required" "$(uci -q get client-guardian.$section.portal_required || echo 0)" - + # Count clients in zone local count=0 config_foreach count_zone_clients client "$section" json_add_int "client_count" "$count" - + json_close_object } @@ -319,29 +767,6 @@ output_schedule() { json_close_object } -# Get portal configuration -get_portal() { - json_init - - json_add_boolean "enabled" "$(uci -q get client-guardian.portal.enabled || echo 1)" - json_add_string "title" "$(uci -q get client-guardian.portal.title)" - json_add_string "subtitle" "$(uci -q get client-guardian.portal.subtitle)" - json_add_string "logo" "$(uci -q get client-guardian.portal.logo)" - json_add_string "background_color" "$(uci -q get client-guardian.portal.background_color)" - json_add_string "accent_color" "$(uci -q get client-guardian.portal.accent_color)" - json_add_boolean "require_terms" "$(uci -q get client-guardian.portal.require_terms || echo 0)" - json_add_string "auth_method" "$(uci -q get client-guardian.portal.auth_method)" - json_add_boolean "allow_registration" "$(uci -q get client-guardian.portal.allow_registration || echo 0)" - json_add_boolean "registration_approval" "$(uci -q get client-guardian.portal.registration_approval || echo 1)" - json_add_boolean "show_bandwidth_info" "$(uci -q get client-guardian.portal.show_bandwidth_info || echo 0)" - - # Active sessions count - local sessions=0 - [ -f "/tmp/client-guardian-sessions" ] && sessions=$(wc -l < /tmp/client-guardian-sessions) - json_add_int "active_sessions" "$sessions" - - json_dump -} # Get alert configuration get_alerts() { @@ -410,6 +835,256 @@ get_logs() { json_dump } +# Profile Management Functions + +# List available zone profiles +list_profiles() { + local profiles_file="/etc/client-guardian/profiles.json" + + if [ -f "$profiles_file" ]; then + cat "$profiles_file" + else + echo '{"profiles":[]}' + fi +} + +# Apply a zone profile +apply_profile() { + read input + json_load "$input" + json_get_var profile_id profile_id + json_get_var auto_refresh auto_refresh + json_get_var refresh_interval refresh_interval + json_get_var threat_enabled threat_enabled + json_get_var auto_ban_threshold auto_ban_threshold + json_get_var auto_quarantine_threshold auto_quarantine_threshold + + json_init + + if [ -z "$profile_id" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Profile ID required" + json_dump + return + fi + + local profiles_file="/etc/client-guardian/profiles.json" + + if [ ! -f "$profiles_file" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Profiles file not found" + json_dump + return + fi + + # Extract profile zones + local profile_data=$(cat "$profiles_file" | jsonfilter -e "@.profiles[@.id='$profile_id']") + + if [ -z "$profile_data" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Profile not found: $profile_id" + json_dump + return + fi + + # Remove existing zones (except quarantine and blocked which are system) + local existing_zones=$(uci show client-guardian | grep "=zone" | cut -d. -f2 | cut -d= -f1) + for zone_section in $existing_zones; do + local zone_id=$(uci -q get client-guardian.$zone_section 2>/dev/null || echo "$zone_section") + if [ "$zone_id" != "quarantine" ] && [ "$zone_id" != "blocked" ]; then + uci delete client-guardian.$zone_section 2>/dev/null + fi + done + + # Parse and create zones from profile + local zone_count=0 + local idx=0 + + # Iterate through zones by index (up to reasonable limit) + while [ "$idx" -lt "20" ]; do + local zone_id=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].id" 2>/dev/null) + + # Break if no more zones + [ -z "$zone_id" ] && break + + local zone_name=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].name") + local zone_desc=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].description") + local zone_network=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].network") + local zone_color=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].color") + local zone_icon=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].icon") + local internet=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].internet_access") + local local_access=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].local_access") + local inter_client=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].inter_client") + local bandwidth=$(echo "$profile_data" | jsonfilter -e "@.zones[$idx].bandwidth_limit") + + # Create UCI zone section + uci set client-guardian.$zone_id=zone 2>/dev/null + [ -n "$zone_name" ] && uci set client-guardian.$zone_id.name="$zone_name" 2>/dev/null + [ -n "$zone_desc" ] && uci set client-guardian.$zone_id.description="$zone_desc" 2>/dev/null + [ -n "$zone_network" ] && uci set client-guardian.$zone_id.network="$zone_network" 2>/dev/null + [ -n "$zone_color" ] && uci set client-guardian.$zone_id.color="$zone_color" 2>/dev/null + [ -n "$zone_icon" ] && uci set client-guardian.$zone_id.icon="$zone_icon" 2>/dev/null + [ -n "$internet" ] && uci set client-guardian.$zone_id.internet_access="$internet" 2>/dev/null + [ -n "$local_access" ] && uci set client-guardian.$zone_id.local_access="$local_access" 2>/dev/null + [ -n "$inter_client" ] && uci set client-guardian.$zone_id.inter_client="$inter_client" 2>/dev/null + uci set client-guardian.$zone_id.bandwidth_limit="${bandwidth:-0}" 2>/dev/null + + zone_count=$((zone_count + 1)) + idx=$((idx + 1)) + done + + # Apply dashboard settings (with error suppression) + [ -n "$auto_refresh" ] && uci set client-guardian.config.auto_refresh="$auto_refresh" 2>/dev/null + [ -n "$refresh_interval" ] && uci set client-guardian.config.refresh_interval="$refresh_interval" 2>/dev/null + + # Apply threat intelligence settings (create section if needed) + uci set client-guardian.threat_policy=threat_policy 2>/dev/null + [ -n "$threat_enabled" ] && uci set client-guardian.threat_policy.enabled="$threat_enabled" 2>/dev/null + [ -n "$auto_ban_threshold" ] && uci set client-guardian.threat_policy.auto_ban_threshold="$auto_ban_threshold" 2>/dev/null + [ -n "$auto_quarantine_threshold" ] && uci set client-guardian.threat_policy.auto_quarantine_threshold="$auto_quarantine_threshold" 2>/dev/null + + uci commit client-guardian 2>/dev/null + + # Sync firewall zones + sync_firewall_zones + + log_event "info" "Applied profile: $profile_id ($zone_count zones)" + + json_add_boolean "success" 1 + json_add_string "message" "Profile $profile_id applied successfully" + json_add_int "zones_created" "$zone_count" + json_dump +} + +# Firewall Zone Synchronization Functions + +# Ensure Client Guardian zones exist in firewall +sync_firewall_zones() { + # Check if firewall zones need to be created + config_load client-guardian + config_foreach create_firewall_zone zone +} + +# Create firewall zone for Client Guardian zone +create_firewall_zone() { + local section="$1" + local zone_name=$(uci -q get client-guardian.$section.name) + local network=$(uci -q get client-guardian.$section.network) + local internet_access=$(uci -q get client-guardian.$section.internet_access) + local local_access=$(uci -q get client-guardian.$section.local_access) + + # Skip if no network defined + [ -z "$network" ] && return + + # Check if firewall zone exists + local fw_zone_exists=$(uci show firewall | grep -c "firewall.*\.name='$network'") + + if [ "$fw_zone_exists" = "0" ]; then + # Create firewall zone + local fw_section=$(uci add firewall zone) + uci set firewall.$fw_section.name="$network" + uci set firewall.$fw_section.input="REJECT" + uci set firewall.$fw_section.output="ACCEPT" + uci set firewall.$fw_section.forward="REJECT" + uci add_list firewall.$fw_section.network="$network" + + # Add forwarding rule to WAN if internet access allowed + if [ "$internet_access" = "1" ]; then + local fwd_section=$(uci add firewall forwarding) + uci set firewall.$fwd_section.src="$network" + uci set firewall.$fwd_section.dest="wan" + fi + + # Add forwarding rule to LAN if local access allowed + if [ "$local_access" = "1" ]; then + local fwd_section=$(uci add firewall forwarding) + uci set firewall.$fwd_section.src="$network" + uci set firewall.$fwd_section.dest="lan" + fi + + uci commit firewall + log_event "info" "Created firewall zone: $network" + fi +} + +# Apply MAC-based firewall rules for client +apply_client_rules() { + local mac="$1" + local zone="$2" + + # Remove existing rules for this MAC + remove_client_rules "$mac" + + # Get zone configuration + local zone_network="" + local zone_internet="" + local zone_local="" + + config_load client-guardian + config_foreach check_zone zone "$zone" + + # Apply rules based on zone + if [ "$zone" = "blocked" ] || [ "$zone_network" = "null" ]; then + # Full block - drop all traffic from this MAC + local rule_section=$(uci add firewall rule) + uci set firewall.$rule_section.src="*" + uci set firewall.$rule_section.src_mac="$mac" + uci set firewall.$rule_section.target="DROP" + uci set firewall.$rule_section.name="CG_BLOCK_$mac" + uci commit firewall + log_event "info" "Applied BLOCK rule for MAC: $mac" + else + # Zone-based access control + # Allow DHCP for client + local rule_section=$(uci add firewall rule) + uci set firewall.$rule_section.src="*" + uci set firewall.$rule_section.src_mac="$mac" + uci set firewall.$rule_section.proto="udp" + uci set firewall.$rule_section.dest_port="67-68" + uci set firewall.$rule_section.target="ACCEPT" + uci set firewall.$rule_section.name="CG_DHCP_$mac" + + # Allow DNS + rule_section=$(uci add firewall rule) + uci set firewall.$rule_section.src="*" + uci set firewall.$rule_section.src_mac="$mac" + uci set firewall.$rule_section.proto="udp" + uci set firewall.$rule_section.dest_port="53" + uci set firewall.$rule_section.target="ACCEPT" + uci set firewall.$rule_section.name="CG_DNS_$mac" + + uci commit firewall + log_event "info" "Applied zone rules for MAC: $mac in zone: $zone" + fi + + # Reload firewall + /etc/init.d/firewall reload >/dev/null 2>&1 & +} + +# Remove firewall rules for client +remove_client_rules() { + local mac="$1" + mac=$(echo "$mac" | tr 'a-f' 'A-F') # Firewall rules use uppercase + + # Find and remove rules with this MAC + uci show firewall | grep -i "$mac" | cut -d. -f1-2 | sort -u | while read rule; do + uci delete "$rule" 2>/dev/null + done + uci commit firewall 2>/dev/null +} + +# Helper to find zone config +check_zone() { + local section="$1" + local target_zone="$2" + + if [ "$section" = "$target_zone" ]; then + zone_network=$(uci -q get client-guardian.$section.network) + zone_internet=$(uci -q get client-guardian.$section.internet_access) + zone_local=$(uci -q get client-guardian.$section.local_access) + fi +} + # Approve client approve_client() { read input @@ -506,10 +1181,10 @@ ban_client() { uci set client-guardian.$section.ban_reason="$reason" uci set client-guardian.$section.ban_date="$(date '+%Y-%m-%d %H:%M:%S')" uci commit client-guardian - - # Block in firewall - block_client "$mac" - + + # Apply firewall block rules + apply_client_rules "$mac" "blocked" + log_event "warning" "Client banned: $mac - Reason: $reason" # Send alert @@ -662,32 +1337,6 @@ update_zone() { json_dump } -# Update portal settings -update_portal() { - read input - json_load "$input" - json_get_var title title - json_get_var subtitle subtitle - json_get_var accent_color accent_color - json_get_var auth_method auth_method - json_get_var guest_password guest_password - - json_init - - [ -n "$title" ] && uci set client-guardian.portal.title="$title" - [ -n "$subtitle" ] && uci set client-guardian.portal.subtitle="$subtitle" - [ -n "$accent_color" ] && uci set client-guardian.portal.accent_color="$accent_color" - [ -n "$auth_method" ] && uci set client-guardian.portal.auth_method="$auth_method" - [ -n "$guest_password" ] && uci set client-guardian.portal.guest_password="$guest_password" - - uci commit client-guardian - - log_event "info" "Portal settings updated" - - json_add_boolean "success" 1 - json_add_string "message" "Portal updated successfully" - json_dump -} # Helper: Apply client firewall rules apply_client_rules() { @@ -752,68 +1401,16 @@ send_alert_internal() { # Nodogsplash Captive Portal Integration # =================================== -# List active captive portal sessions (nodogsplash) -list_sessions() { - json_init - json_add_array "sessions" - - # Check if nodogsplash is running - if pidof nodogsplash >/dev/null; then - # Get sessions from ndsctl - ndsctl status 2>/dev/null | grep -A 100 "Client" | while read line; do - # Parse ndsctl output - # Format: IP MAC Duration Download Upload - if echo "$line" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"; then - local ip=$(echo "$line" | awk '{print $1}') - local mac=$(echo "$line" | awk '{print $2}' | tr 'A-F' 'a-f') - local duration=$(echo "$line" | awk '{print $3}') - local downloaded=$(echo "$line" | awk '{print $4}') - local uploaded=$(echo "$line" | awk '{print $5}') - - # Get hostname - local hostname=$(grep -i "$mac" /tmp/dhcp.leases 2>/dev/null | awk '{print $4}') - [ -z "$hostname" ] && hostname="Unknown" - - json_add_object - json_add_string "ip" "$ip" - json_add_string "mac" "$mac" - json_add_string "hostname" "$hostname" - json_add_int "duration" "$duration" - json_add_int "downloaded" "$downloaded" - json_add_int "uploaded" "$uploaded" - json_add_string "state" "authenticated" - json_close_object - fi - done - fi - - json_close_array - - # Add nodogsplash status - json_add_object "nodogsplash" - if pidof nodogsplash >/dev/null; then - json_add_boolean "running" 1 - json_add_string "status" "active" - else - json_add_boolean "running" 0 - json_add_string "status" "stopped" - fi - json_close_object - - json_dump -} # Get default policy get_policy() { json_init local policy=$(uci -q get client-guardian.config.default_policy || echo "captive") - local portal_enabled=$(uci -q get client-guardian.portal.enabled || echo "1") local auto_approve=$(uci -q get client-guardian.config.auto_approve || echo "0") local session_timeout=$(uci -q get client-guardian.config.session_timeout || echo "86400") json_add_string "default_policy" "$policy" - json_add_boolean "portal_enabled" "$portal_enabled" json_add_boolean "auto_approve" "$auto_approve" json_add_int "session_timeout" "$session_timeout" @@ -831,7 +1428,6 @@ set_policy() { read input json_load "$input" json_get_var policy policy - json_get_var portal_enabled portal_enabled json_get_var auto_approve auto_approve json_get_var session_timeout session_timeout @@ -857,7 +1453,6 @@ set_policy() { ;; esac - [ -n "$portal_enabled" ] && uci set client-guardian.portal.enabled="$portal_enabled" [ -n "$auto_approve" ] && uci set client-guardian.config.auto_approve="$auto_approve" [ -n "$session_timeout" ] && uci set client-guardian.config.session_timeout="$session_timeout" @@ -878,102 +1473,8 @@ set_policy() { } # Authorize client via nodogsplash -authorize_client() { - read input - json_load "$input" - json_get_var mac mac - json_get_var ip ip - - json_init - - if [ -z "$mac" ]; then - json_add_boolean "success" 0 - json_add_string "error" "MAC address required" - json_dump - return - fi - - mac=$(echo "$mac" | tr 'A-F' 'a-f') - - # Use ndsctl to authorize - if pidof nodogsplash >/dev/null; then - if [ -n "$ip" ]; then - # Authorize by IP if provided - ndsctl auth "$ip" 2>&1 - else - # Find IP by MAC - local client_ip=$(cat /proc/net/arp | grep -i "$mac" | awk '{print $1}' | head -1) - if [ -n "$client_ip" ]; then - ndsctl auth "$client_ip" 2>&1 - ip="$client_ip" - else - json_add_boolean "success" 0 - json_add_string "error" "Client not found or offline" - json_dump - return - fi - fi - - log_event "info" "Client authorized via nodogsplash: $mac ($ip)" - - json_add_boolean "success" 1 - json_add_string "message" "Client $mac authorized" - json_add_string "ip" "$ip" - else - json_add_boolean "success" 0 - json_add_string "error" "Nodogsplash not running" - fi - - json_dump -} # Deauthorize client via nodogsplash -deauthorize_client() { - read input - json_load "$input" - json_get_var mac mac - json_get_var ip ip - - json_init - - if [ -z "$mac" ]; then - json_add_boolean "success" 0 - json_add_string "error" "MAC address required" - json_dump - return - fi - - mac=$(echo "$mac" | tr 'A-F' 'a-f') - - # Use ndsctl to deauthorize - if pidof nodogsplash >/dev/null; then - if [ -n "$ip" ]; then - ndsctl deauth "$ip" 2>&1 - else - # Find IP by MAC - local client_ip=$(cat /proc/net/arp | grep -i "$mac" | awk '{print $1}' | head -1) - if [ -n "$client_ip" ]; then - ndsctl deauth "$client_ip" 2>&1 - ip="$client_ip" - else - json_add_boolean "success" 0 - json_add_string "error" "Client not found in active sessions" - json_dump - return - fi - fi - - log_event "info" "Client deauthorized via nodogsplash: $mac ($ip)" - - json_add_boolean "success" 1 - json_add_string "message" "Client $mac deauthorized" - else - json_add_boolean "success" 0 - json_add_string "error" "Nodogsplash not running" - fi - - json_dump -} # Get client details get_client() { @@ -1034,7 +1535,7 @@ get_client() { # Main dispatcher case "$1" in list) - echo '{"status":{},"clients":{},"zones":{},"parental":{},"portal":{},"alerts":{},"logs":{"limit":"int","level":"str"},"approve_client":{"mac":"str","name":"str","zone":"str","notes":"str"},"ban_client":{"mac":"str","reason":"str"},"quarantine_client":{"mac":"str"},"update_client":{"section":"str","name":"str","zone":"str","notes":"str","daily_quota":"int","static_ip":"str"},"update_zone":{"id":"str","name":"str","bandwidth_limit":"int","content_filter":"str"},"update_portal":{"title":"str","subtitle":"str","accent_color":"str"},"send_test_alert":{"type":"str"},"list_sessions":{},"get_policy":{},"set_policy":{"policy":"str","portal_enabled":"bool","auto_approve":"bool","session_timeout":"int"},"authorize_client":{"mac":"str","ip":"str"},"deauthorize_client":{"mac":"str","ip":"str"},"get_client":{"mac":"str"}}' + echo '{"status":{},"clients":{},"zones":{},"parental":{},"alerts":{},"logs":{"limit":"int","level":"str"},"approve_client":{"mac":"str","name":"str","zone":"str","notes":"str"},"ban_client":{"mac":"str","reason":"str"},"quarantine_client":{"mac":"str"},"update_client":{"section":"str","name":"str","zone":"str","notes":"str","daily_quota":"int","static_ip":"str"},"update_zone":{"id":"str","name":"str","bandwidth_limit":"int","content_filter":"str"},"send_test_alert":{"type":"str"},"get_policy":{},"set_policy":{"policy":"str","auto_approve":"bool","session_timeout":"int"},"get_client":{"mac":"str"},"sync_zones":{},"list_profiles":{},"apply_profile":{"profile_id":"str","auto_refresh":"str","refresh_interval":"str","threat_enabled":"str","auto_ban_threshold":"str","auto_quarantine_threshold":"str"}}' ;; call) case "$2" in @@ -1042,7 +1543,6 @@ case "$1" in clients) get_clients ;; zones) get_zones ;; parental) get_parental ;; - portal) get_portal ;; alerts) get_alerts ;; logs) get_logs ;; approve_client) approve_client ;; @@ -1050,14 +1550,19 @@ case "$1" in quarantine_client) quarantine_client ;; update_client) update_client ;; update_zone) update_zone ;; - update_portal) update_portal ;; send_test_alert) send_test_alert ;; - list_sessions) list_sessions ;; get_policy) get_policy ;; set_policy) set_policy ;; - authorize_client) authorize_client ;; - deauthorize_client) deauthorize_client ;; get_client) get_client ;; + sync_zones) + json_init + sync_firewall_zones + json_add_boolean "success" 1 + json_add_string "message" "Firewall zones synchronized" + json_dump + ;; + list_profiles) list_profiles ;; + apply_profile) apply_profile ;; *) echo '{"error": "Unknown method"}' ;; esac ;; diff --git a/package/secubox/luci-app-client-guardian/root/usr/share/luci/menu.d/luci-app-client-guardian.json b/package/secubox/luci-app-client-guardian/root/usr/share/luci/menu.d/luci-app-client-guardian.json index 8a03d194..3d0ff564 100644 --- a/package/secubox/luci-app-client-guardian/root/usr/share/luci/menu.d/luci-app-client-guardian.json +++ b/package/secubox/luci-app-client-guardian/root/usr/share/luci/menu.d/luci-app-client-guardian.json @@ -17,6 +17,14 @@ "path": "client-guardian/overview" } }, + "admin/secubox/security/client-guardian/wizard": { + "title": "Setup Wizard", + "order": 15, + "action": { + "type": "view", + "path": "client-guardian/wizard" + } + }, "admin/secubox/security/client-guardian/clients": { "title": "Clients", "order": 20, @@ -80,5 +88,13 @@ "type": "view", "path": "client-guardian/settings" } + }, + "admin/secubox/security/client-guardian/debug": { + "title": "Debug Console", + "order": 95, + "action": { + "type": "view", + "path": "client-guardian/debug" + } } } \ No newline at end of file diff --git a/package/secubox/luci-app-client-guardian/root/usr/share/rpcd/acl.d/luci-app-client-guardian.json b/package/secubox/luci-app-client-guardian/root/usr/share/rpcd/acl.d/luci-app-client-guardian.json index 3cb62d70..ade504a6 100644 --- a/package/secubox/luci-app-client-guardian/root/usr/share/rpcd/acl.d/luci-app-client-guardian.json +++ b/package/secubox/luci-app-client-guardian/root/usr/share/rpcd/acl.d/luci-app-client-guardian.json @@ -3,7 +3,7 @@ "description": "Grant access to LuCI Client Guardian Dashboard", "read": { "ubus": { - "luci.client-guardian": [ "status", "clients", "get_client", "zones", "parental", "portal", "alerts", "logs", "list_sessions", "get_policy" ], + "luci.client-guardian": [ "status", "clients", "get_client", "zones", "parental", "portal", "alerts", "logs", "list_sessions", "get_policy", "list_profiles" ], "system": [ "info", "board" ], "network.interface": [ "status", "dump" ], "file": [ "read", "stat" ] @@ -18,9 +18,10 @@ }, "write": { "ubus": { - "luci.client-guardian": [ "approve_client", "ban_client", "quarantine_client", "update_client", "update_zone", "update_portal", "send_test_alert", "set_policy", "authorize_client", "deauthorize_client" ] + "luci.client-guardian": [ "approve_client", "ban_client", "quarantine_client", "update_client", "update_zone", "update_portal", "send_test_alert", "set_policy", "authorize_client", "deauthorize_client", "sync_zones", "apply_profile" ], + "uci": [ "apply", "commit", "set", "delete", "add", "reorder", "changes" ] }, - "uci": [ "client-guardian" ] + "uci": [ "client-guardian", "firewall" ] } } }