فهرست منبع

feat(firewall): replicate RouterOS WebFig WinBox v4 Mangle Rule dialog with 5 tabs, interactive matchers, and statistics graphs

Proxmox Router 1 هفته پیش
والد
کامیت
3f62741091
4فایلهای تغییر یافته به همراه1626 افزوده شده و 209 حذف شده
  1. 3 0
      internal/api/firewall_handlers.go
  2. 52 0
      internal/firewall/types.go
  3. 530 93
      internal/web/static/app.js
  4. 1041 116
      internal/web/static/index.html

+ 3 - 0
internal/api/firewall_handlers.go

@@ -149,6 +149,9 @@ func (s *Server) handleSaveMangleRule(w http.ResponseWriter, r *http.Request) {
 		writeError(w, http.StatusBadRequest, "invalid request body")
 		return
 	}
+	if rule.ID == "" {
+		rule.ID = fmt.Sprintf("mangle-%d", time.Now().UnixNano())
+	}
 	if err := s.fwMgr.SaveMangleRule(rule); err != nil {
 		writeError(w, http.StatusInternalServerError, err.Error())
 		return

+ 52 - 0
internal/firewall/types.go

@@ -177,6 +177,58 @@ type MangleRule struct {
 	Disabled           bool     `json:"disabled"`
 	Packets            uint64   `json:"packets"`
 	Bytes              uint64   `json:"bytes"`
+
+	// Extended WebFig / WinBox v4 properties
+	AnyPort            string   `json:"any_port,omitempty"`
+	InInterfaceList    string   `json:"in_interface_list,omitempty"`
+	OutInterfaceList   string   `json:"out_interface_list,omitempty"`
+	PacketMark         string   `json:"packet_mark,omitempty"`
+	ConnectionType     string   `json:"connection_type,omitempty"`
+	ConnectionNATState []string `json:"connection_nat_state,omitempty"`
+
+	// Advanced tab
+	Layer7Protocol    string `json:"layer7_protocol,omitempty"`
+	Content           string `json:"content,omitempty"`
+	ConnectionBytes   string `json:"connection_bytes,omitempty"`
+	ConnectionRate    string `json:"connection_rate,omitempty"`
+	PCC               string `json:"pcc,omitempty"`
+	SrcMACAddress     string `json:"src_mac_address,omitempty"`
+	OutBridgePort     string `json:"out_bridge_port,omitempty"`
+	InBridgePort      string `json:"in_bridge_port,omitempty"`
+	InBridgePortList  string `json:"in_bridge_port_list,omitempty"`
+	OutBridgePortList string `json:"out_bridge_port_list,omitempty"`
+	IPsecPolicy       string `json:"ipsec_policy,omitempty"`
+	TLSHost           string `json:"tls_host,omitempty"`
+	IngressPriority   string `json:"ingress_priority,omitempty"`
+	Priority          string `json:"priority,omitempty"`
+	DSCP              string `json:"dscp,omitempty"`
+	TCPMSS            string `json:"tcp_mss,omitempty"`
+	TOS               string `json:"tos,omitempty"`
+	PacketSize        string `json:"packet_size,omitempty"`
+	Random            string `json:"random,omitempty"`
+	TCPFlags          string `json:"tcp_flags,omitempty"`
+	ICMPOptions       string `json:"icmp_options,omitempty"`
+	IPv4Options       string `json:"ipv4_options,omitempty"`
+	TTL               string `json:"ttl,omitempty"`
+
+	// Extra tab
+	Nth             string `json:"nth,omitempty"`
+	ConnectionLimit string `json:"connection_limit,omitempty"`
+	SrcAddressType  string `json:"src_address_type,omitempty"`
+	DstAddressType  string `json:"dst_address_type,omitempty"`
+	Hotspot         string `json:"hotspot,omitempty"`
+	IPFragment      string `json:"ip_fragment,omitempty"`
+	Limit           string `json:"limit,omitempty"`
+	DstLimit        string `json:"dst_limit,omitempty"`
+	Time            string `json:"time,omitempty"`
+	PSD             string `json:"psd,omitempty"`
+
+	// Action tab extended
+	Log                bool   `json:"log"`
+	LogPrefix          string `json:"log_prefix,omitempty"`
+	JumpTarget         string `json:"jump_target,omitempty"`
+	AddressList        string `json:"address_list,omitempty"`
+	AddressListTimeout string `json:"address_list_timeout,omitempty"`
 }
 
 // AddressListEntry represents a MikroTik /ip firewall address-list entry

+ 530 - 93
internal/web/static/app.js

@@ -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() {
-  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-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-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');
 }
@@ -4140,103 +4299,294 @@ function openEditMangleModal(id) {
   const r = (state.mangleRules || []).find(rule => rule.id === id);
   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-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-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');
 }
 
 function closeMangleModal() {
+  stopMangleStatPolling();
   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 chain = document.getElementById('mangle-modal-chain').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 = {
     id,
     chain,
     action,
-    passthrough,
+    protocol: proto || 'all',
     src_address: src,
     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,
     dst_address_list: dstlist,
-    src_geoip: srcgeoip,
-    dst_geoip: dstgeoip,
-    connection_mark: connmark,
     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 {
     const res = await fetch(`${API_BASE}/firewall/mangle`, {
@@ -4245,15 +4595,102 @@ async function submitSaveMangleRule(e) {
       body: JSON.stringify(payload)
     });
     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);
+      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) {
     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) {
   try {
     const res = await fetch(`${API_BASE}/firewall/mangle/${encodeURIComponent(id)}/move`, {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1041 - 116
internal/web/static/index.html


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است