|
|
@@ -3788,12 +3788,18 @@ async function loadFirewall(populateDropdowns = true) {
|
|
|
]);
|
|
|
|
|
|
if (summaryRes.ok) state.fwSummary = await summaryRes.json();
|
|
|
+ const applyError = document.getElementById('fw-apply-error');
|
|
|
+ if (applyError) {
|
|
|
+ applyError.textContent = state.fwSummary?.last_error ? 'Firewall configuration was not applied: ' + state.fwSummary.last_error : '';
|
|
|
+ applyError.classList.toggle('hidden', !state.fwSummary?.last_error);
|
|
|
+ }
|
|
|
if (filterRes.ok) state.filterRules = await filterRes.json();
|
|
|
if (natRes.ok) state.natRules = await natRes.json();
|
|
|
if (mangleRes.ok) state.mangleRules = await mangleRes.json();
|
|
|
if (addrlistRes.ok) state.addressLists = await addrlistRes.json();
|
|
|
if (geoStatusRes.ok) state.geoStatus = await geoStatusRes.json();
|
|
|
if (geoCountriesRes.ok) state.geoCountries = await geoCountriesRes.json();
|
|
|
+ if (applyError) { const messages=[state.fwSummary?.last_error, state.fwSummary?.connections_error, state.geoStatus?.last_error && 'GeoIP: '+state.geoStatus.last_error].filter(Boolean); applyError.textContent=messages.join(' · '); applyError.classList.toggle('hidden', !messages.length); }
|
|
|
|
|
|
// Badges
|
|
|
const bFilter = document.getElementById('fw-badge-filter');
|
|
|
@@ -3810,7 +3816,7 @@ async function loadFirewall(populateDropdowns = true) {
|
|
|
const sigFilter = (state.filterRules || []).map(r => JSON.stringify({...r, packets: 0, bytes: 0})).join(';');
|
|
|
const sigNat = (state.natRules || []).map(r => JSON.stringify({...r, packets: 0, bytes: 0})).join(';');
|
|
|
const sigMangle = (state.mangleRules || []).map(r => JSON.stringify({...r, packets: 0, bytes: 0})).join(';');
|
|
|
- const sigAddr = (state.addressLists || []).map(a => `${a.id}:${a.name}:${a.address}:${a.disabled}`).join(';');
|
|
|
+ const sigAddr = (state.addressLists || []).map(a => JSON.stringify(a)).join(';');
|
|
|
|
|
|
const tbodyFilter = document.getElementById('filter-table-body');
|
|
|
if (tbodyFilter) {
|
|
|
@@ -3887,7 +3893,8 @@ function renderFilterRules() {
|
|
|
}
|
|
|
empty?.classList.add('hidden');
|
|
|
|
|
|
- rules.forEach((r, idx) => {
|
|
|
+ rules.forEach((raw, idx) => {
|
|
|
+ const r = safeFirewallRow(raw);
|
|
|
const tr = document.createElement('tr');
|
|
|
tr.className = `hover:bg-slate-800/40 transition text-xs ${r.disabled ? 'opacity-50' : ''}`;
|
|
|
const ports = [r.src_port ? 's:' + r.src_port : '', r.dst_port ? 'd:' + r.dst_port : ''].filter(Boolean).join(' ');
|
|
|
@@ -3967,7 +3974,8 @@ function populateAddressListSelect(selectId, currentVal = '') {
|
|
|
sel.innerHTML = '<option value="">(None)</option>';
|
|
|
|
|
|
const cleanVal = (currentVal || '').replace(/^!/, '');
|
|
|
- const customNames = Array.from(new Set((state.addressLists || []).map(a => a.name))).sort();
|
|
|
+ const ruleLists = [...(state.filterRules||[]), ...(state.natRules||[]), ...(state.mangleRules||[])].flatMap(r => [r.src_address_list,r.dst_address_list,r.address_list]).filter(Boolean).map(n => n.replace(/^!/, '')).filter(n => !n.startsWith('geo:') && !n.startsWith('geo_'));
|
|
|
+ const customNames = Array.from(new Set([...(state.addressLists || []).map(a => a.name), ...ruleLists])).sort();
|
|
|
customNames.forEach(name => {
|
|
|
const count = (state.addressLists || []).filter(a => a.name === name).length;
|
|
|
const opt = document.createElement('option');
|
|
|
@@ -3977,7 +3985,7 @@ function populateAddressListSelect(selectId, currentVal = '') {
|
|
|
sel.appendChild(opt);
|
|
|
});
|
|
|
|
|
|
- if (cleanVal && !sel.querySelector(`option[value="${cleanVal}"]`)) {
|
|
|
+ if (cleanVal && !Array.from(sel.options).some(option => option.value === cleanVal)) {
|
|
|
const opt = document.createElement('option');
|
|
|
opt.value = cleanVal;
|
|
|
opt.innerText = cleanVal;
|
|
|
@@ -4108,6 +4116,7 @@ function toggleFwField(fieldKey, expand) {
|
|
|
}
|
|
|
|
|
|
function setFwFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
+ if (fieldKey === 'ipsec' && typeof value === 'string') value = value.replace(':', ',');
|
|
|
if (value !== undefined && value !== null && value !== '' && value !== 'all') {
|
|
|
toggleFwField(fieldKey, true);
|
|
|
let strVal = String(value);
|
|
|
@@ -4116,9 +4125,10 @@ function setFwFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
if (inv) strVal = strVal.slice(1);
|
|
|
const invEl = document.getElementById(`fw-inv-${fieldKey}`);
|
|
|
if (invEl) invEl.checked = inv;
|
|
|
+ else if (inv) strVal = '!'+strVal;
|
|
|
}
|
|
|
const valEl = document.getElementById(`fw-val-${fieldKey}`);
|
|
|
- if (valEl) valEl.value = strVal;
|
|
|
+ if (valEl) { if (valEl.tagName === 'SELECT' && !Array.from(valEl.options).some(o => o.value === strVal)) { const option=document.createElement('option'); option.value=strVal; option.textContent=strVal+' (saved)'; valEl.appendChild(option); } valEl.value = strVal; }
|
|
|
} else {
|
|
|
toggleFwField(fieldKey, false);
|
|
|
}
|
|
|
@@ -4152,7 +4162,9 @@ function toggleMaximizeFilterModal() {
|
|
|
}
|
|
|
|
|
|
function openAddFilterModal() {
|
|
|
+ const tos = document.getElementById('fw-val-tos'); if (tos) tos.value = '';
|
|
|
switchFwRuleTab('general');
|
|
|
+ setFirewallDays('fw', '');
|
|
|
const badge = document.getElementById('fw-modal-badge');
|
|
|
if (badge) badge.innerText = 'New...';
|
|
|
document.getElementById('filter-modal-id').value = '';
|
|
|
@@ -4182,13 +4194,15 @@ function openAddFilterModal() {
|
|
|
];
|
|
|
allFieldKeys.forEach(k => toggleFwField(k, false));
|
|
|
|
|
|
- resetCurrentRuleCounters();
|
|
|
+ resetRuleCounterDisplay();
|
|
|
toggleFilterActionFields();
|
|
|
|
|
|
+ refreshFirewallRejectTypes();
|
|
|
document.getElementById('filter-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
function openEditFilterModal(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
const r = (state.filterRules || []).find(rule => rule.id === id);
|
|
|
if (!r) return;
|
|
|
|
|
|
@@ -4201,7 +4215,7 @@ function openEditFilterModal(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-action').value = r.action || 'accept';
|
|
|
+ setFirewallActionSelection('filter', r.action || 'accept');
|
|
|
document.getElementById('filter-modal-log').checked = !!r.log;
|
|
|
|
|
|
populateInterfaceSelect('fw-val-iniface', (r.in_interface || '').replace(/^!/, ''));
|
|
|
@@ -4272,10 +4286,7 @@ function openEditFilterModal(id) {
|
|
|
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;
|
|
|
- }
|
|
|
+ { 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);
|
|
|
@@ -4289,7 +4300,7 @@ function openEditFilterModal(id) {
|
|
|
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);
|
|
|
+ if (chk) chk.checked = r.days.toLowerCase().includes(d);
|
|
|
});
|
|
|
} else {
|
|
|
toggleFwField('days', false);
|
|
|
@@ -4331,6 +4342,7 @@ function openEditFilterModal(id) {
|
|
|
renderFwSparkline('fw-stat-pktrate-svg', state.fwStatPktHistory, 'p/s', 'fw-stat-legend-pktrate');
|
|
|
|
|
|
toggleFilterActionFields();
|
|
|
+ refreshFirewallRejectTypes();
|
|
|
document.getElementById('filter-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
@@ -4379,7 +4391,7 @@ function copyFilterRule() {
|
|
|
showToast('Rule Cloned', 'Ready to save as a new rule copy', 'info');
|
|
|
}
|
|
|
|
|
|
-function resetCurrentRuleCounters() {
|
|
|
+function resetRuleCounterDisplay() {
|
|
|
state.fwStatRateHistory = new Array(30).fill(0);
|
|
|
state.fwStatPktHistory = new Array(30).fill(0);
|
|
|
state.fwLastBytes = 0;
|
|
|
@@ -4402,7 +4414,7 @@ async function resetAllFwCounters() {
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/reset-counters`, { method: 'POST' });
|
|
|
if (res.ok) {
|
|
|
- resetCurrentRuleCounters();
|
|
|
+ resetRuleCounterDisplay();
|
|
|
showToast('All Counters Reset', 'All firewall counters zeroed', 'success');
|
|
|
loadFirewall(false);
|
|
|
}
|
|
|
@@ -4413,6 +4425,8 @@ async function resetAllFwCounters() {
|
|
|
|
|
|
async function applyFilterRule(closeAfter = false) {
|
|
|
const id = document.getElementById('filter-modal-id').value;
|
|
|
+ if (!reviewLegacyFirewallSettings('filter', id)) return;
|
|
|
+ let selectedDays; try { selectedDays = getFirewallDays('fw'); } catch(error) { showToast('Error', error.message, 'error'); return; }
|
|
|
const chain = document.getElementById('filter-modal-chain').value;
|
|
|
const action = document.getElementById('filter-modal-action').value;
|
|
|
const comment = document.getElementById('filter-modal-comment').value.trim();
|
|
|
@@ -4483,20 +4497,11 @@ async function applyFilterRule(closeAfter = false) {
|
|
|
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 || '',
|
|
|
@@ -4504,14 +4509,13 @@ async function applyFilterRule(closeAfter = false) {
|
|
|
random: getFwFieldVal('random', false),
|
|
|
tcp_flags: getFwFieldVal('tcpflags', false),
|
|
|
icmp_options: getFwFieldVal('icmpoptions', false),
|
|
|
- ipv4_options: getFwFieldVal('ipv4options', false),
|
|
|
ttl: getFwFieldVal('ttl', false),
|
|
|
|
|
|
+ days: selectedDays,
|
|
|
+
|
|
|
// 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 || '') : '',
|
|
|
@@ -4590,8 +4594,8 @@ async function pollRuleStats() {
|
|
|
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));
|
|
|
+ const deltaBytes = Math.max(0, curBytes - (state.fwLastBytes ?? curBytes));
|
|
|
+ const deltaPkts = Math.max(0, curPackets - (state.fwLastPackets ?? curPackets));
|
|
|
|
|
|
state.fwLastBytes = curBytes;
|
|
|
state.fwLastPackets = curPackets;
|
|
|
@@ -4679,6 +4683,7 @@ function renderFwSparkline(svgId, dataArray, unit, legendId) {
|
|
|
}
|
|
|
|
|
|
async function moveFilterRule(id, dir) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/filter/${encodeURIComponent(id)}/move`, {
|
|
|
method: 'POST',
|
|
|
@@ -4692,6 +4697,7 @@ async function moveFilterRule(id, dir) {
|
|
|
}
|
|
|
|
|
|
async function toggleFilterRuleState(id, disabled) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/filter/${encodeURIComponent(id)}/state`, {
|
|
|
method: 'POST',
|
|
|
@@ -4705,6 +4711,7 @@ async function toggleFilterRuleState(id, disabled) {
|
|
|
}
|
|
|
|
|
|
async function deleteFilterRule(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
if (!confirm('Are you sure you want to delete this filter rule?')) return;
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/filter/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
@@ -4734,7 +4741,8 @@ function renderNATRules() {
|
|
|
}
|
|
|
empty?.classList.add('hidden');
|
|
|
|
|
|
- rules.forEach((r, idx) => {
|
|
|
+ rules.forEach((raw, idx) => {
|
|
|
+ const r = safeFirewallRow(raw);
|
|
|
const tr = document.createElement('tr');
|
|
|
tr.className = `hover:bg-slate-800/40 transition text-xs ${r.disabled ? 'opacity-50' : ''}`;
|
|
|
|
|
|
@@ -4840,6 +4848,7 @@ function toggleNATField(fieldKey, expand) {
|
|
|
}
|
|
|
|
|
|
function setNATFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
+ if (fieldKey === 'ipsec' && typeof value === 'string') value = value.replace(':', ',');
|
|
|
if (value !== undefined && value !== null && value !== '' && value !== 'all') {
|
|
|
toggleNATField(fieldKey, true);
|
|
|
let strVal = String(value);
|
|
|
@@ -4848,9 +4857,10 @@ function setNATFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
if (inv) strVal = strVal.slice(1);
|
|
|
const invEl = document.getElementById(`nat-inv-${fieldKey}`);
|
|
|
if (invEl) invEl.checked = inv;
|
|
|
+ else if (inv) strVal = '!'+strVal;
|
|
|
}
|
|
|
const valEl = document.getElementById(`nat-val-${fieldKey}`);
|
|
|
- if (valEl) valEl.value = strVal;
|
|
|
+ if (valEl) { if (valEl.tagName === 'SELECT' && !Array.from(valEl.options).some(o => o.value === strVal)) { const option=document.createElement('option'); option.value=strVal; option.textContent=strVal+' (saved)'; valEl.appendChild(option); } valEl.value = strVal; }
|
|
|
} else {
|
|
|
toggleNATField(fieldKey, false);
|
|
|
}
|
|
|
@@ -4909,7 +4919,7 @@ function toggleNATActionFields() {
|
|
|
if (jumpBox) jumpBox.classList.add('hidden');
|
|
|
if (addrBox) addrBox.classList.add('hidden');
|
|
|
|
|
|
- if (['src-nat', 'dst-nat', 'netmap', 'same', 'socksify', 'endpoint-independent-nat'].includes(act)) {
|
|
|
+ if (['src-nat', 'dst-nat', 'netmap'].includes(act)) {
|
|
|
if (rowAddr) rowAddr.classList.remove('hidden');
|
|
|
if (rowPort) rowPort.classList.remove('hidden');
|
|
|
} else if (act === 'redirect' || act === 'masquerade') {
|
|
|
@@ -4933,7 +4943,9 @@ function toggleNATLogPrefix() {
|
|
|
}
|
|
|
|
|
|
function openAddNATModal() {
|
|
|
+ const tos = document.getElementById('nat-val-tos'); if (tos) tos.value = '';
|
|
|
switchNATRuleTab('general');
|
|
|
+ setFirewallDays('nat', '');
|
|
|
const badge = document.getElementById('nat-modal-badge');
|
|
|
if (badge) badge.innerText = 'New...';
|
|
|
document.getElementById('nat-modal-id').value = '';
|
|
|
@@ -4965,16 +4977,18 @@ function openAddNATModal() {
|
|
|
];
|
|
|
allFieldKeys.forEach(k => toggleNATField(k, false));
|
|
|
|
|
|
- resetCurrentNATRuleCounters();
|
|
|
+ resetNATCounterDisplay();
|
|
|
toggleNATActionFields();
|
|
|
|
|
|
document.getElementById('nat-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
function openEditNATModal(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
const r = (state.natRules || []).find(rule => rule.id === id);
|
|
|
if (!r) return;
|
|
|
|
|
|
+ setFirewallDays('nat', r.days || '');
|
|
|
switchNATRuleTab('general');
|
|
|
const badge = document.getElementById('nat-modal-badge');
|
|
|
if (badge) badge.innerText = `#${r.order != null ? r.order : r.id}`;
|
|
|
@@ -4984,7 +4998,7 @@ function openEditNATModal(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';
|
|
|
+ setFirewallActionSelection('nat', r.action || 'accept');
|
|
|
document.getElementById('nat-modal-toaddr').value = r.to_addresses || '';
|
|
|
document.getElementById('nat-modal-toport').value = r.to_ports || '';
|
|
|
document.getElementById('nat-modal-log').checked = !!r.log;
|
|
|
@@ -5035,10 +5049,7 @@ function openEditNATModal(id) {
|
|
|
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;
|
|
|
- }
|
|
|
+ { 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);
|
|
|
@@ -5051,7 +5062,7 @@ function openEditNATModal(id) {
|
|
|
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);
|
|
|
+ if (chk) chk.checked = r.days.toLowerCase().includes(d);
|
|
|
});
|
|
|
} else {
|
|
|
toggleNATField('days', false);
|
|
|
@@ -5104,7 +5115,7 @@ function copyNATRule() {
|
|
|
showToast('Rule Cloned', 'Ready to save as a new NAT rule copy', 'info');
|
|
|
}
|
|
|
|
|
|
-function resetCurrentNATRuleCounters() {
|
|
|
+function resetNATCounterDisplay() {
|
|
|
state.natStatRateHistory = new Array(30).fill(0);
|
|
|
state.natStatPktHistory = new Array(30).fill(0);
|
|
|
state.natLastBytes = 0;
|
|
|
@@ -5125,6 +5136,8 @@ function resetCurrentNATRuleCounters() {
|
|
|
|
|
|
async function applyNATRule(closeAfter = false) {
|
|
|
const id = document.getElementById('nat-modal-id').value;
|
|
|
+ if (!reviewLegacyFirewallSettings('nat', id)) return;
|
|
|
+ let selectedDays; try { selectedDays = getFirewallDays('nat'); } catch(error) { showToast('Error', error.message, 'error'); return; }
|
|
|
const chain = document.getElementById('nat-modal-chain').value;
|
|
|
const action = document.getElementById('nat-modal-action').value;
|
|
|
const comment = document.getElementById('nat-modal-comment').value.trim();
|
|
|
@@ -5149,8 +5162,8 @@ async function applyNATRule(closeAfter = false) {
|
|
|
const routingmark = getNATFieldVal('routingmark', true);
|
|
|
const conntype = getNATFieldVal('conntype', false);
|
|
|
|
|
|
- const usesAddr = ['src-nat', 'dst-nat', 'netmap', 'same', 'socksify', 'endpoint-independent-nat'].includes(action);
|
|
|
- const usesPort = ['src-nat', 'dst-nat', 'netmap', 'same', 'socksify', 'endpoint-independent-nat', 'redirect', 'masquerade'].includes(action);
|
|
|
+ const usesAddr = ['src-nat', 'dst-nat', 'netmap'].includes(action);
|
|
|
+ const usesPort = ['src-nat', 'dst-nat', 'redirect', 'masquerade'].includes(action);
|
|
|
|
|
|
const payload = {
|
|
|
ip_version: Number(document.getElementById('nat-modal-family').value),
|
|
|
@@ -5181,33 +5194,24 @@ async function applyNATRule(closeAfter = false) {
|
|
|
disabled: !enabled,
|
|
|
|
|
|
// 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),
|
|
|
|
|
|
+ days: selectedDays,
|
|
|
+
|
|
|
// 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 || '') : '',
|
|
|
@@ -5285,8 +5289,8 @@ async function pollNATRuleStats() {
|
|
|
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));
|
|
|
+ const deltaBytes = Math.max(0, curBytes - (state.natLastBytes ?? curBytes));
|
|
|
+ const deltaPkts = Math.max(0, curPackets - (state.natLastPackets ?? curPackets));
|
|
|
|
|
|
state.natLastBytes = curBytes;
|
|
|
state.natLastPackets = curPackets;
|
|
|
@@ -5321,6 +5325,7 @@ async function pollNATRuleStats() {
|
|
|
}
|
|
|
|
|
|
async function moveNATRule(id, dir) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/nat/${encodeURIComponent(id)}/move`, {
|
|
|
method: 'POST',
|
|
|
@@ -5334,6 +5339,7 @@ async function moveNATRule(id, dir) {
|
|
|
}
|
|
|
|
|
|
async function toggleNATRuleState(id, disabled) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/nat/${encodeURIComponent(id)}/state`, {
|
|
|
method: 'POST',
|
|
|
@@ -5347,6 +5353,7 @@ async function toggleNATRuleState(id, disabled) {
|
|
|
}
|
|
|
|
|
|
async function deleteNATRule(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
if (!confirm('Are you sure you want to delete this NAT rule?')) return;
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/nat/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
@@ -5376,7 +5383,8 @@ function renderMangleRules() {
|
|
|
}
|
|
|
empty?.classList.add('hidden');
|
|
|
|
|
|
- rules.forEach((r, idx) => {
|
|
|
+ rules.forEach((raw, idx) => {
|
|
|
+ const r = safeFirewallRow(raw);
|
|
|
const tr = document.createElement('tr');
|
|
|
tr.className = `hover:bg-slate-800/40 transition text-xs ${r.disabled ? 'opacity-50' : ''}`;
|
|
|
let markVal = '<span class="text-slate-500">-</span>';
|
|
|
@@ -5384,7 +5392,7 @@ function renderMangleRules() {
|
|
|
else if (r.new_routing_mark) markVal = `<span class="text-amber-300 font-mono font-bold">${r.new_routing_mark}</span>`;
|
|
|
else if (r.new_packet_mark) markVal = `<span class="text-emerald-300 font-mono font-bold">${r.new_packet_mark}</span>`;
|
|
|
else if (r.new_mss) markVal = `<span class="text-sky-300 font-mono font-bold">mss:${r.new_mss}</span>`;
|
|
|
- else if (r.new_dscp) markVal = `<span class="text-purple-300 font-mono font-bold">dscp:${r.new_dscp}</span>`;
|
|
|
+ else if (r.action === 'change-dscp') markVal = `<span class="text-purple-300 font-mono font-bold">dscp:${r.new_dscp}</span>`;
|
|
|
else if (r.new_ttl) markVal = `<span class="text-blue-300 font-mono font-bold">ttl:${r.new_ttl}</span>`;
|
|
|
else if (r.new_priority) markVal = `<span class="text-indigo-300 font-mono font-bold">prio:${r.new_priority}</span>`;
|
|
|
else if (r.route) markVal = `<span class="text-orange-300 font-mono font-bold">route:${r.route}</span>`;
|
|
|
@@ -5483,6 +5491,7 @@ function toggleMangleField(fieldKey, expand) {
|
|
|
}
|
|
|
|
|
|
function setMangleFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
+ if (fieldKey === 'ipsec' && typeof value === 'string') value = value.replace(':', ',');
|
|
|
if (value !== undefined && value !== null && value !== '' && value !== 'all') {
|
|
|
toggleMangleField(fieldKey, true);
|
|
|
let strVal = String(value);
|
|
|
@@ -5491,9 +5500,10 @@ function setMangleFieldVal(fieldKey, value, hasInvert = false) {
|
|
|
if (inv) strVal = strVal.slice(1);
|
|
|
const invEl = document.getElementById(`mangle-inv-${fieldKey}`);
|
|
|
if (invEl) invEl.checked = inv;
|
|
|
+ else if (inv) strVal = '!'+strVal;
|
|
|
}
|
|
|
const valEl = document.getElementById(`mangle-val-${fieldKey}`);
|
|
|
- if (valEl) valEl.value = strVal;
|
|
|
+ if (valEl) { if (valEl.tagName === 'SELECT' && !Array.from(valEl.options).some(o => o.value === strVal)) { const option=document.createElement('option'); option.value=strVal; option.textContent=strVal+' (saved)'; valEl.appendChild(option); } valEl.value = strVal; }
|
|
|
} else {
|
|
|
toggleMangleField(fieldKey, false);
|
|
|
}
|
|
|
@@ -5600,6 +5610,7 @@ const allMangleFieldKeys = [
|
|
|
];
|
|
|
|
|
|
function openAddMangleModal() {
|
|
|
+ const tos = document.getElementById('mangle-val-tos'); if (tos) tos.value = '';
|
|
|
switchMangleRuleTab('general');
|
|
|
const badge = document.getElementById('mangle-modal-badge');
|
|
|
if (badge) badge.innerText = 'New...';
|
|
|
@@ -5634,13 +5645,14 @@ function openAddMangleModal() {
|
|
|
|
|
|
allMangleFieldKeys.forEach(k => toggleMangleField(k, false));
|
|
|
|
|
|
- resetCurrentMangleRuleCounters();
|
|
|
+ resetMangleCounterDisplay();
|
|
|
toggleMangleActionFields();
|
|
|
|
|
|
document.getElementById('mangle-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
function openEditMangleModal(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
const r = (state.mangleRules || []).find(rule => rule.id === id);
|
|
|
if (!r) return;
|
|
|
|
|
|
@@ -5653,7 +5665,7 @@ function openEditMangleModal(id) {
|
|
|
document.getElementById('mangle-modal-enabled').checked = !r.disabled;
|
|
|
document.getElementById('mangle-modal-comment').value = r.comment || '';
|
|
|
document.getElementById('mangle-modal-chain').value = r.chain || 'prerouting';
|
|
|
- document.getElementById('mangle-modal-action').value = r.action || 'mark-connection';
|
|
|
+ setFirewallActionSelection('mangle', 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 || '';
|
|
|
@@ -5741,10 +5753,7 @@ function openEditMangleModal(id) {
|
|
|
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;
|
|
|
- }
|
|
|
+ { 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);
|
|
|
@@ -5808,7 +5817,7 @@ function copyMangleRule() {
|
|
|
showToast('Rule Cloned', 'Ready to save as a new Mangle rule copy', 'info');
|
|
|
}
|
|
|
|
|
|
-function resetCurrentMangleRuleCounters() {
|
|
|
+function resetMangleCounterDisplay() {
|
|
|
state.mangleStatRateHistory = new Array(30).fill(0);
|
|
|
state.mangleStatPktHistory = new Array(30).fill(0);
|
|
|
state.mangleLastBytes = 0;
|
|
|
@@ -5829,6 +5838,7 @@ function resetCurrentMangleRuleCounters() {
|
|
|
|
|
|
async function applyMangleRule(closeAfter = false) {
|
|
|
const id = document.getElementById('mangle-modal-id').value;
|
|
|
+ if (!reviewLegacyFirewallSettings('mangle', id)) return;
|
|
|
const chain = document.getElementById('mangle-modal-chain').value;
|
|
|
const action = document.getElementById('mangle-modal-action').value;
|
|
|
const comment = document.getElementById('mangle-modal-comment').value.trim();
|
|
|
@@ -5898,20 +5908,11 @@ async function applyMangleRule(closeAfter = false) {
|
|
|
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 || '',
|
|
|
@@ -5919,7 +5920,6 @@ async function applyMangleRule(closeAfter = false) {
|
|
|
random: getMangleFieldVal('random', false),
|
|
|
tcp_flags: getMangleFieldVal('tcpflags', false),
|
|
|
icmp_options: getMangleFieldVal('icmpoptions', false),
|
|
|
- ipv4_options: getMangleFieldVal('ipv4options', false),
|
|
|
ttl: getMangleFieldVal('ttl', false),
|
|
|
|
|
|
// Extra tab
|
|
|
@@ -5927,12 +5927,9 @@ async function applyMangleRule(closeAfter = 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() || '') : '',
|
|
|
@@ -5942,8 +5939,6 @@ async function applyMangleRule(closeAfter = false) {
|
|
|
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() || '') : '',
|
|
|
new_priority: action === 'set-priority' ? (document.getElementById('mangle-modal-newpriority')?.value.trim() || '') : '',
|
|
|
- route: action === 'route' ? (document.getElementById('mangle-modal-route')?.value.trim() || '') : '',
|
|
|
- sniff_target: (action === 'sniff-pc' || action === 'sniff-tzsp') ? (document.getElementById('mangle-modal-sniff')?.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 || '') : '',
|
|
|
@@ -6020,8 +6015,8 @@ async function pollMangleRuleStats() {
|
|
|
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));
|
|
|
+ const deltaBytes = Math.max(0, curBytes - (state.mangleLastBytes ?? curBytes));
|
|
|
+ const deltaPkts = Math.max(0, curPackets - (state.mangleLastPackets ?? curPackets));
|
|
|
|
|
|
state.mangleLastBytes = curBytes;
|
|
|
state.mangleLastPackets = curPackets;
|
|
|
@@ -6056,6 +6051,7 @@ async function pollMangleRuleStats() {
|
|
|
}
|
|
|
|
|
|
async function moveMangleRule(id, dir) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/mangle/${encodeURIComponent(id)}/move`, {
|
|
|
method: 'POST',
|
|
|
@@ -6069,6 +6065,7 @@ async function moveMangleRule(id, dir) {
|
|
|
}
|
|
|
|
|
|
async function toggleMangleRuleState(id, disabled) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/mangle/${encodeURIComponent(id)}/state`, {
|
|
|
method: 'POST',
|
|
|
@@ -6082,6 +6079,7 @@ async function toggleMangleRuleState(id, disabled) {
|
|
|
}
|
|
|
|
|
|
async function deleteMangleRule(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
if (!confirm('Are you sure you want to delete this mangle rule?')) return;
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/mangle/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
@@ -6103,7 +6101,7 @@ function renderAddressLists() {
|
|
|
tbody.innerHTML = '';
|
|
|
|
|
|
const entries = (state.addressLists || []).filter(e => !filterList || e.name === filterList);
|
|
|
- document.getElementById('addrlist-count-badge').innerText = `${entries.length} Entries`;
|
|
|
+ document.getElementById('addrlist-count-badge').innerText = `${entries.length} Entries${entries.filter(e => e.dynamic).length >= 2000 ? ' (dynamic results capped at 2000)' : ''}`;
|
|
|
|
|
|
// Update filter select
|
|
|
const sel = document.getElementById('addrlist-filter-name');
|
|
|
@@ -6126,19 +6124,21 @@ function renderAddressLists() {
|
|
|
}
|
|
|
empty?.classList.add('hidden');
|
|
|
|
|
|
- entries.forEach((e, idx) => {
|
|
|
+ entries.forEach((raw, idx) => {
|
|
|
+ const e = safeFirewallRow(raw);
|
|
|
const tr = document.createElement('tr');
|
|
|
tr.className = `hover:bg-slate-800/40 transition text-xs cursor-pointer ${e.disabled ? 'opacity-50' : ''}`;
|
|
|
- tr.setAttribute('ondblclick', `openEditAddrListModal('${e.id}')`);
|
|
|
+ if (!e.dynamic) tr.setAttribute('ondblclick', `openEditAddrListModal('${e.id}')`);
|
|
|
tr.innerHTML = `
|
|
|
<td class="py-2.5 px-3 text-center text-slate-500 font-bold">${idx + 1}</td>
|
|
|
<td class="py-2.5 px-3 text-center">${e.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '<span class="text-emerald-400 font-bold">✓</span>'}</td>
|
|
|
<td class="py-2.5 px-3 font-bold text-white font-mono">${e.name}</td>
|
|
|
<td class="py-2.5 px-3 font-mono font-bold text-emerald-400">${e.address}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[150px]">${e.comment || '-'}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[150px]">${e.dynamic ? 'Dynamic · '+(e.timeout ? e.timeout+'s remaining' : 'no expiry') : e.comment || '-'}</td>
|
|
|
<td class="py-2.5 px-3 text-right space-x-2 font-sans whitespace-nowrap">
|
|
|
- <button onclick="toggleAddrListState('${e.id}', ${!e.disabled})" class="text-xs ${e.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${e.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
+${e.dynamic ? '' : ` <button onclick="toggleAddrListState('${e.id}', ${!e.disabled})" class="text-xs ${e.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${e.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
<button onclick="openEditAddrListModal('${e.id}')" class="text-xs text-cyan-400 hover:text-cyan-300 font-medium">Edit</button>
|
|
|
+`}
|
|
|
<button onclick="deleteAddrList('${e.id}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
|
|
|
</td>
|
|
|
`;
|
|
|
@@ -6160,6 +6160,7 @@ function openAddAddrListModal() {
|
|
|
}
|
|
|
|
|
|
function openEditAddrListModal(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
const e = (state.addressLists || []).find(item => item.id === id);
|
|
|
if (!e) return;
|
|
|
|
|
|
@@ -6206,6 +6207,7 @@ async function submitSaveAddrList(e) {
|
|
|
}
|
|
|
|
|
|
async function toggleAddrListState(id, disabled) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/address-lists/${encodeURIComponent(id)}/state`, {
|
|
|
method: 'POST',
|
|
|
@@ -6221,6 +6223,7 @@ async function toggleAddrListState(id, disabled) {
|
|
|
}
|
|
|
|
|
|
async function deleteAddrList(id) {
|
|
|
+ id = decodeURIComponent(id);
|
|
|
if (!confirm('Are you sure you want to delete this address list item?')) return;
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/address-lists/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
@@ -6590,12 +6593,14 @@ async function loadConnections() {
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/connections`);
|
|
|
const data = await res.json();
|
|
|
+ if (!res.ok) throw new Error(data.message || 'Connection tracking unavailable');
|
|
|
state.connections = Array.isArray(data) ? data : [];
|
|
|
document.getElementById('fw-badge-connections').innerText = state.connections.length;
|
|
|
document.getElementById('conns-count-badge').innerText = `${state.connections.length} Flows`;
|
|
|
renderConnections();
|
|
|
} catch (err) {
|
|
|
console.error('Failed to load connections:', err);
|
|
|
+ const badge=document.getElementById('conns-count-badge'); if(badge) badge.textContent='Unavailable: '+err.message;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -6639,24 +6644,24 @@ function renderConnections() {
|
|
|
<td class="py-2.5 px-3 font-mono text-purple-300">${c.connection_mark || '-'}</td>
|
|
|
<td class="py-2.5 px-3 text-slate-500">${c.timeout ? c.timeout + 's' : '-'}</td>
|
|
|
<td class="py-2.5 px-3 text-right font-sans">
|
|
|
- <button onclick="killConnection('${c.protocol}', '${c.src_address}', ${c.src_port}, '${c.dst_address}', ${c.dst_port})" class="text-xs text-rose-400 hover:text-rose-300">Kill</button>
|
|
|
+${['TCP','UDP','SCTP','DCCP','ICMP','ICMPV6'].includes(c.protocol) ? ` <button onclick="killConnection('${c.protocol}', '${c.src_address}', ${c.src_port}, '${c.dst_address}', ${c.dst_port}, ${c.ip_version || 4}, ${c.zone || 0}, ${c.icmp_id ?? 'null'}, ${c.icmp_type ?? 'null'}, ${c.icmp_code ?? 'null'})" class="text-xs text-rose-400 hover:text-rose-300">Kill</button>` : ''}
|
|
|
</td>
|
|
|
`;
|
|
|
tbody.appendChild(tr);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-async function killConnection(protocol, src, sport, dst, dport) {
|
|
|
+async function killConnection(protocol, src, sport, dst, dport, ip_version, zone, icmp_id, icmp_type, icmp_code) {
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/connections/kill`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ protocol, src, sport, dst, dport })
|
|
|
+ body: JSON.stringify({ protocol, src, sport, dst, dport, ip_version, zone, icmp_id, icmp_type, icmp_code })
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
showToast('Terminated', `Killed flow ${src}:${sport} -> ${dst}:${dport}`, 'info');
|
|
|
loadConnections();
|
|
|
- }
|
|
|
+ } else { const error = await res.json(); showToast('Error', error.message || 'Unable to terminate this flow', 'error'); }
|
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
|
@@ -7383,8 +7388,8 @@ function showToast(title, message, type = 'info') {
|
|
|
toast.className = `pointer-events-auto border rounded-xl p-4 shadow-2xl backdrop-blur-md flex items-start space-x-3 w-80 transform transition-all duration-300 translate-y-2 opacity-0 ${borderColors[type] || borderColors.info}`;
|
|
|
toast.innerHTML = `
|
|
|
<div class="flex-1">
|
|
|
- <div class="font-bold text-xs text-white">${title}</div>
|
|
|
- <div class="text-[11px] text-slate-300 mt-0.5 leading-relaxed font-sans">${message}</div>
|
|
|
+ <div class="font-bold text-xs text-white">${escapeHtml(title)}</div>
|
|
|
+ <div class="text-[11px] text-slate-300 mt-0.5 leading-relaxed font-sans">${escapeHtml(message)}</div>
|
|
|
</div>
|
|
|
`;
|
|
|
container.appendChild(toast);
|
|
|
@@ -17293,3 +17298,63 @@ async function startUpdate(action) {
|
|
|
await loadUpdateStatus(false);
|
|
|
} catch (err) { showToast('Update Failed', err.message, 'error'); }
|
|
|
}
|
|
|
+
|
|
|
+async function resetCurrentRuleCounters() { return resetFirewallRuleCounter('filter', resetRuleCounterDisplay); }
|
|
|
+
|
|
|
+async function resetCurrentNATRuleCounters() { return resetFirewallRuleCounter('nat', resetNATCounterDisplay); }
|
|
|
+
|
|
|
+async function resetCurrentMangleRuleCounters() { return resetFirewallRuleCounter('mangle', resetMangleCounterDisplay); }
|
|
|
+
|
|
|
+function safeFirewallRow(row) {
|
|
|
+ const result = {...row};
|
|
|
+ for (const [key,value] of Object.entries(result)) if (typeof value === 'string') result[key] = escapeHtml(value);
|
|
|
+ result.id = encodeURIComponent(String(row.id || '')).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
|
+ return result;
|
|
|
+}
|
|
|
+function setFirewallDays(prefix, days) {
|
|
|
+ const selected = days ? days.toLowerCase().split(',') : ['sun','mon','tue','wed','thu','fri','sat'];
|
|
|
+ for (const d of ['sun','mon','tue','wed','thu','fri','sat']) { const el = document.getElementById(prefix+'-day-'+d); if (el) el.checked = selected.includes(d); }
|
|
|
+}
|
|
|
+function getFirewallDays(prefix) {
|
|
|
+ if (document.getElementById(prefix+'-field-days')?.classList.contains('hidden')) return '';
|
|
|
+ const selected = ['sun','mon','tue','wed','thu','fri','sat'].filter(d => document.getElementById(prefix+'-day-'+d)?.checked);
|
|
|
+ if (!selected.length) throw new Error('Select at least one weekday, or remove the Days condition.');
|
|
|
+ return selected.join(',');
|
|
|
+}
|
|
|
+async function resetFirewallRuleCounter(kind, resetDisplay) {
|
|
|
+ const id = document.getElementById(kind+'-modal-id')?.value;
|
|
|
+ if (!id) { resetDisplay(); return; }
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/firewall/${kind}/${encodeURIComponent(id)}/reset-counters`, {method:'POST'});
|
|
|
+ if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Counter reset failed'); }
|
|
|
+ resetDisplay(); await loadFirewall(false);
|
|
|
+ } catch(error) { showToast('Error', error.message, 'error'); }
|
|
|
+}
|
|
|
+
|
|
|
+function reviewLegacyFirewallSettings(kind, id) {
|
|
|
+ if (!id) return true;
|
|
|
+ const rows = kind === 'filter' ? state.filterRules : kind === 'nat' ? state.natRules : state.mangleRules;
|
|
|
+ const rule = (rows || []).find(r => r.id === id);
|
|
|
+ if (!rule) return true;
|
|
|
+ const unsupported = ['layer7_protocol','connection_bytes','connection_rate','pcc','in_bridge_port_list','out_bridge_port_list','tls_host','ingress_priority','priority','ipv4_options','dst_limit','psd','hotspot','route','sniff_target'];
|
|
|
+ const present = unsupported.filter(key => rule[key]);
|
|
|
+ if (!present.length) return true;
|
|
|
+ return confirm('This saved rule contains unsupported settings: '+present.join(', ')+'. Saving removes them. Review the remaining match conditions before continuing.');
|
|
|
+}
|
|
|
+
|
|
|
+function refreshFirewallRejectTypes() {
|
|
|
+ const select=document.getElementById('filter-modal-rejectwith'); if(!select) return;
|
|
|
+ const old=select.value;
|
|
|
+ const ipv6=document.getElementById('filter-modal-family')?.value==='6';
|
|
|
+ const types=ipv6 ? ['icmp6-port-unreachable','icmp6-no-route','icmp6-adm-prohibited','icmp6-addr-unreachable','tcp-reset'] : ['icmp-port-unreachable','icmp-net-unreachable','icmp-host-unreachable','icmp-proto-unreachable','icmp-net-prohibited','icmp-host-prohibited','icmp-admin-prohibited','tcp-reset'];
|
|
|
+ select.replaceChildren(...types.map(value=>{const option=document.createElement('option');option.value=value;option.textContent=value;return option}));
|
|
|
+ if(types.includes(old))select.value=old;
|
|
|
+}
|
|
|
+
|
|
|
+function setFirewallActionSelection(kind, value) {
|
|
|
+ const select=document.getElementById(kind+'-modal-action'); if(!select)return;
|
|
|
+ if(!Array.from(select.options).some(option=>option.value===value)) {
|
|
|
+ const option=document.createElement('option');option.value=value;option.textContent='Unsupported saved action: '+value;option.disabled=true;select.appendChild(option);
|
|
|
+ }
|
|
|
+ select.value=value;
|
|
|
+}
|