|
@@ -4105,33 +4105,192 @@ function renderMangleRules() {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ==================== FIREWALL MANGLE MODAL (ROUTEROS WEBFIG / WINBOX V4) ====================
|
|
|
|
|
+
|
|
|
|
|
+function switchMangleRuleTab(tab) {
|
|
|
|
|
+ state.activeMangleRuleTab = tab;
|
|
|
|
|
+ document.querySelectorAll('.mangle-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(`mangle-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('.mangle-rule-pane').forEach(p => p.classList.add('hidden'));
|
|
|
|
|
+ const activePane = document.getElementById(`mangle-pane-${tab}`);
|
|
|
|
|
+ if (activePane) activePane.classList.remove('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ if (tab === 'statistics') {
|
|
|
|
|
+ startMangleStatPolling();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stopMangleStatPolling();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toggleMangleField(fieldKey, expand) {
|
|
|
|
|
+ const plusBtn = document.getElementById(`mangle-plus-${fieldKey}`);
|
|
|
|
|
+ const fieldBox = document.getElementById(`mangle-field-${fieldKey}`);
|
|
|
|
|
+ if (!plusBtn || !fieldBox) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (expand) {
|
|
|
|
|
+ plusBtn.classList.add('hidden');
|
|
|
|
|
+ fieldBox.classList.remove('hidden');
|
|
|
|
|
+ const valInput = document.getElementById(`mangle-val-${fieldKey}`);
|
|
|
|
|
+ if (valInput) valInput.focus();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fieldBox.classList.add('hidden');
|
|
|
|
|
+ plusBtn.classList.remove('hidden');
|
|
|
|
|
+ const valInput = document.getElementById(`mangle-val-${fieldKey}`);
|
|
|
|
|
+ if (valInput) {
|
|
|
|
|
+ if (valInput.tagName === 'SELECT') {
|
|
|
|
|
+ valInput.selectedIndex = 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ valInput.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const invInput = document.getElementById(`mangle-inv-${fieldKey}`);
|
|
|
|
|
+ if (invInput) invInput.checked = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (fieldKey === 'cs') {
|
|
|
|
|
+ ['mangle-modal-cs-established', 'mangle-modal-cs-related', 'mangle-modal-cs-new', 'mangle-modal-cs-invalid', 'mangle-modal-cs-untracked'].forEach(id => {
|
|
|
|
|
+ const el = document.getElementById(id);
|
|
|
|
|
+ if (el) el.checked = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fieldKey === 'natstate') {
|
|
|
|
|
+ ['mangle-modal-nat-srcnat', 'mangle-modal-nat-dstnat'].forEach(id => {
|
|
|
|
|
+ const el = document.getElementById(id);
|
|
|
|
|
+ if (el) el.checked = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function setMangleFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
|
|
+ if (value !== undefined && value !== null && value !== '' && value !== 'all') {
|
|
|
|
|
+ toggleMangleField(fieldKey, true);
|
|
|
|
|
+ let strVal = String(value);
|
|
|
|
|
+ if (hasInvert) {
|
|
|
|
|
+ const inv = strVal.startsWith('!');
|
|
|
|
|
+ if (inv) strVal = strVal.slice(1);
|
|
|
|
|
+ const invEl = document.getElementById(`mangle-inv-${fieldKey}`);
|
|
|
|
|
+ if (invEl) invEl.checked = inv;
|
|
|
|
|
+ }
|
|
|
|
|
+ const valEl = document.getElementById(`mangle-val-${fieldKey}`);
|
|
|
|
|
+ if (valEl) valEl.value = strVal;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleMangleField(fieldKey, false);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getMangleFieldVal(fieldKey, hasInvert = false) {
|
|
|
|
|
+ const fieldBox = document.getElementById(`mangle-field-${fieldKey}`);
|
|
|
|
|
+ if (!fieldBox || fieldBox.classList.contains('hidden')) return '';
|
|
|
|
|
+ const valEl = document.getElementById(`mangle-val-${fieldKey}`);
|
|
|
|
|
+ let v = valEl ? valEl.value.trim() : '';
|
|
|
|
|
+ if (!v) return '';
|
|
|
|
|
+ if (hasInvert) {
|
|
|
|
|
+ const invEl = document.getElementById(`mangle-inv-${fieldKey}`);
|
|
|
|
|
+ if (invEl && invEl.checked) {
|
|
|
|
|
+ v = '!' + v.replace(/^!/, '');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return v;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toggleMaximizeMangleModal() {
|
|
|
|
|
+ const win = document.getElementById('mangle-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 toggleMangleActionFields() {
|
|
|
|
|
+ const act = document.getElementById('mangle-modal-action')?.value || 'accept';
|
|
|
|
|
+
|
|
|
|
|
+ // Hide all dynamic rows first
|
|
|
|
|
+ ['newconnmark', 'newroutingmark', 'newpktmark', 'newmss', 'newdscp', 'newttl', 'passthrough'].forEach(id => {
|
|
|
|
|
+ const el = document.getElementById(`mangle-row-${id}`);
|
|
|
|
|
+ if (el) el.classList.add('hidden');
|
|
|
|
|
+ });
|
|
|
|
|
+ const jumpBox = document.getElementById('mangle-action-jump-box');
|
|
|
|
|
+ const addrBox = document.getElementById('mangle-action-addrlist-box');
|
|
|
|
|
+ if (jumpBox) jumpBox.classList.add('hidden');
|
|
|
|
|
+ if (addrBox) addrBox.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ // Activate matching rows
|
|
|
|
|
+ if (act === 'mark-connection') {
|
|
|
|
|
+ document.getElementById('mangle-row-newconnmark')?.classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
|
|
|
|
|
+ } else if (act === 'mark-routing') {
|
|
|
|
|
+ document.getElementById('mangle-row-newroutingmark')?.classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
|
|
|
|
|
+ } else if (act === 'mark-packet') {
|
|
|
|
|
+ document.getElementById('mangle-row-newpktmark')?.classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
|
|
|
|
|
+ } else if (act === 'change-mss') {
|
|
|
|
|
+ document.getElementById('mangle-row-newmss')?.classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
|
|
|
|
|
+ } else if (act === 'change-dscp') {
|
|
|
|
|
+ document.getElementById('mangle-row-newdscp')?.classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
|
|
|
|
|
+ } else if (act === 'change-ttl') {
|
|
|
|
|
+ document.getElementById('mangle-row-newttl')?.classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
|
|
|
|
|
+ } else if (act === 'jump') {
|
|
|
|
|
+ if (jumpBox) jumpBox.classList.remove('hidden');
|
|
|
|
|
+ } else if (act.includes('address-list') || act.includes('address list')) {
|
|
|
|
|
+ if (addrBox) addrBox.classList.remove('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const allMangleFieldKeys = [
|
|
|
|
|
+ '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',
|
|
|
|
|
+ 'nth', 'connlimit', 'srcaddrtype', 'dstaddrtype', 'hotspot', 'ipfragment',
|
|
|
|
|
+ 'limit', 'dstlimit', 'time', 'psd', 'logprefix'
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
function openAddMangleModal() {
|
|
function openAddMangleModal() {
|
|
|
- document.getElementById('mangle-modal-title').innerText = 'Add Mangle Rule';
|
|
|
|
|
|
|
+ switchMangleRuleTab('general');
|
|
|
|
|
+ const badge = document.getElementById('mangle-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = 'New...';
|
|
|
document.getElementById('mangle-modal-id').value = '';
|
|
document.getElementById('mangle-modal-id').value = '';
|
|
|
- document.getElementById('mangle-modal-chain').value = 'prerouting';
|
|
|
|
|
- document.getElementById('mangle-modal-action').value = 'mark-connection';
|
|
|
|
|
- document.getElementById('mangle-modal-newmark').value = '';
|
|
|
|
|
- document.getElementById('mangle-modal-passthrough').checked = true;
|
|
|
|
|
- document.getElementById('mangle-modal-src').value = '';
|
|
|
|
|
- document.getElementById('mangle-modal-dst').value = '';
|
|
|
|
|
- document.getElementById('mangle-modal-connmark').value = '';
|
|
|
|
|
|
|
+ document.getElementById('mangle-modal-enabled').checked = true;
|
|
|
document.getElementById('mangle-modal-comment').value = '';
|
|
document.getElementById('mangle-modal-comment').value = '';
|
|
|
- document.getElementById('mangle-modal-disabled').checked = false;
|
|
|
|
|
-
|
|
|
|
|
- populateInterfaceSelect('mangle-modal-iniface');
|
|
|
|
|
- populateAddressListSelect('mangle-modal-srclist');
|
|
|
|
|
- populateAddressListSelect('mangle-modal-dstlist');
|
|
|
|
|
- const srcInvEl = document.getElementById('mangle-modal-srclist-inv');
|
|
|
|
|
- if (srcInvEl) srcInvEl.checked = false;
|
|
|
|
|
- const dstInvEl = document.getElementById('mangle-modal-dstlist-inv');
|
|
|
|
|
- if (dstInvEl) dstInvEl.checked = false;
|
|
|
|
|
-
|
|
|
|
|
- populateGeoIPSelect('mangle-modal-srcgeoip');
|
|
|
|
|
- populateGeoIPSelect('mangle-modal-dstgeoip');
|
|
|
|
|
- const srcGeoInvEl = document.getElementById('mangle-modal-srcgeoip-inv');
|
|
|
|
|
- if (srcGeoInvEl) srcGeoInvEl.checked = false;
|
|
|
|
|
- const dstGeoInvEl = document.getElementById('mangle-modal-dstgeoip-inv');
|
|
|
|
|
- if (dstGeoInvEl) dstGeoInvEl.checked = false;
|
|
|
|
|
|
|
+ document.getElementById('mangle-modal-chain').value = 'prerouting';
|
|
|
|
|
+ document.getElementById('mangle-modal-action').value = 'accept';
|
|
|
|
|
+
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newconnmark')) document.getElementById('mangle-modal-newconnmark').value = '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newroutingmark')) document.getElementById('mangle-modal-newroutingmark').value = '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newpktmark')) document.getElementById('mangle-modal-newpktmark').value = '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newmss')) document.getElementById('mangle-modal-newmss').value = '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newdscp')) document.getElementById('mangle-modal-newdscp').value = '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newttl')) document.getElementById('mangle-modal-newttl').value = '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-passthrough')) document.getElementById('mangle-modal-passthrough').checked = true;
|
|
|
|
|
+ if (document.getElementById('mangle-modal-log')) document.getElementById('mangle-modal-log').checked = false;
|
|
|
|
|
+
|
|
|
|
|
+ populateInterfaceSelect('mangle-val-iniface');
|
|
|
|
|
+ populateInterfaceSelect('mangle-val-outiface');
|
|
|
|
|
+ populateAddressListSelect('mangle-val-srclist');
|
|
|
|
|
+ populateAddressListSelect('mangle-val-dstlist');
|
|
|
|
|
+
|
|
|
|
|
+ allMangleFieldKeys.forEach(k => toggleMangleField(k, false));
|
|
|
|
|
+
|
|
|
|
|
+ resetCurrentMangleRuleCounters();
|
|
|
|
|
+ toggleMangleActionFields();
|
|
|
|
|
|
|
|
document.getElementById('mangle-modal').classList.remove('hidden');
|
|
document.getElementById('mangle-modal').classList.remove('hidden');
|
|
|
}
|
|
}
|
|
@@ -4140,103 +4299,294 @@ function openEditMangleModal(id) {
|
|
|
const r = (state.mangleRules || []).find(rule => rule.id === id);
|
|
const r = (state.mangleRules || []).find(rule => rule.id === id);
|
|
|
if (!r) return;
|
|
if (!r) return;
|
|
|
|
|
|
|
|
- document.getElementById('mangle-modal-title').innerText = `Edit Mangle Rule (#${r.order})`;
|
|
|
|
|
|
|
+ switchMangleRuleTab('general');
|
|
|
|
|
+ const badge = document.getElementById('mangle-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = `#${r.order != null ? r.order : r.id}`;
|
|
|
|
|
+
|
|
|
document.getElementById('mangle-modal-id').value = r.id;
|
|
document.getElementById('mangle-modal-id').value = r.id;
|
|
|
- document.getElementById('mangle-modal-chain').value = r.chain || 'prerouting';
|
|
|
|
|
- document.getElementById('mangle-modal-action').value = r.action || 'mark-connection';
|
|
|
|
|
- document.getElementById('mangle-modal-newmark').value = r.new_connection_mark || r.new_routing_mark || r.new_packet_mark || r.new_mss || '';
|
|
|
|
|
- document.getElementById('mangle-modal-passthrough').checked = r.passthrough ?? true;
|
|
|
|
|
- document.getElementById('mangle-modal-src').value = r.src_address || '';
|
|
|
|
|
- document.getElementById('mangle-modal-dst').value = r.dst_address || '';
|
|
|
|
|
- document.getElementById('mangle-modal-connmark').value = r.connection_mark || '';
|
|
|
|
|
|
|
+ document.getElementById('mangle-modal-enabled').checked = !r.disabled;
|
|
|
document.getElementById('mangle-modal-comment').value = r.comment || '';
|
|
document.getElementById('mangle-modal-comment').value = r.comment || '';
|
|
|
- document.getElementById('mangle-modal-disabled').checked = !!r.disabled;
|
|
|
|
|
-
|
|
|
|
|
- populateInterfaceSelect('mangle-modal-iniface', r.in_interface);
|
|
|
|
|
-
|
|
|
|
|
- const srcInv = (r.src_address_list || '').startsWith('!');
|
|
|
|
|
- const dstInv = (r.dst_address_list || '').startsWith('!');
|
|
|
|
|
- const srcInvEl = document.getElementById('mangle-modal-srclist-inv');
|
|
|
|
|
- if (srcInvEl) srcInvEl.checked = srcInv;
|
|
|
|
|
- const dstInvEl = document.getElementById('mangle-modal-dstlist-inv');
|
|
|
|
|
- if (dstInvEl) dstInvEl.checked = dstInv;
|
|
|
|
|
- populateAddressListSelect('mangle-modal-srclist', r.src_address_list || '');
|
|
|
|
|
- populateAddressListSelect('mangle-modal-dstlist', r.dst_address_list || '');
|
|
|
|
|
-
|
|
|
|
|
- const srcGeoInv = (r.src_geoip || '').startsWith('!');
|
|
|
|
|
- const dstGeoInv = (r.dst_geoip || '').startsWith('!');
|
|
|
|
|
- const srcGeoInvEl = document.getElementById('mangle-modal-srcgeoip-inv');
|
|
|
|
|
- if (srcGeoInvEl) srcGeoInvEl.checked = srcGeoInv;
|
|
|
|
|
- const dstGeoInvEl = document.getElementById('mangle-modal-dstgeoip-inv');
|
|
|
|
|
- if (dstGeoInvEl) dstGeoInvEl.checked = dstGeoInv;
|
|
|
|
|
- populateGeoIPSelect('mangle-modal-srcgeoip', r.src_geoip || '');
|
|
|
|
|
- populateGeoIPSelect('mangle-modal-dstgeoip', r.dst_geoip || '');
|
|
|
|
|
|
|
+ document.getElementById('mangle-modal-chain').value = r.chain || 'prerouting';
|
|
|
|
|
+ document.getElementById('mangle-modal-action').value = r.action || 'accept';
|
|
|
|
|
+
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newconnmark')) document.getElementById('mangle-modal-newconnmark').value = r.new_connection_mark || '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newroutingmark')) document.getElementById('mangle-modal-newroutingmark').value = r.new_routing_mark || '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newpktmark')) document.getElementById('mangle-modal-newpktmark').value = r.new_packet_mark || '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newmss')) document.getElementById('mangle-modal-newmss').value = r.new_mss || '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newdscp')) document.getElementById('mangle-modal-newdscp').value = r.new_dscp != null ? r.new_dscp : '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-newttl')) document.getElementById('mangle-modal-newttl').value = r.new_ttl || '';
|
|
|
|
|
+ if (document.getElementById('mangle-modal-passthrough')) document.getElementById('mangle-modal-passthrough').checked = r.passthrough ?? true;
|
|
|
|
|
+ if (document.getElementById('mangle-modal-log')) document.getElementById('mangle-modal-log').checked = !!r.log;
|
|
|
|
|
+
|
|
|
|
|
+ populateInterfaceSelect('mangle-val-iniface', (r.in_interface || '').replace(/^!/, ''));
|
|
|
|
|
+ populateInterfaceSelect('mangle-val-outiface', (r.out_interface || '').replace(/^!/, ''));
|
|
|
|
|
+ populateAddressListSelect('mangle-val-srclist', (r.src_address_list || '').replace(/^!/, ''));
|
|
|
|
|
+ populateAddressListSelect('mangle-val-dstlist', (r.dst_address_list || '').replace(/^!/, ''));
|
|
|
|
|
+
|
|
|
|
|
+ setMangleFieldVal('src', r.src_address, true);
|
|
|
|
|
+ setMangleFieldVal('dst', r.dst_address, true);
|
|
|
|
|
+ setMangleFieldVal('srclist', r.src_address_list, true);
|
|
|
|
|
+ setMangleFieldVal('dstlist', r.dst_address_list, true);
|
|
|
|
|
+ if (r.protocol && r.protocol !== 'all') {
|
|
|
|
|
+ setMangleFieldVal('proto', r.protocol, true);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleMangleField('proto', false);
|
|
|
|
|
+ }
|
|
|
|
|
+ setMangleFieldVal('sport', r.src_port, true);
|
|
|
|
|
+ setMangleFieldVal('dport', r.dst_port, true);
|
|
|
|
|
+ setMangleFieldVal('anyport', r.any_port, true);
|
|
|
|
|
+ setMangleFieldVal('iniface', r.in_interface, true);
|
|
|
|
|
+ setMangleFieldVal('outiface', r.out_interface, true);
|
|
|
|
|
+ setMangleFieldVal('inifacelist', r.in_interface_list, true);
|
|
|
|
|
+ setMangleFieldVal('outifacelist', r.out_interface_list, true);
|
|
|
|
|
+ setMangleFieldVal('pktmark', r.packet_mark, true);
|
|
|
|
|
+ setMangleFieldVal('connmark', r.connection_mark, true);
|
|
|
|
|
+ setMangleFieldVal('routingmark', r.routing_mark, true);
|
|
|
|
|
+ setMangleFieldVal('conntype', r.connection_type, false);
|
|
|
|
|
+
|
|
|
|
|
+ // Connection State
|
|
|
|
|
+ const states = r.connection_state || [];
|
|
|
|
|
+ if (states.length > 0) {
|
|
|
|
|
+ toggleMangleField('cs', true);
|
|
|
|
|
+ ['established', 'related', 'new', 'invalid', 'untracked'].forEach(s => {
|
|
|
|
|
+ const el = document.getElementById(`mangle-modal-cs-${s}`);
|
|
|
|
|
+ if (el) el.checked = states.includes(s);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleMangleField('cs', false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Connection NAT State
|
|
|
|
|
+ const natStates = r.connection_nat_state || [];
|
|
|
|
|
+ if (natStates.length > 0) {
|
|
|
|
|
+ toggleMangleField('natstate', true);
|
|
|
|
|
+ ['srcnat', 'dstnat'].forEach(s => {
|
|
|
|
|
+ const el = document.getElementById(`mangle-modal-nat-${s}`);
|
|
|
|
|
+ if (el) el.checked = natStates.includes(s);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toggleMangleField('natstate', false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Advanced tab fields
|
|
|
|
|
+ setMangleFieldVal('layer7', r.layer7_protocol, false);
|
|
|
|
|
+ setMangleFieldVal('content', r.content, false);
|
|
|
|
|
+ setMangleFieldVal('connbytes', r.connection_bytes, false);
|
|
|
|
|
+ setMangleFieldVal('connrate', r.connection_rate, false);
|
|
|
|
|
+ setMangleFieldVal('pcc', r.pcc, false);
|
|
|
|
|
+ setMangleFieldVal('srcmac', r.src_mac_address, true);
|
|
|
|
|
+ setMangleFieldVal('outbridgeport', r.out_bridge_port, false);
|
|
|
|
|
+ setMangleFieldVal('inbridgeport', r.in_bridge_port, false);
|
|
|
|
|
+ setMangleFieldVal('inbridgeportlist', r.in_bridge_port_list, false);
|
|
|
|
|
+ setMangleFieldVal('outbridgeportlist', r.out_bridge_port_list, false);
|
|
|
|
|
+ setMangleFieldVal('ipsec', r.ipsec_policy, false);
|
|
|
|
|
+ setMangleFieldVal('tlshost', r.tls_host, false);
|
|
|
|
|
+ setMangleFieldVal('ingresspriority', r.ingress_priority, false);
|
|
|
|
|
+ setMangleFieldVal('priority', r.priority, false);
|
|
|
|
|
+ setMangleFieldVal('dscp', r.dscp, false);
|
|
|
|
|
+ setMangleFieldVal('tcpmss', r.tcp_mss, false);
|
|
|
|
|
+ if (r.tos) {
|
|
|
|
|
+ const tosEl = document.getElementById('mangle-val-tos');
|
|
|
|
|
+ if (tosEl) tosEl.value = r.tos;
|
|
|
|
|
+ }
|
|
|
|
|
+ setMangleFieldVal('pktsize', r.packet_size, false);
|
|
|
|
|
+ setMangleFieldVal('random', r.random, false);
|
|
|
|
|
+ setMangleFieldVal('tcpflags', r.tcp_flags, false);
|
|
|
|
|
+ setMangleFieldVal('icmpoptions', r.icmp_options, false);
|
|
|
|
|
+ setMangleFieldVal('ipv4options', r.ipv4_options, false);
|
|
|
|
|
+ setMangleFieldVal('ttl', r.ttl, false);
|
|
|
|
|
+
|
|
|
|
|
+ // Extra tab fields
|
|
|
|
|
+ setMangleFieldVal('nth', r.nth, false);
|
|
|
|
|
+ setMangleFieldVal('connlimit', r.connection_limit, false);
|
|
|
|
|
+ setMangleFieldVal('srcaddrtype', r.src_address_type, false);
|
|
|
|
|
+ setMangleFieldVal('dstaddrtype', r.dst_address_type, false);
|
|
|
|
|
+ setMangleFieldVal('hotspot', r.hotspot, false);
|
|
|
|
|
+ setMangleFieldVal('ipfragment', r.ip_fragment, false);
|
|
|
|
|
+ setMangleFieldVal('limit', r.limit, false);
|
|
|
|
|
+ setMangleFieldVal('dstlimit', r.dst_limit, false);
|
|
|
|
|
+ setMangleFieldVal('time', r.time, false);
|
|
|
|
|
+ setMangleFieldVal('psd', r.psd, false);
|
|
|
|
|
+
|
|
|
|
|
+ // Action fields
|
|
|
|
|
+ if (r.jump_target) {
|
|
|
|
|
+ const jtEl = document.getElementById('mangle-modal-jumptarget');
|
|
|
|
|
+ if (jtEl) jtEl.value = r.jump_target;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (r.address_list) {
|
|
|
|
|
+ const alEl = document.getElementById('mangle-modal-action-addrlist');
|
|
|
|
|
+ if (alEl) alEl.value = r.address_list;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (r.address_list_timeout) {
|
|
|
|
|
+ const toEl = document.getElementById('mangle-modal-action-timeout');
|
|
|
|
|
+ if (toEl) toEl.value = r.address_list_timeout;
|
|
|
|
|
+ }
|
|
|
|
|
+ setMangleFieldVal('logprefix', r.log_prefix, false);
|
|
|
|
|
|
|
|
|
|
+ // Statistics
|
|
|
|
|
+ const bEl = document.getElementById('mangle-stat-bytes');
|
|
|
|
|
+ const pEl = document.getElementById('mangle-stat-packets');
|
|
|
|
|
+ if (bEl) bEl.value = formatTraffic(r.bytes || 0).replace('/s', '');
|
|
|
|
|
+ if (pEl) pEl.value = (r.packets || 0).toLocaleString();
|
|
|
|
|
+ state.mangleLastBytes = r.bytes || 0;
|
|
|
|
|
+ state.mangleLastPackets = r.packets || 0;
|
|
|
|
|
+ state.mangleLastTimestamp = Date.now();
|
|
|
|
|
+ state.mangleStatRateHistory = new Array(30).fill(0);
|
|
|
|
|
+ state.mangleStatPktHistory = new Array(30).fill(0);
|
|
|
|
|
+ renderFwSparkline('mangle-stat-rate-svg', state.mangleStatRateHistory, 'bps', 'mangle-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('mangle-stat-pktrate-svg', state.mangleStatPktHistory, 'p/s', 'mangle-stat-legend-pktrate');
|
|
|
|
|
+
|
|
|
|
|
+ toggleMangleActionFields();
|
|
|
document.getElementById('mangle-modal').classList.remove('hidden');
|
|
document.getElementById('mangle-modal').classList.remove('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function closeMangleModal() {
|
|
function closeMangleModal() {
|
|
|
|
|
+ stopMangleStatPolling();
|
|
|
document.getElementById('mangle-modal').classList.add('hidden');
|
|
document.getElementById('mangle-modal').classList.add('hidden');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function toggleMangleActionFields() {
|
|
|
|
|
- // Handled smoothly by single dynamic input
|
|
|
|
|
|
|
+function copyMangleRule() {
|
|
|
|
|
+ document.getElementById('mangle-modal-id').value = '';
|
|
|
|
|
+ const badge = document.getElementById('mangle-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = 'New...';
|
|
|
|
|
+ showToast('Rule Cloned', 'Ready to save as a new Mangle rule copy', 'info');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resetCurrentMangleRuleCounters() {
|
|
|
|
|
+ state.mangleStatRateHistory = new Array(30).fill(0);
|
|
|
|
|
+ state.mangleStatPktHistory = new Array(30).fill(0);
|
|
|
|
|
+ state.mangleLastBytes = 0;
|
|
|
|
|
+ state.mangleLastPackets = 0;
|
|
|
|
|
+ state.mangleLastTimestamp = Date.now();
|
|
|
|
|
+ const bEl = document.getElementById('mangle-stat-bytes');
|
|
|
|
|
+ const pEl = document.getElementById('mangle-stat-packets');
|
|
|
|
|
+ const rEl = document.getElementById('mangle-stat-rate');
|
|
|
|
|
+ const prEl = document.getElementById('mangle-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('mangle-stat-rate-svg', state.mangleStatRateHistory, 'bps', 'mangle-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('mangle-stat-pktrate-svg', state.mangleStatPktHistory, 'p/s', 'mangle-stat-legend-pktrate');
|
|
|
|
|
+ showToast('Counters Reset', 'Mangle rule counter display reset to 0', 'info');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function submitSaveMangleRule(e) {
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
|
|
+async function applyMangleRule(closeAfter = false) {
|
|
|
const id = document.getElementById('mangle-modal-id').value;
|
|
const id = document.getElementById('mangle-modal-id').value;
|
|
|
const chain = document.getElementById('mangle-modal-chain').value;
|
|
const chain = document.getElementById('mangle-modal-chain').value;
|
|
|
const action = document.getElementById('mangle-modal-action').value;
|
|
const action = document.getElementById('mangle-modal-action').value;
|
|
|
- const newmark = document.getElementById('mangle-modal-newmark').value.trim();
|
|
|
|
|
- const passthrough = document.getElementById('mangle-modal-passthrough').checked;
|
|
|
|
|
- const src = document.getElementById('mangle-modal-src').value.trim();
|
|
|
|
|
- const dst = document.getElementById('mangle-modal-dst').value.trim();
|
|
|
|
|
- const inIface = document.getElementById('mangle-modal-iniface').value;
|
|
|
|
|
|
|
+ const comment = document.getElementById('mangle-modal-comment').value.trim();
|
|
|
|
|
+ const enabled = document.getElementById('mangle-modal-enabled').checked;
|
|
|
|
|
+
|
|
|
|
|
+ const src = getMangleFieldVal('src', true);
|
|
|
|
|
+ const dst = getMangleFieldVal('dst', true);
|
|
|
|
|
+ const srclist = getMangleFieldVal('srclist', true);
|
|
|
|
|
+ const dstlist = getMangleFieldVal('dstlist', true);
|
|
|
|
|
+ const proto = getMangleFieldVal('proto', true);
|
|
|
|
|
+ const sport = getMangleFieldVal('sport', true);
|
|
|
|
|
+ const dport = getMangleFieldVal('dport', true);
|
|
|
|
|
+ const anyport = getMangleFieldVal('anyport', true);
|
|
|
|
|
+ const iniface = getMangleFieldVal('iniface', true);
|
|
|
|
|
+ const outiface = getMangleFieldVal('outiface', true);
|
|
|
|
|
+ const inifacelist = getMangleFieldVal('inifacelist', true);
|
|
|
|
|
+ const outifacelist = getMangleFieldVal('outifacelist', true);
|
|
|
|
|
+ const pktmark = getMangleFieldVal('pktmark', true);
|
|
|
|
|
+ const connmark = getMangleFieldVal('connmark', true);
|
|
|
|
|
+ const routingmark = getMangleFieldVal('routingmark', true);
|
|
|
|
|
+ const conntype = getMangleFieldVal('conntype', false);
|
|
|
|
|
|
|
|
- let srclist = document.getElementById('mangle-modal-srclist')?.value || '';
|
|
|
|
|
- if (srclist && document.getElementById('mangle-modal-srclist-inv')?.checked) {
|
|
|
|
|
- srclist = '!' + srclist.replace(/^!/, '');
|
|
|
|
|
- }
|
|
|
|
|
- let dstlist = document.getElementById('mangle-modal-dstlist')?.value || '';
|
|
|
|
|
- if (dstlist && document.getElementById('mangle-modal-dstlist-inv')?.checked) {
|
|
|
|
|
- dstlist = '!' + dstlist.replace(/^!/, '');
|
|
|
|
|
|
|
+ const states = [];
|
|
|
|
|
+ const csBox = document.getElementById('mangle-field-cs');
|
|
|
|
|
+ if (csBox && !csBox.classList.contains('hidden')) {
|
|
|
|
|
+ ['established', 'related', 'new', 'invalid', 'untracked'].forEach(s => {
|
|
|
|
|
+ if (document.getElementById(`mangle-modal-cs-${s}`)?.checked) states.push(s);
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- let srcgeoip = document.getElementById('mangle-modal-srcgeoip')?.value || '';
|
|
|
|
|
- if (srcgeoip && document.getElementById('mangle-modal-srcgeoip-inv')?.checked) {
|
|
|
|
|
- srcgeoip = '!' + srcgeoip.replace(/^!/, '');
|
|
|
|
|
- }
|
|
|
|
|
- let dstgeoip = document.getElementById('mangle-modal-dstgeoip')?.value || '';
|
|
|
|
|
- if (dstgeoip && document.getElementById('mangle-modal-dstgeoip-inv')?.checked) {
|
|
|
|
|
- dstgeoip = '!' + dstgeoip.replace(/^!/, '');
|
|
|
|
|
|
|
+ const natStates = [];
|
|
|
|
|
+ const natBox = document.getElementById('mangle-field-natstate');
|
|
|
|
|
+ if (natBox && !natBox.classList.contains('hidden')) {
|
|
|
|
|
+ ['srcnat', 'dstnat'].forEach(s => {
|
|
|
|
|
+ if (document.getElementById(`mangle-modal-nat-${s}`)?.checked) natStates.push(s);
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const connmark = document.getElementById('mangle-modal-connmark').value.trim();
|
|
|
|
|
- const comment = document.getElementById('mangle-modal-comment').value.trim();
|
|
|
|
|
- const disabled = document.getElementById('mangle-modal-disabled').checked;
|
|
|
|
|
-
|
|
|
|
|
const payload = {
|
|
const payload = {
|
|
|
id,
|
|
id,
|
|
|
chain,
|
|
chain,
|
|
|
action,
|
|
action,
|
|
|
- passthrough,
|
|
|
|
|
|
|
+ protocol: proto || 'all',
|
|
|
src_address: src,
|
|
src_address: src,
|
|
|
dst_address: dst,
|
|
dst_address: dst,
|
|
|
- in_interface: inIface,
|
|
|
|
|
|
|
+ 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,
|
|
src_address_list: srclist,
|
|
|
dst_address_list: dstlist,
|
|
dst_address_list: dstlist,
|
|
|
- src_geoip: srcgeoip,
|
|
|
|
|
- dst_geoip: dstgeoip,
|
|
|
|
|
- connection_mark: connmark,
|
|
|
|
|
comment,
|
|
comment,
|
|
|
- disabled
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ disabled: !enabled,
|
|
|
|
|
+
|
|
|
|
|
+ // Advanced tab
|
|
|
|
|
+ layer7_protocol: getMangleFieldVal('layer7', false),
|
|
|
|
|
+ content: getMangleFieldVal('content', false),
|
|
|
|
|
+ connection_bytes: getMangleFieldVal('connbytes', false),
|
|
|
|
|
+ connection_rate: getMangleFieldVal('connrate', false),
|
|
|
|
|
+ pcc: getMangleFieldVal('pcc', false),
|
|
|
|
|
+ src_mac_address: getMangleFieldVal('srcmac', true),
|
|
|
|
|
+ out_bridge_port: getMangleFieldVal('outbridgeport', false),
|
|
|
|
|
+ in_bridge_port: getMangleFieldVal('inbridgeport', false),
|
|
|
|
|
+ in_bridge_port_list: getMangleFieldVal('inbridgeportlist', false),
|
|
|
|
|
+ out_bridge_port_list: getMangleFieldVal('outbridgeportlist', false),
|
|
|
|
|
+ ipsec_policy: getMangleFieldVal('ipsec', false),
|
|
|
|
|
+ tls_host: getMangleFieldVal('tlshost', false),
|
|
|
|
|
+ ingress_priority: getMangleFieldVal('ingresspriority', false),
|
|
|
|
|
+ priority: getMangleFieldVal('priority', false),
|
|
|
|
|
+ dscp: getMangleFieldVal('dscp', false),
|
|
|
|
|
+ tcp_mss: getMangleFieldVal('tcpmss', false),
|
|
|
|
|
+ tos: document.getElementById('mangle-val-tos')?.value || '',
|
|
|
|
|
+ packet_size: getMangleFieldVal('pktsize', false),
|
|
|
|
|
+ random: getMangleFieldVal('random', false),
|
|
|
|
|
+ tcp_flags: getMangleFieldVal('tcpflags', false),
|
|
|
|
|
+ icmp_options: getMangleFieldVal('icmpoptions', false),
|
|
|
|
|
+ ipv4_options: getMangleFieldVal('ipv4options', false),
|
|
|
|
|
+ ttl: getMangleFieldVal('ttl', false),
|
|
|
|
|
|
|
|
- if (action === 'mark-connection') payload.new_connection_mark = newmark;
|
|
|
|
|
- else if (action === 'mark-routing') payload.new_routing_mark = newmark;
|
|
|
|
|
- else if (action === 'mark-packet') payload.new_packet_mark = newmark;
|
|
|
|
|
- else if (action === 'change-mss') payload.new_mss = newmark || 'clamp-to-pmtu';
|
|
|
|
|
|
|
+ // Extra tab
|
|
|
|
|
+ nth: getMangleFieldVal('nth', false),
|
|
|
|
|
+ connection_limit: getMangleFieldVal('connlimit', false),
|
|
|
|
|
+ src_address_type: getMangleFieldVal('srcaddrtype', false),
|
|
|
|
|
+ dst_address_type: getMangleFieldVal('dstaddrtype', false),
|
|
|
|
|
+ hotspot: getMangleFieldVal('hotspot', false),
|
|
|
|
|
+ ip_fragment: getMangleFieldVal('ipfragment', false),
|
|
|
|
|
+ limit: getMangleFieldVal('limit', false),
|
|
|
|
|
+ dst_limit: getMangleFieldVal('dstlimit', false),
|
|
|
|
|
+ time: getMangleFieldVal('time', false),
|
|
|
|
|
+ psd: getMangleFieldVal('psd', false),
|
|
|
|
|
+
|
|
|
|
|
+ // Action tab
|
|
|
|
|
+ new_connection_mark: action === 'mark-connection' ? (document.getElementById('mangle-modal-newconnmark')?.value.trim() || '') : '',
|
|
|
|
|
+ new_routing_mark: action === 'mark-routing' ? (document.getElementById('mangle-modal-newroutingmark')?.value.trim() || '') : '',
|
|
|
|
|
+ new_packet_mark: action === 'mark-packet' ? (document.getElementById('mangle-modal-newpktmark')?.value.trim() || '') : '',
|
|
|
|
|
+ new_mss: action === 'change-mss' ? (document.getElementById('mangle-modal-newmss')?.value.trim() || '') : '',
|
|
|
|
|
+ new_dscp: action === 'change-dscp' ? parseInt(document.getElementById('mangle-modal-newdscp')?.value || '0', 10) : 0,
|
|
|
|
|
+ new_ttl: action === 'change-ttl' ? (document.getElementById('mangle-modal-newttl')?.value.trim() || '') : '',
|
|
|
|
|
+ passthrough: document.getElementById('mangle-modal-passthrough')?.checked ?? true,
|
|
|
|
|
+ jump_target: action === 'jump' ? (document.getElementById('mangle-modal-jumptarget')?.value || '') : '',
|
|
|
|
|
+ address_list: action.includes('address') ? (document.getElementById('mangle-modal-action-addrlist')?.value || '') : '',
|
|
|
|
|
+ address_list_timeout: action.includes('address') ? (document.getElementById('mangle-modal-action-timeout')?.value || '') : '',
|
|
|
|
|
+ log: document.getElementById('mangle-modal-log')?.checked || false,
|
|
|
|
|
+ log_prefix: getMangleFieldVal('logprefix', false)
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/mangle`, {
|
|
const res = await fetch(`${API_BASE}/firewall/mangle`, {
|
|
@@ -4245,15 +4595,102 @@ async function submitSaveMangleRule(e) {
|
|
|
body: JSON.stringify(payload)
|
|
body: JSON.stringify(payload)
|
|
|
});
|
|
});
|
|
|
if (res.ok) {
|
|
if (res.ok) {
|
|
|
- showToast('Mangle Rule Saved', 'Marking rule applied', 'success');
|
|
|
|
|
- closeMangleModal();
|
|
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ showToast('Mangle Rule Saved', 'Marking rule updated', 'success');
|
|
|
loadFirewall(false);
|
|
loadFirewall(false);
|
|
|
|
|
+ if (closeAfter) {
|
|
|
|
|
+ closeMangleModal();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (data.id) {
|
|
|
|
|
+ document.getElementById('mangle-modal-id').value = data.id;
|
|
|
|
|
+ const badge = document.getElementById('mangle-modal-badge');
|
|
|
|
|
+ if (badge) badge.innerText = `#${data.id}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.message || 'Failed to save mangle rule', 'error');
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function submitSaveMangleRule(e) {
|
|
|
|
|
+ if (e && e.preventDefault) e.preventDefault();
|
|
|
|
|
+ return applyMangleRule(true);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function startMangleStatPolling() {
|
|
|
|
|
+ stopMangleStatPolling();
|
|
|
|
|
+ pollMangleRuleStats();
|
|
|
|
|
+ state.mangleStatInterval = setInterval(pollMangleRuleStats, 1000);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function stopMangleStatPolling() {
|
|
|
|
|
+ if (state.mangleStatInterval) {
|
|
|
|
|
+ clearInterval(state.mangleStatInterval);
|
|
|
|
|
+ state.mangleStatInterval = null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function pollMangleRuleStats() {
|
|
|
|
|
+ const id = document.getElementById('mangle-modal-id')?.value;
|
|
|
|
|
+ if (!id) {
|
|
|
|
|
+ renderFwSparkline('mangle-stat-rate-svg', state.mangleStatRateHistory || new Array(30).fill(0), 'bps', 'mangle-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('mangle-stat-pktrate-svg', state.mangleStatPktHistory || new Array(30).fill(0), 'p/s', 'mangle-stat-legend-pktrate');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/firewall/mangle`);
|
|
|
|
|
+ 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.mangleLastTimestamp || now)) / 1000);
|
|
|
|
|
+ state.mangleLastTimestamp = now;
|
|
|
|
|
+
|
|
|
|
|
+ const curBytes = r.bytes || 0;
|
|
|
|
|
+ const curPackets = r.packets || 0;
|
|
|
|
|
+
|
|
|
|
|
+ const deltaBytes = Math.max(0, curBytes - (state.mangleLastBytes || curBytes));
|
|
|
|
|
+ const deltaPkts = Math.max(0, curPackets - (state.mangleLastPackets || curPackets));
|
|
|
|
|
+
|
|
|
|
|
+ state.mangleLastBytes = curBytes;
|
|
|
|
|
+ state.mangleLastPackets = curPackets;
|
|
|
|
|
+
|
|
|
|
|
+ const rateBps = Math.round((deltaBytes * 8) / dt);
|
|
|
|
|
+ const ratePkt = Math.round(deltaPkts / dt);
|
|
|
|
|
+
|
|
|
|
|
+ if (!state.mangleStatRateHistory) state.mangleStatRateHistory = new Array(30).fill(0);
|
|
|
|
|
+ if (!state.mangleStatPktHistory) state.mangleStatPktHistory = new Array(30).fill(0);
|
|
|
|
|
+
|
|
|
|
|
+ state.mangleStatRateHistory.push(rateBps);
|
|
|
|
|
+ if (state.mangleStatRateHistory.length > 30) state.mangleStatRateHistory.shift();
|
|
|
|
|
+
|
|
|
|
|
+ state.mangleStatPktHistory.push(ratePkt);
|
|
|
|
|
+ if (state.mangleStatPktHistory.length > 30) state.mangleStatPktHistory.shift();
|
|
|
|
|
+
|
|
|
|
|
+ const bEl = document.getElementById('mangle-stat-bytes');
|
|
|
|
|
+ const pEl = document.getElementById('mangle-stat-packets');
|
|
|
|
|
+ const rEl = document.getElementById('mangle-stat-rate');
|
|
|
|
|
+ const prEl = document.getElementById('mangle-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('mangle-stat-rate-svg', state.mangleStatRateHistory, 'bps', 'mangle-stat-legend-rate');
|
|
|
|
|
+ renderFwSparkline('mangle-stat-pktrate-svg', state.mangleStatPktHistory, 'p/s', 'mangle-stat-legend-pktrate');
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('Error polling Mangle rule stats', e);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function moveMangleRule(id, dir) {
|
|
async function moveMangleRule(id, dir) {
|
|
|
try {
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/mangle/${encodeURIComponent(id)}/move`, {
|
|
const res = await fetch(`${API_BASE}/firewall/mangle/${encodeURIComponent(id)}/move`, {
|