|
|
@@ -3511,35 +3511,172 @@ function renderNATRules() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+// ==================== FIREWALL NAT MODAL (ROUTEROS WEBFIG / WINBOX V4) ====================
|
|
|
+
|
|
|
+function switchNATRuleTab(tab) {
|
|
|
+ state.activeNATRuleTab = tab;
|
|
|
+ document.querySelectorAll('.nat-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(`nat-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('.nat-rule-pane').forEach(p => p.classList.add('hidden'));
|
|
|
+ const activePane = document.getElementById(`nat-pane-${tab}`);
|
|
|
+ if (activePane) activePane.classList.remove('hidden');
|
|
|
+
|
|
|
+ if (tab === 'statistics') {
|
|
|
+ startNATStatPolling();
|
|
|
+ } else {
|
|
|
+ stopNATStatPolling();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function toggleNATField(fieldKey, expand) {
|
|
|
+ const plusBtn = document.getElementById(`nat-plus-${fieldKey}`);
|
|
|
+ const fieldBox = document.getElementById(`nat-field-${fieldKey}`);
|
|
|
+ if (!plusBtn || !fieldBox) return;
|
|
|
+
|
|
|
+ if (expand) {
|
|
|
+ plusBtn.classList.add('hidden');
|
|
|
+ fieldBox.classList.remove('hidden');
|
|
|
+ const valInput = document.getElementById(`nat-val-${fieldKey}`);
|
|
|
+ if (valInput) valInput.focus();
|
|
|
+ } else {
|
|
|
+ fieldBox.classList.add('hidden');
|
|
|
+ plusBtn.classList.remove('hidden');
|
|
|
+ const valInput = document.getElementById(`nat-val-${fieldKey}`);
|
|
|
+ if (valInput) {
|
|
|
+ if (valInput.tagName === 'SELECT') {
|
|
|
+ valInput.selectedIndex = 0;
|
|
|
+ } else {
|
|
|
+ valInput.value = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const invInput = document.getElementById(`nat-inv-${fieldKey}`);
|
|
|
+ if (invInput) invInput.checked = false;
|
|
|
+
|
|
|
+ if (fieldKey === 'days') {
|
|
|
+ ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].forEach(d => {
|
|
|
+ const el = document.getElementById(`nat-day-${d}`);
|
|
|
+ if (el) el.checked = true;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function setNATFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
+ if (value !== undefined && value !== null && value !== '' && value !== 'all') {
|
|
|
+ toggleNATField(fieldKey, true);
|
|
|
+ let strVal = String(value);
|
|
|
+ if (hasInvert) {
|
|
|
+ const inv = strVal.startsWith('!');
|
|
|
+ if (inv) strVal = strVal.slice(1);
|
|
|
+ const invEl = document.getElementById(`nat-inv-${fieldKey}`);
|
|
|
+ if (invEl) invEl.checked = inv;
|
|
|
+ }
|
|
|
+ const valEl = document.getElementById(`nat-val-${fieldKey}`);
|
|
|
+ if (valEl) valEl.value = strVal;
|
|
|
+ } else {
|
|
|
+ toggleNATField(fieldKey, false);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function getNATFieldVal(fieldKey, hasInvert = false) {
|
|
|
+ const fieldBox = document.getElementById(`nat-field-${fieldKey}`);
|
|
|
+ if (!fieldBox || fieldBox.classList.contains('hidden')) return '';
|
|
|
+ const valEl = document.getElementById(`nat-val-${fieldKey}`);
|
|
|
+ let v = valEl ? valEl.value.trim() : '';
|
|
|
+ if (!v) return '';
|
|
|
+ if (hasInvert) {
|
|
|
+ const invEl = document.getElementById(`nat-inv-${fieldKey}`);
|
|
|
+ if (invEl && invEl.checked) {
|
|
|
+ v = '!' + v.replace(/^!/, '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return v;
|
|
|
+}
|
|
|
+
|
|
|
+function toggleMaximizeNATModal() {
|
|
|
+ const win = document.getElementById('nat-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 handleNATChainChange() {
|
|
|
+ const ch = document.getElementById('nat-modal-chain')?.value || 'srcnat';
|
|
|
+ const actSel = document.getElementById('nat-modal-action');
|
|
|
+ if (actSel) {
|
|
|
+ if (ch === 'srcnat' && (actSel.value === 'dst-nat' || actSel.value === 'redirect')) {
|
|
|
+ actSel.value = 'masquerade';
|
|
|
+ } else if (ch === 'dstnat' && (actSel.value === 'masquerade' || actSel.value === 'src-nat')) {
|
|
|
+ actSel.value = 'dst-nat';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ toggleNATActionFields();
|
|
|
+}
|
|
|
+
|
|
|
+function toggleNATActionFields() {
|
|
|
+ const act = document.getElementById('nat-modal-action')?.value || 'masquerade';
|
|
|
+ const toBox = document.getElementById('nat-action-to-box');
|
|
|
+ const jumpBox = document.getElementById('nat-action-jump-box');
|
|
|
+ const addrBox = document.getElementById('nat-action-addrlist-box');
|
|
|
+
|
|
|
+ if (toBox) {
|
|
|
+ const needsTo = ['src-nat', 'dst-nat', 'redirect', 'netmap', 'same', 'endpoint-independent-nat'].includes(act);
|
|
|
+ if (needsTo) toBox.classList.remove('hidden');
|
|
|
+ else toBox.classList.add('hidden');
|
|
|
+ }
|
|
|
+ if (jumpBox) {
|
|
|
+ if (act === 'jump') jumpBox.classList.remove('hidden');
|
|
|
+ else jumpBox.classList.add('hidden');
|
|
|
+ }
|
|
|
+ if (addrBox) {
|
|
|
+ if (act.includes('address-list') || act.includes('address list')) addrBox.classList.remove('hidden');
|
|
|
+ else addrBox.classList.add('hidden');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function openAddNATModal() {
|
|
|
- document.getElementById('nat-modal-title').innerText = 'Add NAT Rule';
|
|
|
+ switchNATRuleTab('general');
|
|
|
+ const badge = document.getElementById('nat-modal-badge');
|
|
|
+ if (badge) badge.innerText = 'New...';
|
|
|
document.getElementById('nat-modal-id').value = '';
|
|
|
+ document.getElementById('nat-modal-enabled').checked = true;
|
|
|
+ document.getElementById('nat-modal-comment').value = '';
|
|
|
document.getElementById('nat-modal-chain').value = 'srcnat';
|
|
|
document.getElementById('nat-modal-action').value = 'masquerade';
|
|
|
document.getElementById('nat-modal-toaddr').value = '';
|
|
|
document.getElementById('nat-modal-toport').value = '';
|
|
|
- document.getElementById('nat-modal-proto').value = 'all';
|
|
|
- document.getElementById('nat-modal-src').value = '';
|
|
|
- document.getElementById('nat-modal-dst').value = '';
|
|
|
- document.getElementById('nat-modal-dport').value = '';
|
|
|
- document.getElementById('nat-modal-comment').value = '';
|
|
|
- document.getElementById('nat-modal-disabled').checked = false;
|
|
|
+ document.getElementById('nat-modal-log').checked = false;
|
|
|
|
|
|
- populateInterfaceSelect('nat-modal-iniface');
|
|
|
- populateInterfaceSelect('nat-modal-outiface');
|
|
|
- populateAddressListSelect('nat-modal-srclist');
|
|
|
- populateAddressListSelect('nat-modal-dstlist');
|
|
|
- const srcInvEl = document.getElementById('nat-modal-srclist-inv');
|
|
|
- if (srcInvEl) srcInvEl.checked = false;
|
|
|
- const dstInvEl = document.getElementById('nat-modal-dstlist-inv');
|
|
|
- if (dstInvEl) dstInvEl.checked = false;
|
|
|
+ populateInterfaceSelect('nat-val-iniface');
|
|
|
+ populateInterfaceSelect('nat-val-outiface');
|
|
|
+ populateAddressListSelect('nat-val-srclist');
|
|
|
+ populateAddressListSelect('nat-val-dstlist');
|
|
|
|
|
|
- populateGeoIPSelect('nat-modal-srcgeoip');
|
|
|
- populateGeoIPSelect('nat-modal-dstgeoip');
|
|
|
- const srcGeoInvEl = document.getElementById('nat-modal-srcgeoip-inv');
|
|
|
- if (srcGeoInvEl) srcGeoInvEl.checked = false;
|
|
|
- const dstGeoInvEl = document.getElementById('nat-modal-dstgeoip-inv');
|
|
|
- if (dstGeoInvEl) dstGeoInvEl.checked = false;
|
|
|
+ const allFieldKeys = [
|
|
|
+ 'src', 'dst', 'srclist', 'dstlist', 'proto', 'sport', 'dport', 'anyport',
|
|
|
+ 'iniface', 'outiface', 'inifacelist', 'outifacelist', 'pktmark', 'connmark',
|
|
|
+ 'routingmark', 'conntype',
|
|
|
+ 'layer7', 'content', 'connbytes', 'connrate', 'pcc', 'srcmac',
|
|
|
+ 'outbridgeport', 'inbridgeport', 'inbridgeportlist', 'outbridgeportlist',
|
|
|
+ 'ipsec', 'ingresspriority', 'priority', 'dscp', 'tcpmss',
|
|
|
+ 'pktsize', 'random', 'icmpoptions', 'ipv4options', 'ttl',
|
|
|
+ 'time', 'days', 'limit', 'dstlimit', 'psd', 'logprefix'
|
|
|
+ ];
|
|
|
+ allFieldKeys.forEach(k => toggleNATField(k, false));
|
|
|
+
|
|
|
+ resetCurrentNATRuleCounters();
|
|
|
toggleNATActionFields();
|
|
|
|
|
|
document.getElementById('nat-modal').classList.remove('hidden');
|
|
|
@@ -3549,137 +3686,338 @@ function openEditNATModal(id) {
|
|
|
const r = (state.natRules || []).find(rule => rule.id === id);
|
|
|
if (!r) return;
|
|
|
|
|
|
- document.getElementById('nat-modal-title').innerText = `Edit NAT Rule (#${r.order})`;
|
|
|
+ switchNATRuleTab('general');
|
|
|
+ const badge = document.getElementById('nat-modal-badge');
|
|
|
+ if (badge) badge.innerText = `#${r.order != null ? r.order : r.id}`;
|
|
|
+
|
|
|
document.getElementById('nat-modal-id').value = r.id;
|
|
|
+ document.getElementById('nat-modal-enabled').checked = !r.disabled;
|
|
|
+ document.getElementById('nat-modal-comment').value = r.comment || '';
|
|
|
document.getElementById('nat-modal-chain').value = r.chain || 'srcnat';
|
|
|
document.getElementById('nat-modal-action').value = r.action || 'masquerade';
|
|
|
document.getElementById('nat-modal-toaddr').value = r.to_addresses || '';
|
|
|
document.getElementById('nat-modal-toport').value = r.to_ports || '';
|
|
|
- document.getElementById('nat-modal-proto').value = r.protocol || 'all';
|
|
|
- document.getElementById('nat-modal-src').value = r.src_address || '';
|
|
|
- document.getElementById('nat-modal-dst').value = r.dst_address || '';
|
|
|
- document.getElementById('nat-modal-dport').value = r.dst_port || '';
|
|
|
- document.getElementById('nat-modal-comment').value = r.comment || '';
|
|
|
- document.getElementById('nat-modal-disabled').checked = !!r.disabled;
|
|
|
+ document.getElementById('nat-modal-log').checked = !!r.log;
|
|
|
|
|
|
- populateInterfaceSelect('nat-modal-iniface', r.in_interface);
|
|
|
- populateInterfaceSelect('nat-modal-outiface', r.out_interface);
|
|
|
+ populateInterfaceSelect('nat-val-iniface', (r.in_interface || '').replace(/^!/, ''));
|
|
|
+ populateInterfaceSelect('nat-val-outiface', (r.out_interface || '').replace(/^!/, ''));
|
|
|
+ populateAddressListSelect('nat-val-srclist', (r.src_address_list || '').replace(/^!/, ''));
|
|
|
+ populateAddressListSelect('nat-val-dstlist', (r.dst_address_list || '').replace(/^!/, ''));
|
|
|
|
|
|
- const srcInv = (r.src_address_list || '').startsWith('!');
|
|
|
- const dstInv = (r.dst_address_list || '').startsWith('!');
|
|
|
- const srcInvEl = document.getElementById('nat-modal-srclist-inv');
|
|
|
- if (srcInvEl) srcInvEl.checked = srcInv;
|
|
|
- const dstInvEl = document.getElementById('nat-modal-dstlist-inv');
|
|
|
- if (dstInvEl) dstInvEl.checked = dstInv;
|
|
|
- populateAddressListSelect('nat-modal-srclist', r.src_address_list || '');
|
|
|
- populateAddressListSelect('nat-modal-dstlist', r.dst_address_list || '');
|
|
|
+ setNATFieldVal('src', r.src_address, true);
|
|
|
+ setNATFieldVal('dst', r.dst_address, true);
|
|
|
+ setNATFieldVal('srclist', r.src_address_list, true);
|
|
|
+ setNATFieldVal('dstlist', r.dst_address_list, true);
|
|
|
+ if (r.protocol && r.protocol !== 'all') {
|
|
|
+ setNATFieldVal('proto', r.protocol, true);
|
|
|
+ } else {
|
|
|
+ toggleNATField('proto', false);
|
|
|
+ }
|
|
|
+ setNATFieldVal('sport', r.src_port, true);
|
|
|
+ setNATFieldVal('dport', r.dst_port, true);
|
|
|
+ setNATFieldVal('anyport', r.any_port, true);
|
|
|
+ setNATFieldVal('iniface', r.in_interface, true);
|
|
|
+ setNATFieldVal('outiface', r.out_interface, true);
|
|
|
+ setNATFieldVal('inifacelist', r.in_interface_list, true);
|
|
|
+ setNATFieldVal('outifacelist', r.out_interface_list, true);
|
|
|
+ setNATFieldVal('pktmark', r.packet_mark, true);
|
|
|
+ setNATFieldVal('connmark', r.connection_mark, true);
|
|
|
+ setNATFieldVal('routingmark', r.routing_mark, true);
|
|
|
+ setNATFieldVal('conntype', r.connection_type, false);
|
|
|
|
|
|
- const srcGeoInv = (r.src_geoip || '').startsWith('!');
|
|
|
- const dstGeoInv = (r.dst_geoip || '').startsWith('!');
|
|
|
- const srcGeoInvEl = document.getElementById('nat-modal-srcgeoip-inv');
|
|
|
- if (srcGeoInvEl) srcGeoInvEl.checked = srcGeoInv;
|
|
|
- const dstGeoInvEl = document.getElementById('nat-modal-dstgeoip-inv');
|
|
|
- if (dstGeoInvEl) dstGeoInvEl.checked = dstGeoInv;
|
|
|
- populateGeoIPSelect('nat-modal-srcgeoip', r.src_geoip || '');
|
|
|
- populateGeoIPSelect('nat-modal-dstgeoip', r.dst_geoip || '');
|
|
|
- toggleNATActionFields();
|
|
|
+ // Advanced tab fields
|
|
|
+ setNATFieldVal('layer7', r.layer7_protocol, false);
|
|
|
+ setNATFieldVal('content', r.content, false);
|
|
|
+ setNATFieldVal('connbytes', r.connection_bytes, false);
|
|
|
+ setNATFieldVal('connrate', r.connection_rate, false);
|
|
|
+ setNATFieldVal('pcc', r.pcc, false);
|
|
|
+ setNATFieldVal('srcmac', r.src_mac_address, true);
|
|
|
+ setNATFieldVal('outbridgeport', r.out_bridge_port, false);
|
|
|
+ setNATFieldVal('inbridgeport', r.in_bridge_port, false);
|
|
|
+ setNATFieldVal('inbridgeportlist', r.in_bridge_port_list, false);
|
|
|
+ setNATFieldVal('outbridgeportlist', r.out_bridge_port_list, false);
|
|
|
+ setNATFieldVal('ipsec', r.ipsec_policy, false);
|
|
|
+ setNATFieldVal('ingresspriority', r.ingress_priority, false);
|
|
|
+ setNATFieldVal('priority', r.priority, false);
|
|
|
+ setNATFieldVal('dscp', r.dscp, false);
|
|
|
+ setNATFieldVal('tcpmss', r.tcp_mss, false);
|
|
|
+ if (r.tos) {
|
|
|
+ const tosEl = document.getElementById('nat-val-tos');
|
|
|
+ if (tosEl) tosEl.value = r.tos;
|
|
|
+ }
|
|
|
+ setNATFieldVal('pktsize', r.packet_size, false);
|
|
|
+ setNATFieldVal('random', r.random, false);
|
|
|
+ setNATFieldVal('icmpoptions', r.icmp_options, false);
|
|
|
+ setNATFieldVal('ipv4options', r.ipv4_options, false);
|
|
|
+ setNATFieldVal('ttl', r.ttl, false);
|
|
|
|
|
|
+ // Extra tab fields
|
|
|
+ setNATFieldVal('time', r.time, false);
|
|
|
+ if (r.days) {
|
|
|
+ toggleNATField('days', true);
|
|
|
+ ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].forEach(d => {
|
|
|
+ const chk = document.getElementById(`nat-day-${d}`);
|
|
|
+ if (chk) chk.checked = r.days.includes(d);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ toggleNATField('days', false);
|
|
|
+ }
|
|
|
+ setNATFieldVal('limit', r.limit, false);
|
|
|
+ setNATFieldVal('dstlimit', r.dst_limit, false);
|
|
|
+ setNATFieldVal('psd', r.psd, false);
|
|
|
+
|
|
|
+ // Action fields
|
|
|
+ if (r.jump_target) {
|
|
|
+ const jtEl = document.getElementById('nat-modal-jumptarget');
|
|
|
+ if (jtEl) jtEl.value = r.jump_target;
|
|
|
+ }
|
|
|
+ if (r.address_list) {
|
|
|
+ const alEl = document.getElementById('nat-modal-action-addrlist');
|
|
|
+ if (alEl) alEl.value = r.address_list;
|
|
|
+ }
|
|
|
+ if (r.address_list_timeout) {
|
|
|
+ const toEl = document.getElementById('nat-modal-action-timeout');
|
|
|
+ if (toEl) toEl.value = r.address_list_timeout;
|
|
|
+ }
|
|
|
+ setNATFieldVal('logprefix', r.log_prefix, false);
|
|
|
+
|
|
|
+ // Statistics
|
|
|
+ const bEl = document.getElementById('nat-stat-bytes');
|
|
|
+ const pEl = document.getElementById('nat-stat-packets');
|
|
|
+ if (bEl) bEl.value = formatTraffic(r.bytes || 0).replace('/s', '');
|
|
|
+ if (pEl) pEl.value = (r.packets || 0).toLocaleString();
|
|
|
+ state.natLastBytes = r.bytes || 0;
|
|
|
+ state.natLastPackets = r.packets || 0;
|
|
|
+ state.natLastTimestamp = Date.now();
|
|
|
+ state.natStatRateHistory = new Array(30).fill(0);
|
|
|
+ state.natStatPktHistory = new Array(30).fill(0);
|
|
|
+ renderFwSparkline('nat-stat-rate-svg', state.natStatRateHistory, 'bps', 'nat-stat-legend-rate');
|
|
|
+ renderFwSparkline('nat-stat-pktrate-svg', state.natStatPktHistory, 'p/s', 'nat-stat-legend-pktrate');
|
|
|
+
|
|
|
+ toggleNATActionFields();
|
|
|
document.getElementById('nat-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
function closeNATModal() {
|
|
|
+ stopNATStatPolling();
|
|
|
document.getElementById('nat-modal').classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
-function toggleNATChainFields() {
|
|
|
- const ch = document.getElementById('nat-modal-chain').value;
|
|
|
- const actSel = document.getElementById('nat-modal-action');
|
|
|
- if (ch === 'srcnat' && actSel.value === 'dst-nat') {
|
|
|
- actSel.value = 'masquerade';
|
|
|
- } else if (ch === 'dstnat' && actSel.value === 'masquerade') {
|
|
|
- actSel.value = 'dst-nat';
|
|
|
- }
|
|
|
- toggleNATActionFields();
|
|
|
-}
|
|
|
-
|
|
|
-function toggleNATActionFields() {
|
|
|
- const act = document.getElementById('nat-modal-action').value;
|
|
|
- const toBox = document.getElementById('nat-action-to-box');
|
|
|
- if (toBox) {
|
|
|
- if (act === 'masquerade' || act === 'accept') toBox.classList.add('hidden');
|
|
|
- else toBox.classList.remove('hidden');
|
|
|
- }
|
|
|
+function copyNATRule() {
|
|
|
+ document.getElementById('nat-modal-id').value = '';
|
|
|
+ const badge = document.getElementById('nat-modal-badge');
|
|
|
+ if (badge) badge.innerText = 'New...';
|
|
|
+ showToast('Rule Cloned', 'Ready to save as a new NAT rule copy', 'info');
|
|
|
+}
|
|
|
+
|
|
|
+function resetCurrentNATRuleCounters() {
|
|
|
+ state.natStatRateHistory = new Array(30).fill(0);
|
|
|
+ state.natStatPktHistory = new Array(30).fill(0);
|
|
|
+ state.natLastBytes = 0;
|
|
|
+ state.natLastPackets = 0;
|
|
|
+ state.natLastTimestamp = Date.now();
|
|
|
+ const bEl = document.getElementById('nat-stat-bytes');
|
|
|
+ const pEl = document.getElementById('nat-stat-packets');
|
|
|
+ const rEl = document.getElementById('nat-stat-rate');
|
|
|
+ const prEl = document.getElementById('nat-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('nat-stat-rate-svg', state.natStatRateHistory, 'bps', 'nat-stat-legend-rate');
|
|
|
+ renderFwSparkline('nat-stat-pktrate-svg', state.natStatPktHistory, 'p/s', 'nat-stat-legend-pktrate');
|
|
|
+ showToast('Counters Reset', 'NAT rule counter display reset to 0', 'info');
|
|
|
}
|
|
|
|
|
|
-async function submitSaveNATRule(e) {
|
|
|
- e.preventDefault();
|
|
|
+async function applyNATRule(closeAfter = false) {
|
|
|
const id = document.getElementById('nat-modal-id').value;
|
|
|
const chain = document.getElementById('nat-modal-chain').value;
|
|
|
const action = document.getElementById('nat-modal-action').value;
|
|
|
- const toaddr = document.getElementById('nat-modal-toaddr').value.trim();
|
|
|
- const toport = document.getElementById('nat-modal-toport').value.trim();
|
|
|
- const proto = document.getElementById('nat-modal-proto').value;
|
|
|
- const src = document.getElementById('nat-modal-src').value.trim();
|
|
|
- const dst = document.getElementById('nat-modal-dst').value.trim();
|
|
|
- const dport = document.getElementById('nat-modal-dport').value.trim();
|
|
|
- const inIface = document.getElementById('nat-modal-iniface').value;
|
|
|
- const outIface = document.getElementById('nat-modal-outiface').value;
|
|
|
-
|
|
|
- let srclist = document.getElementById('nat-modal-srclist')?.value || '';
|
|
|
- if (srclist && document.getElementById('nat-modal-srclist-inv')?.checked) {
|
|
|
- srclist = '!' + srclist.replace(/^!/, '');
|
|
|
- }
|
|
|
- let dstlist = document.getElementById('nat-modal-dstlist')?.value || '';
|
|
|
- if (dstlist && document.getElementById('nat-modal-dstlist-inv')?.checked) {
|
|
|
- dstlist = '!' + dstlist.replace(/^!/, '');
|
|
|
- }
|
|
|
+ const comment = document.getElementById('nat-modal-comment').value.trim();
|
|
|
+ const enabled = document.getElementById('nat-modal-enabled').checked;
|
|
|
+
|
|
|
+ const src = getNATFieldVal('src', true);
|
|
|
+ const dst = getNATFieldVal('dst', true);
|
|
|
+ const srclist = getNATFieldVal('srclist', true);
|
|
|
+ const dstlist = getNATFieldVal('dstlist', true);
|
|
|
+ const proto = getNATFieldVal('proto', true);
|
|
|
+ const sport = getNATFieldVal('sport', true);
|
|
|
+ const dport = getNATFieldVal('dport', true);
|
|
|
+ const anyport = getNATFieldVal('anyport', true);
|
|
|
+ const iniface = getNATFieldVal('iniface', true);
|
|
|
+ const outiface = getNATFieldVal('outiface', true);
|
|
|
+ const inifacelist = getNATFieldVal('inifacelist', true);
|
|
|
+ const outifacelist = getNATFieldVal('outifacelist', true);
|
|
|
+ const pktmark = getNATFieldVal('pktmark', true);
|
|
|
+ const connmark = getNATFieldVal('connmark', true);
|
|
|
+ const routingmark = getNATFieldVal('routingmark', true);
|
|
|
+ const conntype = getNATFieldVal('conntype', false);
|
|
|
|
|
|
- let srcgeoip = document.getElementById('nat-modal-srcgeoip')?.value || '';
|
|
|
- if (srcgeoip && document.getElementById('nat-modal-srcgeoip-inv')?.checked) {
|
|
|
- srcgeoip = '!' + srcgeoip.replace(/^!/, '');
|
|
|
- }
|
|
|
- let dstgeoip = document.getElementById('nat-modal-dstgeoip')?.value || '';
|
|
|
- if (dstgeoip && document.getElementById('nat-modal-dstgeoip-inv')?.checked) {
|
|
|
- dstgeoip = '!' + dstgeoip.replace(/^!/, '');
|
|
|
- }
|
|
|
+ const payload = {
|
|
|
+ id,
|
|
|
+ chain,
|
|
|
+ action,
|
|
|
+ to_addresses: document.getElementById('nat-modal-toaddr')?.value.trim() || '',
|
|
|
+ to_ports: document.getElementById('nat-modal-toport')?.value.trim() || '',
|
|
|
+ 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,
|
|
|
+ src_address_list: srclist,
|
|
|
+ dst_address_list: dstlist,
|
|
|
+ comment,
|
|
|
+ disabled: !enabled,
|
|
|
|
|
|
- const comment = document.getElementById('nat-modal-comment').value.trim();
|
|
|
- const disabled = document.getElementById('nat-modal-disabled').checked;
|
|
|
+ // Advanced tab
|
|
|
+ layer7_protocol: getNATFieldVal('layer7', false),
|
|
|
+ content: getNATFieldVal('content', false),
|
|
|
+ connection_bytes: getNATFieldVal('connbytes', false),
|
|
|
+ connection_rate: getNATFieldVal('connrate', false),
|
|
|
+ pcc: getNATFieldVal('pcc', false),
|
|
|
+ src_mac_address: getNATFieldVal('srcmac', true),
|
|
|
+ out_bridge_port: getNATFieldVal('outbridgeport', false),
|
|
|
+ in_bridge_port: getNATFieldVal('inbridgeport', false),
|
|
|
+ in_bridge_port_list: getNATFieldVal('inbridgeportlist', false),
|
|
|
+ out_bridge_port_list: getNATFieldVal('outbridgeportlist', false),
|
|
|
+ ipsec_policy: getNATFieldVal('ipsec', false),
|
|
|
+ ingress_priority: getNATFieldVal('ingresspriority', false),
|
|
|
+ priority: getNATFieldVal('priority', false),
|
|
|
+ dscp: getNATFieldVal('dscp', false),
|
|
|
+ tcp_mss: getNATFieldVal('tcpmss', false),
|
|
|
+ tos: document.getElementById('nat-val-tos')?.value || '',
|
|
|
+ packet_size: getNATFieldVal('pktsize', false),
|
|
|
+ random: getNATFieldVal('random', false),
|
|
|
+ icmp_options: getNATFieldVal('icmpoptions', false),
|
|
|
+ ipv4_options: getNATFieldVal('ipv4options', false),
|
|
|
+ ttl: getNATFieldVal('ttl', false),
|
|
|
+
|
|
|
+ // Extra tab
|
|
|
+ time: getNATFieldVal('time', false),
|
|
|
+ limit: getNATFieldVal('limit', false),
|
|
|
+ dst_limit: getNATFieldVal('dstlimit', false),
|
|
|
+ psd: getNATFieldVal('psd', false),
|
|
|
+
|
|
|
+ // Action tab
|
|
|
+ jump_target: action === 'jump' ? (document.getElementById('nat-modal-jumptarget')?.value || '') : '',
|
|
|
+ address_list: action.includes('address') ? (document.getElementById('nat-modal-action-addrlist')?.value || '') : '',
|
|
|
+ address_list_timeout: action.includes('address') ? (document.getElementById('nat-modal-action-timeout')?.value || '') : '',
|
|
|
+ log: document.getElementById('nat-modal-log')?.checked || false,
|
|
|
+ log_prefix: getNATFieldVal('logprefix', false)
|
|
|
+ };
|
|
|
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/nat`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({
|
|
|
- id,
|
|
|
- chain,
|
|
|
- action,
|
|
|
- to_addresses: toaddr,
|
|
|
- to_ports: toport,
|
|
|
- protocol: proto,
|
|
|
- src_address: src,
|
|
|
- dst_address: dst,
|
|
|
- dst_port: dport,
|
|
|
- in_interface: inIface,
|
|
|
- out_interface: outIface,
|
|
|
- src_address_list: srclist,
|
|
|
- dst_address_list: dstlist,
|
|
|
- src_geoip: srcgeoip,
|
|
|
- dst_geoip: dstgeoip,
|
|
|
- comment,
|
|
|
- disabled
|
|
|
- })
|
|
|
+ body: JSON.stringify(payload)
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
+ const data = await res.json();
|
|
|
showToast('NAT Rule Saved', 'NAT mapping updated', 'success');
|
|
|
- closeNATModal();
|
|
|
loadFirewall(false);
|
|
|
+ if (closeAfter) {
|
|
|
+ closeNATModal();
|
|
|
+ } else {
|
|
|
+ if (data.id) {
|
|
|
+ document.getElementById('nat-modal-id').value = data.id;
|
|
|
+ const badge = document.getElementById('nat-modal-badge');
|
|
|
+ if (badge) badge.innerText = `#${data.id}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.message || 'Failed to save rule', 'error');
|
|
|
}
|
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function submitSaveNATRule(e) {
|
|
|
+ if (e && e.preventDefault) e.preventDefault();
|
|
|
+ return applyNATRule(true);
|
|
|
+}
|
|
|
+
|
|
|
+function startNATStatPolling() {
|
|
|
+ stopNATStatPolling();
|
|
|
+ pollNATRuleStats();
|
|
|
+ state.natStatInterval = setInterval(pollNATRuleStats, 1000);
|
|
|
+}
|
|
|
+
|
|
|
+function stopNATStatPolling() {
|
|
|
+ if (state.natStatInterval) {
|
|
|
+ clearInterval(state.natStatInterval);
|
|
|
+ state.natStatInterval = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function pollNATRuleStats() {
|
|
|
+ const id = document.getElementById('nat-modal-id')?.value;
|
|
|
+ if (!id) {
|
|
|
+ renderFwSparkline('nat-stat-rate-svg', state.natStatRateHistory || new Array(30).fill(0), 'bps', 'nat-stat-legend-rate');
|
|
|
+ renderFwSparkline('nat-stat-pktrate-svg', state.natStatPktHistory || new Array(30).fill(0), 'p/s', 'nat-stat-legend-pktrate');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/firewall/nat`);
|
|
|
+ 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.natLastTimestamp || now)) / 1000);
|
|
|
+ state.natLastTimestamp = now;
|
|
|
+
|
|
|
+ const curBytes = r.bytes || 0;
|
|
|
+ const curPackets = r.packets || 0;
|
|
|
+
|
|
|
+ const deltaBytes = Math.max(0, curBytes - (state.natLastBytes || curBytes));
|
|
|
+ const deltaPkts = Math.max(0, curPackets - (state.natLastPackets || curPackets));
|
|
|
+
|
|
|
+ state.natLastBytes = curBytes;
|
|
|
+ state.natLastPackets = curPackets;
|
|
|
+
|
|
|
+ const rateBps = Math.round((deltaBytes * 8) / dt);
|
|
|
+ const ratePkt = Math.round(deltaPkts / dt);
|
|
|
+
|
|
|
+ if (!state.natStatRateHistory) state.natStatRateHistory = new Array(30).fill(0);
|
|
|
+ if (!state.natStatPktHistory) state.natStatPktHistory = new Array(30).fill(0);
|
|
|
+
|
|
|
+ state.natStatRateHistory.push(rateBps);
|
|
|
+ if (state.natStatRateHistory.length > 30) state.natStatRateHistory.shift();
|
|
|
+
|
|
|
+ state.natStatPktHistory.push(ratePkt);
|
|
|
+ if (state.natStatPktHistory.length > 30) state.natStatPktHistory.shift();
|
|
|
+
|
|
|
+ const bEl = document.getElementById('nat-stat-bytes');
|
|
|
+ const pEl = document.getElementById('nat-stat-packets');
|
|
|
+ const rEl = document.getElementById('nat-stat-rate');
|
|
|
+ const prEl = document.getElementById('nat-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('nat-stat-rate-svg', state.natStatRateHistory, 'bps', 'nat-stat-legend-rate');
|
|
|
+ renderFwSparkline('nat-stat-pktrate-svg', state.natStatPktHistory, 'p/s', 'nat-stat-legend-pktrate');
|
|
|
+ } catch (e) {
|
|
|
+ console.error('Error polling NAT rule stats', e);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
async function moveNATRule(id, dir) {
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/nat/${encodeURIComponent(id)}/move`, {
|