|
@@ -2808,36 +2808,148 @@ function populateGeoIPSelect(selectId, currentVal = '') {
|
|
|
sel.appendChild(grpCountries);
|
|
sel.appendChild(grpCountries);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ==================== FIREWALL FILTER MODAL (ROUTEROS WEBFIG / WINBOX V4) ====================
|
|
|
|
|
+
|
|
|
|
|
+function switchFwRuleTab(tab) {
|
|
|
|
|
+ state.activeFwRuleTab = tab;
|
|
|
|
|
+ document.querySelectorAll('.fw-rule-tab-btn').forEach(btn => {
|
|
|
|
|
+ btn.classList.remove('bg-[#182638]', 'text-white', 'border-sky-400');
|
|
|
|
|
+ btn.classList.add('text-slate-400', 'border-transparent');
|
|
|
|
|
+ });
|
|
|
|
|
+ const activeBtn = document.getElementById(`fw-tab-btn-${tab}`);
|
|
|
|
|
+ if (activeBtn) {
|
|
|
|
|
+ activeBtn.classList.remove('text-slate-400', 'border-transparent');
|
|
|
|
|
+ activeBtn.classList.add('bg-[#182638]', 'text-white', 'border-sky-400');
|
|
|
|
|
+ }
|
|
|
|
|
+ document.querySelectorAll('.fw-rule-pane').forEach(p => p.classList.add('hidden'));
|
|
|
|
|
+ const activePane = document.getElementById(`fw-pane-${tab}`);
|
|
|
|
|
+ if (activePane) activePane.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ if (tab === 'statistics') {
|
|
|
|
|
+ startFwStatPolling();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stopFwStatPolling();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toggleFwField(fieldKey, expand) {
|
|
|
|
|
+ const plusBtn = document.getElementById(`fw-plus-${fieldKey}`);
|
|
|
|
|
+ const fieldBox = document.getElementById(`fw-field-${fieldKey}`);
|
|
|
|
|
+ if (!plusBtn || !fieldBox) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (expand) {
|
|
|
|
|
+ plusBtn.classList.add('hidden');
|
|
|
|
|
+ fieldBox.classList.remove('hidden');
|
|
|
|
|
+ const valInput = document.getElementById(`fw-val-${fieldKey}`);
|
|
|
|
|
+ if (valInput) valInput.focus();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fieldBox.classList.add('hidden');
|
|
|
|
|
+ plusBtn.classList.remove('hidden');
|
|
|
|
|
+ const valInput = document.getElementById(`fw-val-${fieldKey}`);
|
|
|
|
|
+ if (valInput) {
|
|
|
|
|
+ if (valInput.tagName === 'SELECT') {
|
|
|
|
|
+ valInput.selectedIndex = 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ valInput.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const invInput = document.getElementById(`fw-inv-${fieldKey}`);
|
|
|
|
|
+ if (invInput) invInput.checked = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (fieldKey === 'cs') {
|
|
|
|
|
+ ['filter-modal-cs-established', 'filter-modal-cs-related', 'filter-modal-cs-new', 'filter-modal-cs-invalid', 'filter-modal-cs-untracked'].forEach(id => {
|
|
|
|
|
+ const el = document.getElementById(id);
|
|
|
|
|
+ if (el) el.checked = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fieldKey === 'natstate') {
|
|
|
|
|
+ ['filter-modal-nat-srcnat', 'filter-modal-nat-dstnat'].forEach(id => {
|
|
|
|
|
+ const el = document.getElementById(id);
|
|
|
|
|
+ if (el) el.checked = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fieldKey === 'days') {
|
|
|
|
|
+ ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].forEach(d => {
|
|
|
|
|
+ const el = document.getElementById(`fw-day-${d}`);
|
|
|
|
|
+ if (el) el.checked = true;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function setFwFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
|
|
+ if (value !== undefined && value !== null && value !== '' && value !== 'all') {
|
|
|
|
|
+ toggleFwField(fieldKey, true);
|
|
|
|
|
+ let strVal = String(value);
|
|
|
|
|
+ if (hasInvert) {
|
|
|
|
|
+ const inv = strVal.startsWith('!');
|
|
|
|
|
+ if (inv) strVal = strVal.slice(1);
|
|
|
|
|
+ const invEl = document.getElementById(`fw-inv-${fieldKey}`);
|
|
|
|
|
+ if (invEl) invEl.checked = inv;
|
|
|
|
|
+ }
|
|
|
|
|
+ const valEl = document.getElementById(`fw-val-${fieldKey}`);
|
|
|
|
|
+ if (valEl) valEl.value = strVal;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleFwField(fieldKey, false);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getFwFieldVal(fieldKey, hasInvert = false) {
|
|
|
|
|
+ const fieldBox = document.getElementById(`fw-field-${fieldKey}`);
|
|
|
|
|
+ if (!fieldBox || fieldBox.classList.contains('hidden')) return '';
|
|
|
|
|
+ const valEl = document.getElementById(`fw-val-${fieldKey}`);
|
|
|
|
|
+ let v = valEl ? valEl.value.trim() : '';
|
|
|
|
|
+ if (!v) return '';
|
|
|
|
|
+ if (hasInvert) {
|
|
|
|
|
+ const invEl = document.getElementById(`fw-inv-${fieldKey}`);
|
|
|
|
|
+ if (invEl && invEl.checked) {
|
|
|
|
|
+ v = '!' + v.replace(/^!/, '');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return v;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toggleMaximizeFilterModal() {
|
|
|
|
|
+ const win = document.getElementById('filter-modal-window');
|
|
|
|
|
+ if (!win) return;
|
|
|
|
|
+ if (win.classList.contains('max-w-[720px]')) {
|
|
|
|
|
+ win.classList.remove('max-w-[720px]', 'max-h-[92vh]');
|
|
|
|
|
+ win.classList.add('max-w-[96vw]', 'h-[94vh]');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ win.classList.remove('max-w-[96vw]', 'h-[94vh]');
|
|
|
|
|
+ win.classList.add('max-w-[720px]', 'max-h-[92vh]');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function openAddFilterModal() {
|
|
function openAddFilterModal() {
|
|
|
- document.getElementById('filter-modal-title').innerText = 'Add Firewall Filter Rule';
|
|
|
|
|
|
|
+ switchFwRuleTab('general');
|
|
|
|
|
+ const badge = document.getElementById('fw-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = 'New...';
|
|
|
document.getElementById('filter-modal-id').value = '';
|
|
document.getElementById('filter-modal-id').value = '';
|
|
|
|
|
+ document.getElementById('filter-modal-enabled').checked = true;
|
|
|
|
|
+ document.getElementById('filter-modal-comment').value = '';
|
|
|
document.getElementById('filter-modal-chain').value = 'forward';
|
|
document.getElementById('filter-modal-chain').value = 'forward';
|
|
|
document.getElementById('filter-modal-action').value = 'accept';
|
|
document.getElementById('filter-modal-action').value = 'accept';
|
|
|
- document.getElementById('filter-modal-proto').value = 'all';
|
|
|
|
|
- document.getElementById('filter-modal-src').value = '';
|
|
|
|
|
- document.getElementById('filter-modal-dst').value = '';
|
|
|
|
|
- document.getElementById('filter-modal-sport').value = '';
|
|
|
|
|
- document.getElementById('filter-modal-dport').value = '';
|
|
|
|
|
- document.getElementById('filter-modal-connmark').value = '';
|
|
|
|
|
- document.getElementById('filter-modal-comment').value = '';
|
|
|
|
|
- document.getElementById('filter-modal-disabled').checked = false;
|
|
|
|
|
-
|
|
|
|
|
- document.getElementById('filter-modal-cs-established').checked = false;
|
|
|
|
|
- document.getElementById('filter-modal-cs-related').checked = false;
|
|
|
|
|
- document.getElementById('filter-modal-cs-new').checked = false;
|
|
|
|
|
- document.getElementById('filter-modal-cs-invalid').checked = false;
|
|
|
|
|
-
|
|
|
|
|
- populateInterfaceSelect('filter-modal-iniface');
|
|
|
|
|
- populateInterfaceSelect('filter-modal-outiface');
|
|
|
|
|
- if (document.getElementById('filter-modal-srclist-inv')) document.getElementById('filter-modal-srclist-inv').checked = false;
|
|
|
|
|
- if (document.getElementById('filter-modal-dstlist-inv')) document.getElementById('filter-modal-dstlist-inv').checked = false;
|
|
|
|
|
- populateAddressListSelect('filter-modal-srclist');
|
|
|
|
|
- populateAddressListSelect('filter-modal-dstlist');
|
|
|
|
|
-
|
|
|
|
|
- if (document.getElementById('filter-modal-srcgeoip-inv')) document.getElementById('filter-modal-srcgeoip-inv').checked = false;
|
|
|
|
|
- if (document.getElementById('filter-modal-dstgeoip-inv')) document.getElementById('filter-modal-dstgeoip-inv').checked = false;
|
|
|
|
|
- populateGeoIPSelect('filter-modal-srcgeoip');
|
|
|
|
|
- populateGeoIPSelect('filter-modal-dstgeoip');
|
|
|
|
|
|
|
+ document.getElementById('filter-modal-log').checked = false;
|
|
|
|
|
+
|
|
|
|
|
+ populateInterfaceSelect('fw-val-iniface');
|
|
|
|
|
+ populateInterfaceSelect('fw-val-outiface');
|
|
|
|
|
+ populateAddressListSelect('fw-val-srclist');
|
|
|
|
|
+ populateAddressListSelect('fw-val-dstlist');
|
|
|
|
|
+
|
|
|
|
|
+ const allFieldKeys = [
|
|
|
|
|
+ 'src', 'dst', 'srclist', 'dstlist', 'proto', 'sport', 'dport', 'anyport',
|
|
|
|
|
+ 'iniface', 'outiface', 'inifacelist', 'outifacelist', 'pktmark', 'connmark',
|
|
|
|
|
+ 'routingmark', 'conntype', 'cs', 'natstate',
|
|
|
|
|
+ 'layer7', 'content', 'connbytes', 'connrate', 'pcc', 'srcmac',
|
|
|
|
|
+ 'outbridgeport', 'inbridgeport', 'inbridgeportlist', 'outbridgeportlist',
|
|
|
|
|
+ 'ipsec', 'tlshost', 'ingresspriority', 'priority', 'dscp', 'tcpmss',
|
|
|
|
|
+ 'pktsize', 'random', 'tcpflags', 'icmpoptions', 'ipv4options', 'ttl',
|
|
|
|
|
+ 'time', 'days', 'limit', 'dstlimit', 'psd', 'logprefix'
|
|
|
|
|
+ ];
|
|
|
|
|
+ allFieldKeys.forEach(k => toggleFwField(k, false));
|
|
|
|
|
+
|
|
|
|
|
+ resetCurrentRuleCounters();
|
|
|
toggleFilterActionFields();
|
|
toggleFilterActionFields();
|
|
|
|
|
|
|
|
document.getElementById('filter-modal').classList.remove('hidden');
|
|
document.getElementById('filter-modal').classList.remove('hidden');
|
|
@@ -2847,140 +2959,469 @@ function openEditFilterModal(id) {
|
|
|
const r = (state.filterRules || []).find(rule => rule.id === id);
|
|
const r = (state.filterRules || []).find(rule => rule.id === id);
|
|
|
if (!r) return;
|
|
if (!r) return;
|
|
|
|
|
|
|
|
- document.getElementById('filter-modal-title').innerText = `Edit Filter Rule (#${r.order})`;
|
|
|
|
|
|
|
+ switchFwRuleTab('general');
|
|
|
|
|
+ const badge = document.getElementById('fw-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = `#${r.order != null ? r.order : r.id}`;
|
|
|
|
|
+
|
|
|
document.getElementById('filter-modal-id').value = r.id;
|
|
document.getElementById('filter-modal-id').value = r.id;
|
|
|
|
|
+ document.getElementById('filter-modal-enabled').checked = !r.disabled;
|
|
|
|
|
+ document.getElementById('filter-modal-comment').value = r.comment || '';
|
|
|
document.getElementById('filter-modal-chain').value = r.chain || 'forward';
|
|
document.getElementById('filter-modal-chain').value = r.chain || 'forward';
|
|
|
document.getElementById('filter-modal-action').value = r.action || 'accept';
|
|
document.getElementById('filter-modal-action').value = r.action || 'accept';
|
|
|
- document.getElementById('filter-modal-proto').value = r.protocol || 'all';
|
|
|
|
|
- document.getElementById('filter-modal-src').value = r.src_address || '';
|
|
|
|
|
- document.getElementById('filter-modal-dst').value = r.dst_address || '';
|
|
|
|
|
- document.getElementById('filter-modal-sport').value = r.src_port || '';
|
|
|
|
|
- document.getElementById('filter-modal-dport').value = r.dst_port || '';
|
|
|
|
|
- document.getElementById('filter-modal-connmark').value = r.connection_mark || '';
|
|
|
|
|
- document.getElementById('filter-modal-comment').value = r.comment || '';
|
|
|
|
|
- document.getElementById('filter-modal-disabled').checked = !!r.disabled;
|
|
|
|
|
|
|
+ document.getElementById('filter-modal-log').checked = !!r.log;
|
|
|
|
|
+
|
|
|
|
|
+ populateInterfaceSelect('fw-val-iniface', (r.in_interface || '').replace(/^!/, ''));
|
|
|
|
|
+ populateInterfaceSelect('fw-val-outiface', (r.out_interface || '').replace(/^!/, ''));
|
|
|
|
|
+ populateAddressListSelect('fw-val-srclist', (r.src_address_list || '').replace(/^!/, ''));
|
|
|
|
|
+ populateAddressListSelect('fw-val-dstlist', (r.dst_address_list || '').replace(/^!/, ''));
|
|
|
|
|
+
|
|
|
|
|
+ setFwFieldVal('src', r.src_address, true);
|
|
|
|
|
+ setFwFieldVal('dst', r.dst_address, true);
|
|
|
|
|
+ setFwFieldVal('srclist', r.src_address_list, true);
|
|
|
|
|
+ setFwFieldVal('dstlist', r.dst_address_list, true);
|
|
|
|
|
+ if (r.protocol && r.protocol !== 'all') {
|
|
|
|
|
+ setFwFieldVal('proto', r.protocol, true);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleFwField('proto', false);
|
|
|
|
|
+ }
|
|
|
|
|
+ setFwFieldVal('sport', r.src_port, true);
|
|
|
|
|
+ setFwFieldVal('dport', r.dst_port, true);
|
|
|
|
|
+ setFwFieldVal('anyport', r.any_port, true);
|
|
|
|
|
+ setFwFieldVal('iniface', r.in_interface, true);
|
|
|
|
|
+ setFwFieldVal('outiface', r.out_interface, true);
|
|
|
|
|
+ setFwFieldVal('inifacelist', r.in_interface_list, true);
|
|
|
|
|
+ setFwFieldVal('outifacelist', r.out_interface_list, true);
|
|
|
|
|
+ setFwFieldVal('pktmark', r.packet_mark, true);
|
|
|
|
|
+ setFwFieldVal('connmark', r.connection_mark, true);
|
|
|
|
|
+ setFwFieldVal('routingmark', r.routing_mark, true);
|
|
|
|
|
+ setFwFieldVal('conntype', r.connection_type, false);
|
|
|
|
|
|
|
|
const states = r.connection_state || [];
|
|
const states = r.connection_state || [];
|
|
|
- document.getElementById('filter-modal-cs-established').checked = states.includes('established');
|
|
|
|
|
- document.getElementById('filter-modal-cs-related').checked = states.includes('related');
|
|
|
|
|
- document.getElementById('filter-modal-cs-new').checked = states.includes('new');
|
|
|
|
|
- document.getElementById('filter-modal-cs-invalid').checked = states.includes('invalid');
|
|
|
|
|
-
|
|
|
|
|
- populateInterfaceSelect('filter-modal-iniface', r.in_interface);
|
|
|
|
|
- populateInterfaceSelect('filter-modal-outiface', r.out_interface);
|
|
|
|
|
|
|
+ if (states.length > 0) {
|
|
|
|
|
+ toggleFwField('cs', true);
|
|
|
|
|
+ document.getElementById('filter-modal-cs-established').checked = states.includes('established');
|
|
|
|
|
+ document.getElementById('filter-modal-cs-related').checked = states.includes('related');
|
|
|
|
|
+ document.getElementById('filter-modal-cs-new').checked = states.includes('new');
|
|
|
|
|
+ document.getElementById('filter-modal-cs-invalid').checked = states.includes('invalid');
|
|
|
|
|
+ document.getElementById('filter-modal-cs-untracked').checked = states.includes('untracked');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleFwField('cs', false);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const srcInv = (r.src_address_list || '').startsWith('!');
|
|
|
|
|
- const dstInv = (r.dst_address_list || '').startsWith('!');
|
|
|
|
|
- if (document.getElementById('filter-modal-srclist-inv')) document.getElementById('filter-modal-srclist-inv').checked = srcInv;
|
|
|
|
|
- if (document.getElementById('filter-modal-dstlist-inv')) document.getElementById('filter-modal-dstlist-inv').checked = dstInv;
|
|
|
|
|
- populateAddressListSelect('filter-modal-srclist', r.src_address_list || '');
|
|
|
|
|
- populateAddressListSelect('filter-modal-dstlist', r.dst_address_list || '');
|
|
|
|
|
|
|
+ const natStates = r.connection_nat_state || [];
|
|
|
|
|
+ if (natStates.length > 0) {
|
|
|
|
|
+ toggleFwField('natstate', true);
|
|
|
|
|
+ document.getElementById('filter-modal-nat-srcnat').checked = natStates.includes('srcnat');
|
|
|
|
|
+ document.getElementById('filter-modal-nat-dstnat').checked = natStates.includes('dstnat');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleFwField('natstate', false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Advanced tab fields
|
|
|
|
|
+ setFwFieldVal('layer7', r.layer7_protocol, false);
|
|
|
|
|
+ setFwFieldVal('content', r.content, false);
|
|
|
|
|
+ setFwFieldVal('connbytes', r.connection_bytes, false);
|
|
|
|
|
+ setFwFieldVal('connrate', r.connection_rate, false);
|
|
|
|
|
+ setFwFieldVal('pcc', r.pcc, false);
|
|
|
|
|
+ setFwFieldVal('srcmac', r.src_mac_address, true);
|
|
|
|
|
+ setFwFieldVal('outbridgeport', r.out_bridge_port, false);
|
|
|
|
|
+ setFwFieldVal('inbridgeport', r.in_bridge_port, false);
|
|
|
|
|
+ setFwFieldVal('inbridgeportlist', r.in_bridge_port_list, false);
|
|
|
|
|
+ setFwFieldVal('outbridgeportlist', r.out_bridge_port_list, false);
|
|
|
|
|
+ setFwFieldVal('ipsec', r.ipsec_policy, false);
|
|
|
|
|
+ setFwFieldVal('tlshost', r.tls_host, false);
|
|
|
|
|
+ setFwFieldVal('ingresspriority', r.ingress_priority, false);
|
|
|
|
|
+ setFwFieldVal('priority', r.priority, false);
|
|
|
|
|
+ setFwFieldVal('dscp', r.dscp, false);
|
|
|
|
|
+ setFwFieldVal('tcpmss', r.tcp_mss, false);
|
|
|
|
|
+ if (r.tos) {
|
|
|
|
|
+ const tosEl = document.getElementById('fw-val-tos');
|
|
|
|
|
+ if (tosEl) tosEl.value = r.tos;
|
|
|
|
|
+ }
|
|
|
|
|
+ setFwFieldVal('pktsize', r.packet_size, false);
|
|
|
|
|
+ setFwFieldVal('random', r.random, false);
|
|
|
|
|
+ setFwFieldVal('tcpflags', r.tcp_flags, false);
|
|
|
|
|
+ setFwFieldVal('icmpoptions', r.icmp_options, false);
|
|
|
|
|
+ setFwFieldVal('ipv4options', r.ipv4_options, false);
|
|
|
|
|
+ setFwFieldVal('ttl', r.ttl, false);
|
|
|
|
|
+
|
|
|
|
|
+ // Extra tab fields
|
|
|
|
|
+ setFwFieldVal('time', r.time, false);
|
|
|
|
|
+ if (r.days) {
|
|
|
|
|
+ toggleFwField('days', true);
|
|
|
|
|
+ ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].forEach(d => {
|
|
|
|
|
+ const chk = document.getElementById(`fw-day-${d}`);
|
|
|
|
|
+ if (chk) chk.checked = r.days.includes(d);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleFwField('days', false);
|
|
|
|
|
+ }
|
|
|
|
|
+ setFwFieldVal('limit', r.limit, false);
|
|
|
|
|
+ setFwFieldVal('dstlimit', r.dst_limit, false);
|
|
|
|
|
+ setFwFieldVal('psd', r.psd, false);
|
|
|
|
|
+
|
|
|
|
|
+ // Action fields
|
|
|
|
|
+ if (r.reject_with) {
|
|
|
|
|
+ const rwEl = document.getElementById('filter-modal-rejectwith');
|
|
|
|
|
+ if (rwEl) rwEl.value = r.reject_with;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (r.jump_target) {
|
|
|
|
|
+ const jtEl = document.getElementById('filter-modal-jumptarget');
|
|
|
|
|
+ if (jtEl) jtEl.value = r.jump_target;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (r.address_list) {
|
|
|
|
|
+ const alEl = document.getElementById('filter-modal-action-addrlist');
|
|
|
|
|
+ if (alEl) alEl.value = r.address_list;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (r.address_list_timeout) {
|
|
|
|
|
+ const toEl = document.getElementById('filter-modal-action-timeout');
|
|
|
|
|
+ if (toEl) toEl.value = r.address_list_timeout;
|
|
|
|
|
+ }
|
|
|
|
|
+ setFwFieldVal('logprefix', r.log_prefix, false);
|
|
|
|
|
+
|
|
|
|
|
+ // Statistics
|
|
|
|
|
+ const bEl = document.getElementById('fw-stat-bytes');
|
|
|
|
|
+ const pEl = document.getElementById('fw-stat-packets');
|
|
|
|
|
+ if (bEl) bEl.value = formatTraffic(r.bytes || 0).replace('/s', '');
|
|
|
|
|
+ if (pEl) pEl.value = (r.packets || 0).toLocaleString();
|
|
|
|
|
+ state.fwLastBytes = r.bytes || 0;
|
|
|
|
|
+ state.fwLastPackets = r.packets || 0;
|
|
|
|
|
+ state.fwLastTimestamp = Date.now();
|
|
|
|
|
+ state.fwStatRateHistory = new Array(30).fill(0);
|
|
|
|
|
+ state.fwStatPktHistory = new Array(30).fill(0);
|
|
|
|
|
+ renderFwSparkline('fw-stat-rate-svg', state.fwStatRateHistory, 'bps', 'fw-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('fw-stat-pktrate-svg', state.fwStatPktHistory, 'p/s', 'fw-stat-legend-pktrate');
|
|
|
|
|
|
|
|
- const srcGeoInv = (r.src_geoip || '').startsWith('!');
|
|
|
|
|
- const dstGeoInv = (r.dst_geoip || '').startsWith('!');
|
|
|
|
|
- if (document.getElementById('filter-modal-srcgeoip-inv')) document.getElementById('filter-modal-srcgeoip-inv').checked = srcGeoInv;
|
|
|
|
|
- if (document.getElementById('filter-modal-dstgeoip-inv')) document.getElementById('filter-modal-dstgeoip-inv').checked = dstGeoInv;
|
|
|
|
|
- populateGeoIPSelect('filter-modal-srcgeoip', r.src_geoip || '');
|
|
|
|
|
- populateGeoIPSelect('filter-modal-dstgeoip', r.dst_geoip || '');
|
|
|
|
|
toggleFilterActionFields();
|
|
toggleFilterActionFields();
|
|
|
-
|
|
|
|
|
document.getElementById('filter-modal').classList.remove('hidden');
|
|
document.getElementById('filter-modal').classList.remove('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function closeFilterModal() {
|
|
function closeFilterModal() {
|
|
|
|
|
+ stopFwStatPolling();
|
|
|
document.getElementById('filter-modal').classList.add('hidden');
|
|
document.getElementById('filter-modal').classList.add('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function toggleFilterActionFields() {
|
|
function toggleFilterActionFields() {
|
|
|
- const act = document.getElementById('filter-modal-action').value;
|
|
|
|
|
|
|
+ const act = document.getElementById('filter-modal-action')?.value || 'accept';
|
|
|
const rejectBox = document.getElementById('filter-action-reject-box');
|
|
const rejectBox = document.getElementById('filter-action-reject-box');
|
|
|
|
|
+ const jumpBox = document.getElementById('filter-action-jump-box');
|
|
|
|
|
+ const addrBox = document.getElementById('filter-action-addrlist-box');
|
|
|
|
|
+
|
|
|
if (rejectBox) {
|
|
if (rejectBox) {
|
|
|
if (act === 'reject') rejectBox.classList.remove('hidden');
|
|
if (act === 'reject') rejectBox.classList.remove('hidden');
|
|
|
else rejectBox.classList.add('hidden');
|
|
else rejectBox.classList.add('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
+ if (jumpBox) {
|
|
|
|
|
+ if (act === 'jump') jumpBox.classList.remove('hidden');
|
|
|
|
|
+ else jumpBox.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (addrBox) {
|
|
|
|
|
+ if (act === 'add-dst-to-address-list' || act === 'add-src-to-address-list') addrBox.classList.remove('hidden');
|
|
|
|
|
+ else addrBox.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function submitSaveFilterRule(e) {
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
|
|
+function copyFilterRule() {
|
|
|
|
|
+ document.getElementById('filter-modal-id').value = '';
|
|
|
|
|
+ const badge = document.getElementById('fw-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = 'New...';
|
|
|
|
|
+ showToast('Rule Cloned', 'Ready to save as a new rule copy', 'info');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resetCurrentRuleCounters() {
|
|
|
|
|
+ state.fwStatRateHistory = new Array(30).fill(0);
|
|
|
|
|
+ state.fwStatPktHistory = new Array(30).fill(0);
|
|
|
|
|
+ state.fwLastBytes = 0;
|
|
|
|
|
+ state.fwLastPackets = 0;
|
|
|
|
|
+ state.fwLastTimestamp = Date.now();
|
|
|
|
|
+ const bEl = document.getElementById('fw-stat-bytes');
|
|
|
|
|
+ const pEl = document.getElementById('fw-stat-packets');
|
|
|
|
|
+ const rEl = document.getElementById('fw-stat-rate');
|
|
|
|
|
+ const prEl = document.getElementById('fw-stat-pktrate');
|
|
|
|
|
+ if (bEl) bEl.value = '0 B';
|
|
|
|
|
+ if (pEl) pEl.value = '0';
|
|
|
|
|
+ if (rEl) rEl.value = '0 bps';
|
|
|
|
|
+ if (prEl) prEl.value = '0 p/s';
|
|
|
|
|
+ renderFwSparkline('fw-stat-rate-svg', state.fwStatRateHistory, 'bps', 'fw-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('fw-stat-pktrate-svg', state.fwStatPktHistory, 'p/s', 'fw-stat-legend-pktrate');
|
|
|
|
|
+ showToast('Counters Reset', 'Rule counter display reset to 0', 'info');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function resetAllFwCounters() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/firewall/reset-counters`, { method: 'POST' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ resetCurrentRuleCounters();
|
|
|
|
|
+ showToast('All Counters Reset', 'All firewall counters zeroed', 'success');
|
|
|
|
|
+ loadFirewall(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function applyFilterRule(closeAfter = false) {
|
|
|
const id = document.getElementById('filter-modal-id').value;
|
|
const id = document.getElementById('filter-modal-id').value;
|
|
|
const chain = document.getElementById('filter-modal-chain').value;
|
|
const chain = document.getElementById('filter-modal-chain').value;
|
|
|
const action = document.getElementById('filter-modal-action').value;
|
|
const action = document.getElementById('filter-modal-action').value;
|
|
|
- const proto = document.getElementById('filter-modal-proto').value;
|
|
|
|
|
- const src = document.getElementById('filter-modal-src').value.trim();
|
|
|
|
|
- const dst = document.getElementById('filter-modal-dst').value.trim();
|
|
|
|
|
- const sport = document.getElementById('filter-modal-sport').value.trim();
|
|
|
|
|
- const dport = document.getElementById('filter-modal-dport').value.trim();
|
|
|
|
|
- const inIface = document.getElementById('filter-modal-iniface').value;
|
|
|
|
|
- const outIface = document.getElementById('filter-modal-outiface').value;
|
|
|
|
|
- const connmark = document.getElementById('filter-modal-connmark').value.trim();
|
|
|
|
|
-
|
|
|
|
|
- let srclist = document.getElementById('filter-modal-srclist')?.value || '';
|
|
|
|
|
- if (srclist && document.getElementById('filter-modal-srclist-inv')?.checked) {
|
|
|
|
|
- srclist = '!' + srclist.replace(/^!/, '');
|
|
|
|
|
- }
|
|
|
|
|
- let dstlist = document.getElementById('filter-modal-dstlist')?.value || '';
|
|
|
|
|
- if (dstlist && document.getElementById('filter-modal-dstlist-inv')?.checked) {
|
|
|
|
|
- dstlist = '!' + dstlist.replace(/^!/, '');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const comment = document.getElementById('filter-modal-comment').value.trim();
|
|
|
|
|
+ const enabled = document.getElementById('filter-modal-enabled').checked;
|
|
|
|
|
+
|
|
|
|
|
+ const src = getFwFieldVal('src', true);
|
|
|
|
|
+ const dst = getFwFieldVal('dst', true);
|
|
|
|
|
+ const srclist = getFwFieldVal('srclist', true);
|
|
|
|
|
+ const dstlist = getFwFieldVal('dstlist', true);
|
|
|
|
|
+ const proto = getFwFieldVal('proto', true);
|
|
|
|
|
+ const sport = getFwFieldVal('sport', true);
|
|
|
|
|
+ const dport = getFwFieldVal('dport', true);
|
|
|
|
|
+ const anyport = getFwFieldVal('anyport', true);
|
|
|
|
|
+ const iniface = getFwFieldVal('iniface', true);
|
|
|
|
|
+ const outiface = getFwFieldVal('outiface', true);
|
|
|
|
|
+ const inifacelist = getFwFieldVal('inifacelist', true);
|
|
|
|
|
+ const outifacelist = getFwFieldVal('outifacelist', true);
|
|
|
|
|
+ const pktmark = getFwFieldVal('pktmark', true);
|
|
|
|
|
+ const connmark = getFwFieldVal('connmark', true);
|
|
|
|
|
+ const routingmark = getFwFieldVal('routingmark', true);
|
|
|
|
|
+ const conntype = getFwFieldVal('conntype', false);
|
|
|
|
|
|
|
|
- let srcgeoip = document.getElementById('filter-modal-srcgeoip')?.value || '';
|
|
|
|
|
- if (srcgeoip && document.getElementById('filter-modal-srcgeoip-inv')?.checked) {
|
|
|
|
|
- srcgeoip = '!' + srcgeoip.replace(/^!/, '');
|
|
|
|
|
- }
|
|
|
|
|
- let dstgeoip = document.getElementById('filter-modal-dstgeoip')?.value || '';
|
|
|
|
|
- if (dstgeoip && document.getElementById('filter-modal-dstgeoip-inv')?.checked) {
|
|
|
|
|
- dstgeoip = '!' + dstgeoip.replace(/^!/, '');
|
|
|
|
|
|
|
+ const states = [];
|
|
|
|
|
+ const csBox = document.getElementById('fw-field-cs');
|
|
|
|
|
+ if (csBox && !csBox.classList.contains('hidden')) {
|
|
|
|
|
+ if (document.getElementById('filter-modal-cs-established')?.checked) states.push('established');
|
|
|
|
|
+ if (document.getElementById('filter-modal-cs-related')?.checked) states.push('related');
|
|
|
|
|
+ if (document.getElementById('filter-modal-cs-new')?.checked) states.push('new');
|
|
|
|
|
+ if (document.getElementById('filter-modal-cs-invalid')?.checked) states.push('invalid');
|
|
|
|
|
+ if (document.getElementById('filter-modal-cs-untracked')?.checked) states.push('untracked');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const comment = document.getElementById('filter-modal-comment').value.trim();
|
|
|
|
|
- const disabled = document.getElementById('filter-modal-disabled').checked;
|
|
|
|
|
- const rejectWith = document.getElementById('filter-modal-rejectwith')?.value || '';
|
|
|
|
|
|
|
+ const natStates = [];
|
|
|
|
|
+ const natBox = document.getElementById('fw-field-natstate');
|
|
|
|
|
+ if (natBox && !natBox.classList.contains('hidden')) {
|
|
|
|
|
+ if (document.getElementById('filter-modal-nat-srcnat')?.checked) natStates.push('srcnat');
|
|
|
|
|
+ if (document.getElementById('filter-modal-nat-dstnat')?.checked) natStates.push('dstnat');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const states = [];
|
|
|
|
|
- if (document.getElementById('filter-modal-cs-established').checked) states.push('established');
|
|
|
|
|
- if (document.getElementById('filter-modal-cs-related').checked) states.push('related');
|
|
|
|
|
- if (document.getElementById('filter-modal-cs-new').checked) states.push('new');
|
|
|
|
|
- if (document.getElementById('filter-modal-cs-invalid').checked) states.push('invalid');
|
|
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ id,
|
|
|
|
|
+ chain,
|
|
|
|
|
+ action,
|
|
|
|
|
+ protocol: proto || 'all',
|
|
|
|
|
+ src_address: src,
|
|
|
|
|
+ dst_address: dst,
|
|
|
|
|
+ src_port: sport,
|
|
|
|
|
+ dst_port: dport,
|
|
|
|
|
+ any_port: anyport,
|
|
|
|
|
+ in_interface: iniface,
|
|
|
|
|
+ out_interface: outiface,
|
|
|
|
|
+ in_interface_list: inifacelist,
|
|
|
|
|
+ out_interface_list: outifacelist,
|
|
|
|
|
+ packet_mark: pktmark,
|
|
|
|
|
+ connection_mark: connmark,
|
|
|
|
|
+ routing_mark: routingmark,
|
|
|
|
|
+ connection_type: conntype,
|
|
|
|
|
+ connection_state: states,
|
|
|
|
|
+ connection_nat_state: natStates,
|
|
|
|
|
+ src_address_list: srclist,
|
|
|
|
|
+ dst_address_list: dstlist,
|
|
|
|
|
+ comment,
|
|
|
|
|
+ disabled: !enabled,
|
|
|
|
|
+
|
|
|
|
|
+ // Advanced tab
|
|
|
|
|
+ layer7_protocol: getFwFieldVal('layer7', false),
|
|
|
|
|
+ content: getFwFieldVal('content', false),
|
|
|
|
|
+ connection_bytes: getFwFieldVal('connbytes', false),
|
|
|
|
|
+ connection_rate: getFwFieldVal('connrate', false),
|
|
|
|
|
+ pcc: getFwFieldVal('pcc', false),
|
|
|
|
|
+ src_mac_address: getFwFieldVal('srcmac', true),
|
|
|
|
|
+ out_bridge_port: getFwFieldVal('outbridgeport', false),
|
|
|
|
|
+ in_bridge_port: getFwFieldVal('inbridgeport', false),
|
|
|
|
|
+ in_bridge_port_list: getFwFieldVal('inbridgeportlist', false),
|
|
|
|
|
+ out_bridge_port_list: getFwFieldVal('outbridgeportlist', false),
|
|
|
|
|
+ ipsec_policy: getFwFieldVal('ipsec', false),
|
|
|
|
|
+ tls_host: getFwFieldVal('tlshost', false),
|
|
|
|
|
+ ingress_priority: getFwFieldVal('ingresspriority', false),
|
|
|
|
|
+ priority: getFwFieldVal('priority', false),
|
|
|
|
|
+ dscp: getFwFieldVal('dscp', false),
|
|
|
|
|
+ tcp_mss: getFwFieldVal('tcpmss', false),
|
|
|
|
|
+ tos: document.getElementById('fw-val-tos')?.value || '',
|
|
|
|
|
+ packet_size: getFwFieldVal('pktsize', false),
|
|
|
|
|
+ random: getFwFieldVal('random', false),
|
|
|
|
|
+ tcp_flags: getFwFieldVal('tcpflags', false),
|
|
|
|
|
+ icmp_options: getFwFieldVal('icmpoptions', false),
|
|
|
|
|
+ ipv4_options: getFwFieldVal('ipv4options', false),
|
|
|
|
|
+ ttl: getFwFieldVal('ttl', false),
|
|
|
|
|
+
|
|
|
|
|
+ // Extra tab
|
|
|
|
|
+ time: getFwFieldVal('time', false),
|
|
|
|
|
+ limit: getFwFieldVal('limit', false),
|
|
|
|
|
+ dst_limit: getFwFieldVal('dstlimit', false),
|
|
|
|
|
+ psd: getFwFieldVal('psd', false),
|
|
|
|
|
+
|
|
|
|
|
+ // Action tab
|
|
|
|
|
+ reject_with: action === 'reject' ? (document.getElementById('filter-modal-rejectwith')?.value || '') : '',
|
|
|
|
|
+ jump_target: action === 'jump' ? (document.getElementById('filter-modal-jumptarget')?.value || '') : '',
|
|
|
|
|
+ address_list: action.startsWith('add-') ? (document.getElementById('filter-modal-action-addrlist')?.value || '') : '',
|
|
|
|
|
+ address_list_timeout: action.startsWith('add-') ? (document.getElementById('filter-modal-action-timeout')?.value || '') : '',
|
|
|
|
|
+ log: document.getElementById('filter-modal-log')?.checked || false,
|
|
|
|
|
+ log_prefix: getFwFieldVal('logprefix', false)
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/filter`, {
|
|
const res = await fetch(`${API_BASE}/firewall/filter`, {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({
|
|
|
|
|
- id,
|
|
|
|
|
- chain,
|
|
|
|
|
- action,
|
|
|
|
|
- reject_with: rejectWith,
|
|
|
|
|
- protocol: proto,
|
|
|
|
|
- src_address: src,
|
|
|
|
|
- dst_address: dst,
|
|
|
|
|
- src_port: sport,
|
|
|
|
|
- dst_port: dport,
|
|
|
|
|
- in_interface: inIface,
|
|
|
|
|
- out_interface: outIface,
|
|
|
|
|
- connection_state: states,
|
|
|
|
|
- connection_mark: connmark,
|
|
|
|
|
- src_address_list: srclist,
|
|
|
|
|
- dst_address_list: dstlist,
|
|
|
|
|
- src_geoip: srcgeoip,
|
|
|
|
|
- dst_geoip: dstgeoip,
|
|
|
|
|
- comment,
|
|
|
|
|
- disabled
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
});
|
|
});
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
|
|
|
+ const data = await res.json();
|
|
|
showToast('Filter Rule Saved', 'Rule applied successfully to firewall', 'success');
|
|
showToast('Filter Rule Saved', 'Rule applied successfully to firewall', 'success');
|
|
|
- closeFilterModal();
|
|
|
|
|
loadFirewall(false);
|
|
loadFirewall(false);
|
|
|
|
|
+ if (closeAfter) {
|
|
|
|
|
+ closeFilterModal();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (data.id) {
|
|
|
|
|
+ document.getElementById('filter-modal-id').value = data.id;
|
|
|
|
|
+ const badge = document.getElementById('fw-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = `#${data.id}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
const err = await res.json();
|
|
const err = await res.json();
|
|
|
- showToast('Error', err.message, 'error');
|
|
|
|
|
|
|
+ showToast('Error', err.message || 'Failed to save rule', 'error');
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function submitSaveFilterRule(e) {
|
|
|
|
|
+ if (e && e.preventDefault) e.preventDefault();
|
|
|
|
|
+ return applyFilterRule(true);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function startFwStatPolling() {
|
|
|
|
|
+ stopFwStatPolling();
|
|
|
|
|
+ pollRuleStats();
|
|
|
|
|
+ state.fwStatInterval = setInterval(pollRuleStats, 1000);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function stopFwStatPolling() {
|
|
|
|
|
+ if (state.fwStatInterval) {
|
|
|
|
|
+ clearInterval(state.fwStatInterval);
|
|
|
|
|
+ state.fwStatInterval = null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function pollRuleStats() {
|
|
|
|
|
+ const id = document.getElementById('filter-modal-id')?.value;
|
|
|
|
|
+ if (!id) {
|
|
|
|
|
+ renderFwSparkline('fw-stat-rate-svg', state.fwStatRateHistory || new Array(30).fill(0), 'bps', 'fw-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('fw-stat-pktrate-svg', state.fwStatPktHistory || new Array(30).fill(0), 'p/s', 'fw-stat-legend-pktrate');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/firewall/filter`);
|
|
|
|
|
+ if (!res.ok) return;
|
|
|
|
|
+ const rules = await res.json();
|
|
|
|
|
+ const r = rules.find(x => x.id === id);
|
|
|
|
|
+ if (!r) return;
|
|
|
|
|
+
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ const dt = Math.max(0.2, (now - (state.fwLastTimestamp || now)) / 1000);
|
|
|
|
|
+ state.fwLastTimestamp = now;
|
|
|
|
|
+
|
|
|
|
|
+ const curBytes = r.bytes || 0;
|
|
|
|
|
+ const curPackets = r.packets || 0;
|
|
|
|
|
+
|
|
|
|
|
+ const deltaBytes = Math.max(0, curBytes - (state.fwLastBytes || curBytes));
|
|
|
|
|
+ const deltaPkts = Math.max(0, curPackets - (state.fwLastPackets || curPackets));
|
|
|
|
|
+
|
|
|
|
|
+ state.fwLastBytes = curBytes;
|
|
|
|
|
+ state.fwLastPackets = curPackets;
|
|
|
|
|
+
|
|
|
|
|
+ const rateBps = Math.round((deltaBytes * 8) / dt);
|
|
|
|
|
+ const ratePkt = Math.round(deltaPkts / dt);
|
|
|
|
|
+
|
|
|
|
|
+ if (!state.fwStatRateHistory) state.fwStatRateHistory = new Array(30).fill(0);
|
|
|
|
|
+ if (!state.fwStatPktHistory) state.fwStatPktHistory = new Array(30).fill(0);
|
|
|
|
|
+
|
|
|
|
|
+ state.fwStatRateHistory.push(rateBps);
|
|
|
|
|
+ if (state.fwStatRateHistory.length > 30) state.fwStatRateHistory.shift();
|
|
|
|
|
+
|
|
|
|
|
+ state.fwStatPktHistory.push(ratePkt);
|
|
|
|
|
+ if (state.fwStatPktHistory.length > 30) state.fwStatPktHistory.shift();
|
|
|
|
|
+
|
|
|
|
|
+ const bEl = document.getElementById('fw-stat-bytes');
|
|
|
|
|
+ const pEl = document.getElementById('fw-stat-packets');
|
|
|
|
|
+ const rEl = document.getElementById('fw-stat-rate');
|
|
|
|
|
+ const prEl = document.getElementById('fw-stat-pktrate');
|
|
|
|
|
+
|
|
|
|
|
+ if (bEl) bEl.value = formatTraffic(curBytes).replace('/s', '');
|
|
|
|
|
+ if (pEl) pEl.value = curPackets.toLocaleString();
|
|
|
|
|
+ if (rEl) rEl.value = formatBps(rateBps);
|
|
|
|
|
+ if (prEl) prEl.value = `${ratePkt.toLocaleString()} p/s`;
|
|
|
|
|
+
|
|
|
|
|
+ renderFwSparkline('fw-stat-rate-svg', state.fwStatRateHistory, 'bps', 'fw-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('fw-stat-pktrate-svg', state.fwStatPktHistory, 'p/s', 'fw-stat-legend-pktrate');
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('Error polling rule stats', e);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatBps(bps) {
|
|
|
|
|
+ if (bps >= 1000000000) return (bps / 1000000000).toFixed(1) + ' Gbps';
|
|
|
|
|
+ if (bps >= 1000000) return (bps / 1000000).toFixed(1) + ' Mbps';
|
|
|
|
|
+ if (bps >= 1000) return (bps / 1000).toFixed(1) + ' kbps';
|
|
|
|
|
+ return bps + ' bps';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderFwSparkline(svgId, dataArray, unit, legendId) {
|
|
|
|
|
+ const svg = document.getElementById(svgId);
|
|
|
|
|
+ if (!svg) return;
|
|
|
|
|
+
|
|
|
|
|
+ const w = 500;
|
|
|
|
|
+ const h = 100;
|
|
|
|
|
+ const paddingRight = 45;
|
|
|
|
|
+ const graphWidth = w - paddingRight;
|
|
|
|
|
+
|
|
|
|
|
+ const maxVal = Math.max(5, ...dataArray);
|
|
|
|
|
+ const currentVal = dataArray[dataArray.length - 1] || 0;
|
|
|
|
|
+
|
|
|
|
|
+ const leg = document.getElementById(legendId);
|
|
|
|
|
+ if (leg) {
|
|
|
|
|
+ leg.innerText = unit === 'bps' ? formatBps(currentVal) : `${currentVal.toLocaleString()} p/s`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let gridLines = '';
|
|
|
|
|
+ for (let i = 0; i <= 5; i++) {
|
|
|
|
|
+ const y = 8 + (i * (h - 18) / 5);
|
|
|
|
|
+ const valAtLevel = Math.round(maxVal * (5 - i) / 5);
|
|
|
|
|
+ const label = unit === 'bps' ? formatBps(valAtLevel) : `${valAtLevel} p/s`;
|
|
|
|
|
+ gridLines += `
|
|
|
|
|
+ <line x1="0" y1="${y.toFixed(1)}" x2="${graphWidth}" y2="${y.toFixed(1)}" stroke="#1e2c3e" stroke-width="1" />
|
|
|
|
|
+ <text x="${w - 2}" y="${(y + 3).toFixed(1)}" fill="#64748b" font-size="9" font-family="monospace" text-anchor="end">${label}</text>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const step = graphWidth / (dataArray.length - 1);
|
|
|
|
|
+ const points = dataArray.map((val, idx) => {
|
|
|
|
|
+ const x = idx * step;
|
|
|
|
|
+ const y = (h - 10) - (val / maxVal) * (h - 20);
|
|
|
|
|
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const polylineStr = points.join(' ');
|
|
|
|
|
+ const areaPath = `M 0,${h - 10} L ${polylineStr} L ${graphWidth},${h - 10} Z`;
|
|
|
|
|
+
|
|
|
|
|
+ svg.innerHTML = `
|
|
|
|
|
+ ${gridLines}
|
|
|
|
|
+ <path d="${areaPath}" fill="rgba(14, 165, 233, 0.12)" />
|
|
|
|
|
+ <polyline fill="none" stroke="#38bdf8" stroke-width="1.8" stroke-linejoin="round" points="${polylineStr}" />
|
|
|
|
|
+ <circle cx="${graphWidth}" cy="${points[points.length - 1].split(',')[1]}" r="3" fill="#38bdf8" />
|
|
|
|
|
+ `;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function moveFilterRule(id, dir) {
|
|
async function moveFilterRule(id, dir) {
|
|
|
try {
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/filter/${encodeURIComponent(id)}/move`, {
|
|
const res = await fetch(`${API_BASE}/firewall/filter/${encodeURIComponent(id)}/move`, {
|