Jelajahi Sumber

feat(firewall): dynamic action parameters across Filter, NAT, and Mangle rules

Proxmox Router 1 Minggu lalu
induk
melakukan
3f3e0a86de

+ 82 - 1
internal/firewall/iptables.go

@@ -93,6 +93,13 @@ func BuildIPTablesRestore(filterRules []FilterRule, natRules []NATRule, mangleRu
 				userMangleChains[ch] = true
 			}
 		}
+		if r.Action == "jump" && r.JumpTarget != "" {
+			jt := strings.ToUpper(r.JumpTarget)
+			if jt != "ACCEPT" && jt != "DROP" && jt != "RETURN" && !userMangleChains[jt] {
+				sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
+				userMangleChains[jt] = true
+			}
+		}
 	}
 
 	for _, r := range mangleRules {
@@ -122,6 +129,13 @@ func BuildIPTablesRestore(filterRules []FilterRule, natRules []NATRule, mangleRu
 				userNATChains[ch] = true
 			}
 		}
+		if r.Action == "jump" && r.JumpTarget != "" {
+			jt := strings.ToUpper(r.JumpTarget)
+			if jt != "ACCEPT" && jt != "RETURN" && !userNATChains[jt] {
+				sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
+				userNATChains[jt] = true
+			}
+		}
 	}
 
 	for _, r := range natRules {
@@ -150,6 +164,13 @@ func BuildIPTablesRestore(filterRules []FilterRule, natRules []NATRule, mangleRu
 				userFilterChains[ch] = true
 			}
 		}
+		if r.Action == "jump" && r.JumpTarget != "" {
+			jt := strings.ToUpper(r.JumpTarget)
+			if jt != "ACCEPT" && jt != "DROP" && jt != "REJECT" && jt != "RETURN" && !userFilterChains[jt] {
+				sb.WriteString(fmt.Sprintf(":%s - [0:0]\n", jt))
+				userFilterChains[jt] = true
+			}
+		}
 	}
 
 	for _, r := range filterRules {
@@ -204,6 +225,14 @@ func buildFilterRuleLine(r FilterRule) string {
 		if r.LogPrefix != "" {
 			args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
 		}
+	case "add-src-to-address-list":
+		if r.AddressList != "" {
+			args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetIPSetName(r.AddressList)))
+		}
+	case "add-dst-to-address-list":
+		if r.AddressList != "" {
+			args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetIPSetName(r.AddressList)))
+		}
 	case "passthrough":
 		// in iptables, passthrough continues evaluation
 		return ""
@@ -256,6 +285,23 @@ func buildNATRuleLine(r NATRule) string {
 		} else {
 			args = append(args, "-j REDIRECT")
 		}
+	case "jump":
+		if r.JumpTarget != "" {
+			args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
+		}
+	case "log":
+		args = append(args, "-j LOG")
+		if r.LogPrefix != "" {
+			args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
+		}
+	case "add-src-to-address-list":
+		if r.AddressList != "" {
+			args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetIPSetName(r.AddressList)))
+		}
+	case "add-dst-to-address-list":
+		if r.AddressList != "" {
+			args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetIPSetName(r.AddressList)))
+		}
 	case "accept":
 		args = append(args, "-j ACCEPT")
 	case "return":
@@ -303,14 +349,49 @@ func buildMangleRuleLine(r MangleRule) string {
 		} else {
 			args = append(args, fmt.Sprintf("--set-mss %s", r.NewMSS))
 		}
-	case "set-dscp":
+	case "set-dscp", "change-dscp":
 		if r.NewDSCP > 0 {
 			args = append(args, fmt.Sprintf("-j DSCP --set-dscp %d", r.NewDSCP))
 		}
+	case "change-ttl":
+		if r.NewTTL != "" {
+			if strings.HasPrefix(r.NewTTL, "+") {
+				args = append(args, fmt.Sprintf("-j TTL --ttl-inc %s", strings.TrimPrefix(r.NewTTL, "+")))
+			} else if strings.HasPrefix(r.NewTTL, "-") {
+				args = append(args, fmt.Sprintf("-j TTL --ttl-dec %s", strings.TrimPrefix(r.NewTTL, "-")))
+			} else {
+				args = append(args, fmt.Sprintf("-j TTL --ttl-set %s", r.NewTTL))
+			}
+		}
+	case "set-priority":
+		if r.NewPriority != "" && chain == "POSTROUTING" {
+			args = append(args, fmt.Sprintf("-j CLASSIFY --set-class 0:%s", r.NewPriority))
+		}
+	case "drop":
+		args = append(args, "-j DROP")
+	case "jump":
+		if r.JumpTarget != "" {
+			args = append(args, fmt.Sprintf("-j %s", strings.ToUpper(r.JumpTarget)))
+		}
+	case "log":
+		args = append(args, "-j LOG")
+		if r.LogPrefix != "" {
+			args = append(args, fmt.Sprintf("--log-prefix \"%s: \"", r.LogPrefix))
+		}
+	case "add-src-to-address-list":
+		if r.AddressList != "" {
+			args = append(args, fmt.Sprintf("-j SET --add-set %s src", GetIPSetName(r.AddressList)))
+		}
+	case "add-dst-to-address-list":
+		if r.AddressList != "" {
+			args = append(args, fmt.Sprintf("-j SET --add-set %s dst", GetIPSetName(r.AddressList)))
+		}
 	case "accept":
 		args = append(args, "-j ACCEPT")
 	case "return":
 		args = append(args, "-j RETURN")
+	case "clear-df", "strip-ipv4-options", "route", "sniff-pc", "sniff-tzsp", "passthrough":
+		return ""
 	default:
 		return ""
 	}

+ 3 - 0
internal/firewall/manager.go

@@ -145,18 +145,21 @@ func (m *FirewallManager) Apply() error {
 		if !r.Disabled {
 			if r.SrcAddressList != "" { refLists = append(refLists, r.SrcAddressList) }
 			if r.DstAddressList != "" { refLists = append(refLists, r.DstAddressList) }
+			if r.AddressList != "" { refLists = append(refLists, r.AddressList) }
 		}
 	}
 	for _, r := range m.natRules {
 		if !r.Disabled {
 			if r.SrcAddressList != "" { refLists = append(refLists, r.SrcAddressList) }
 			if r.DstAddressList != "" { refLists = append(refLists, r.DstAddressList) }
+			if r.AddressList != "" { refLists = append(refLists, r.AddressList) }
 		}
 	}
 	for _, r := range m.mangleRules {
 		if !r.Disabled {
 			if r.SrcAddressList != "" { refLists = append(refLists, r.SrcAddressList) }
 			if r.DstAddressList != "" { refLists = append(refLists, r.DstAddressList) }
+			if r.AddressList != "" { refLists = append(refLists, r.AddressList) }
 		}
 	}
 

+ 4 - 0
internal/firewall/types.go

@@ -158,6 +158,10 @@ type MangleRule struct {
 	NewPacketMark      string   `json:"new_packet_mark,omitempty"`
 	NewMSS             string   `json:"new_mss,omitempty"` // e.g. "1440" or "clamp-to-pmtu"
 	NewDSCP            int      `json:"new_dscp,omitempty"`
+	NewTTL             string   `json:"new_ttl,omitempty"`
+	NewPriority        string   `json:"new_priority,omitempty"`
+	Route              string   `json:"route,omitempty"`
+	SniffTarget        string   `json:"sniff_target,omitempty"`
 	Passthrough        bool     `json:"passthrough"` // default true in MikroTik
 	SrcAddress         string   `json:"src_address,omitempty"`
 	DstAddress         string   `json:"dst_address,omitempty"`

+ 145 - 25
internal/web/static/app.js

@@ -2687,6 +2687,17 @@ function renderFilterRules() {
     const ports = [r.src_port ? 's:' + r.src_port : '', r.dst_port ? 'd:' + r.dst_port : ''].filter(Boolean).join(' ');
     const ifaces = [r.in_interface ? 'in:' + r.in_interface : '', r.out_interface ? 'out:' + r.out_interface : ''].filter(Boolean).join(' ');
 
+    let actionExtra = '';
+    if (r.action === 'reject') {
+      actionExtra = `<span class="block text-[10px] text-rose-300 font-mono mt-0.5">(${r.reject_with || 'icmp-net-unreach'})</span>`;
+    } else if (r.action === 'jump') {
+      actionExtra = `<span class="block text-[10px] text-yellow-300 font-mono mt-0.5">&rarr; ${r.jump_target || '-'}</span>`;
+    } else if (r.action.includes('address-list') || r.action.includes('address list')) {
+      actionExtra = `<span class="block text-[10px] text-emerald-300 font-mono mt-0.5">&rarr; ${r.address_list || '-'}</span>`;
+    } else if (r.action === 'log' && r.log_prefix) {
+      actionExtra = `<span class="block text-[10px] text-slate-400 font-mono mt-0.5">${r.log_prefix}</span>`;
+    }
+
     tr.innerHTML = `
       <td class="py-2.5 px-3 text-center text-slate-500 font-bold">${r.order || idx + 1}</td>
       <td class="py-2.5 px-3 text-center">${renderRuleFlag(r.disabled, !!r.comment)}</td>
@@ -2696,7 +2707,7 @@ function renderFilterRules() {
       <td class="py-2.5 px-3 text-cyan-400 uppercase">${r.protocol || 'all'}</td>
       <td class="py-2.5 px-3 text-amber-400">${ports || '-'}</td>
       <td class="py-2.5 px-3 text-emerald-400">${ifaces || '-'}</td>
-      <td class="py-2.5 px-3">${renderActionBadge(r.action)}</td>
+      <td class="py-2.5 px-3">${renderActionBadge(r.action)}${actionExtra}</td>
       <td class="py-2.5 px-3 text-[11px] text-slate-400">
         <span class="text-slate-300">${(r.packets || 0).toLocaleString()}p</span> / 
         <span class="text-slate-500">${formatTraffic(r.bytes || 0).replace('/s', '')}</span>
@@ -2767,6 +2778,19 @@ function populateAddressListSelect(selectId, currentVal = '') {
     opt.selected = true;
     sel.appendChild(opt);
   }
+  populateCommonAddrListDatalist();
+}
+
+function populateCommonAddrListDatalist() {
+  const dl = document.getElementById('fw-addrlist-datalist');
+  if (!dl) return;
+  dl.innerHTML = '';
+  const names = Array.from(new Set((state.addressLists || []).map(a => a.name).filter(Boolean))).sort();
+  names.forEach(name => {
+    const opt = document.createElement('option');
+    opt.value = name;
+    dl.appendChild(opt);
+  });
 }
 
 function populateGeoIPSelect(selectId, currentVal = '') {
@@ -3116,9 +3140,22 @@ function toggleFilterActionFields() {
     else jumpBox.classList.add('hidden');
   }
   if (addrBox) {
-    if (act === 'add-dst-to-address-list' || act === 'add-src-to-address-list') addrBox.classList.remove('hidden');
-    else addrBox.classList.add('hidden');
+    if (act === 'add-dst-to-address-list' || act === 'add-src-to-address-list' || act.includes('address-list') || act.includes('address list')) {
+      addrBox.classList.remove('hidden');
+    } else {
+      addrBox.classList.add('hidden');
+    }
   }
+  if (act === 'log') {
+    const logEl = document.getElementById('filter-modal-log');
+    if (logEl) logEl.checked = true;
+    toggleFwField('logprefix', true);
+  }
+}
+
+function toggleFwLogPrefix() {
+  const isLog = document.getElementById('filter-modal-log')?.checked;
+  toggleFwField('logprefix', !!isLog);
 }
 
 function copyFilterRule() {
@@ -3481,7 +3518,22 @@ function renderNATRules() {
   rules.forEach((r, idx) => {
     const tr = document.createElement('tr');
     tr.className = `hover:bg-slate-800/40 transition text-xs ${r.disabled ? 'opacity-50' : ''}`;
-    const toTarget = [r.to_addresses, r.to_ports].filter(Boolean).join(':');
+    
+    let toTarget = '-';
+    if (r.action === 'jump') {
+      toTarget = `<span class="text-yellow-300 font-mono font-bold">&rarr; ${r.jump_target || '-'}</span>`;
+    } else if (r.action.includes('address-list') || r.action.includes('address list')) {
+      toTarget = `<span class="text-emerald-300 font-mono font-bold">&rarr; ${r.address_list || '-'}</span>`;
+    } else if (r.action === 'redirect') {
+      toTarget = `<span class="text-amber-300 font-mono font-bold">ports:${r.to_ports || 'all'}</span>`;
+    } else if (r.action === 'masquerade') {
+      toTarget = r.to_ports ? `<span class="text-amber-300 font-mono font-bold">ports:${r.to_ports}</span>` : `<span class="text-slate-500 font-mono">-</span>`;
+    } else if (r.action === 'log') {
+      toTarget = `<span class="text-slate-400 font-mono font-bold">${r.log_prefix || 'log'}</span>`;
+    } else {
+      const parts = [r.to_addresses, r.to_ports].filter(Boolean);
+      toTarget = parts.length ? `<span class="text-amber-300 font-mono font-bold">${parts.join(':')}</span>` : '-';
+    }
     const ifaces = [r.out_interface ? 'out:' + r.out_interface : '', r.in_interface ? 'in:' + r.in_interface : ''].filter(Boolean).join(' ');
 
     tr.innerHTML = `
@@ -3493,7 +3545,7 @@ function renderNATRules() {
       <td class="py-2.5 px-3 text-cyan-400 uppercase">${r.protocol || 'all'}${r.dst_port ? ':' + r.dst_port : ''}</td>
       <td class="py-2.5 px-3 text-emerald-400">${ifaces || '-'}</td>
       <td class="py-2.5 px-3">${renderActionBadge(r.action)}</td>
-      <td class="py-2.5 px-3 font-mono font-bold text-amber-300">${toTarget || '-'}</td>
+      <td class="py-2.5 px-3 font-mono text-xs">${toTarget}</td>
       <td class="py-2.5 px-3 text-[11px] text-slate-400">
         <span class="text-slate-300">${(r.packets || 0).toLocaleString()}p</span> / 
         <span class="text-slate-500">${formatTraffic(r.bytes || 0).replace('/s', '')}</span>
@@ -3627,25 +3679,40 @@ function handleNATChainChange() {
 
 function toggleNATActionFields() {
   const act = document.getElementById('nat-modal-action')?.value || 'masquerade';
-  const toBox = document.getElementById('nat-action-to-box');
+  const rowAddr = document.getElementById('nat-row-toaddr');
+  const rowPort = document.getElementById('nat-row-toport');
   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');
+  // Hide all dynamic rows first
+  if (rowAddr) rowAddr.classList.add('hidden');
+  if (rowPort) rowPort.classList.add('hidden');
+  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 (rowAddr) rowAddr.classList.remove('hidden');
+    if (rowPort) rowPort.classList.remove('hidden');
+  } else if (act === 'redirect' || act === 'masquerade') {
+    if (rowPort) rowPort.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');
   }
-  if (addrBox) {
-    if (act.includes('address-list') || act.includes('address list')) addrBox.classList.remove('hidden');
-    else addrBox.classList.add('hidden');
+
+  if (act === 'log') {
+    const logEl = document.getElementById('nat-modal-log');
+    if (logEl) logEl.checked = true;
+    toggleNATField('logprefix', true);
   }
 }
 
+function toggleNATLogPrefix() {
+  const isLog = document.getElementById('nat-modal-log')?.checked;
+  toggleNATField('logprefix', !!isLog);
+}
+
 function openAddNATModal() {
   switchNATRuleTab('general');
   const badge = document.getElementById('nat-modal-badge');
@@ -3853,12 +3920,15 @@ 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 payload = {
     id,
     chain,
     action,
-    to_addresses: document.getElementById('nat-modal-toaddr')?.value.trim() || '',
-    to_ports: document.getElementById('nat-modal-toport')?.value.trim() || '',
+    to_addresses: usesAddr ? (document.getElementById('nat-modal-toaddr')?.value.trim() || '') : '',
+    to_ports: usesPort ? (document.getElementById('nat-modal-toport')?.value.trim() || '') : '',
     protocol: proto || 'all',
     src_address: src,
     dst_address: dst,
@@ -4077,7 +4147,19 @@ function renderMangleRules() {
   rules.forEach((r, idx) => {
     const tr = document.createElement('tr');
     tr.className = `hover:bg-slate-800/40 transition text-xs ${r.disabled ? 'opacity-50' : ''}`;
-    const markVal = r.new_connection_mark || r.new_routing_mark || r.new_packet_mark || r.new_mss || (r.new_dscp ? 'DSCP:' + r.new_dscp : '');
+    let markVal = '<span class="text-slate-500">-</span>';
+    if (r.new_connection_mark) markVal = `<span class="text-cyan-300 font-mono font-bold">${r.new_connection_mark}</span>`;
+    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.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>`;
+    else if (r.sniff_target) markVal = `<span class="text-pink-300 font-mono font-bold">sniff:${r.sniff_target}</span>`;
+    else if (r.action === 'jump') markVal = `<span class="text-yellow-300 font-mono font-bold">&rarr; ${r.jump_target || '-'}</span>`;
+    else if (r.action.includes('address-list') || r.action.includes('address list')) markVal = `<span class="text-emerald-300 font-mono font-bold">&rarr; ${r.address_list || '-'}</span>`;
+    else if (r.action === 'log') markVal = `<span class="text-slate-400 font-mono font-bold">${r.log_prefix || 'log'}</span>`;
 
     tr.innerHTML = `
       <td class="py-2.5 px-3 text-center text-slate-500 font-bold">${r.order || idx + 1}</td>
@@ -4086,7 +4168,7 @@ function renderMangleRules() {
       <td class="py-2.5 px-3 text-slate-300">${formatTarget(r.src_address, r.src_address_list, r.src_geoip)}</td>
       <td class="py-2.5 px-3 text-slate-300">${formatTarget(r.dst_address, r.dst_address_list, r.dst_geoip)}</td>
       <td class="py-2.5 px-3">${renderActionBadge(r.action)}</td>
-      <td class="py-2.5 px-3 font-mono font-bold text-cyan-300">${markVal || '-'}</td>
+      <td class="py-2.5 px-3 font-mono text-xs">${markVal}</td>
       <td class="py-2.5 px-3 text-center">${r.passthrough ? '<span class="text-emerald-400">yes</span>' : '<span class="text-slate-500">no</span>'}</td>
       <td class="py-2.5 px-3 text-[11px] text-slate-400">
         <span class="text-slate-300">${(r.packets || 0).toLocaleString()}p</span> / 
@@ -4213,10 +4295,10 @@ function toggleMaximizeMangleModal() {
 }
 
 function toggleMangleActionFields() {
-  const act = document.getElementById('mangle-modal-action')?.value || 'accept';
+  const act = document.getElementById('mangle-modal-action')?.value || 'mark-connection';
   
   // Hide all dynamic rows first
-  ['newconnmark', 'newroutingmark', 'newpktmark', 'newmss', 'newdscp', 'newttl', 'passthrough'].forEach(id => {
+  ['newconnmark', 'newroutingmark', 'newpktmark', 'newmss', 'newdscp', 'newttl', 'newpriority', 'route', 'sniff', 'passthrough'].forEach(id => {
     const el = document.getElementById(`mangle-row-${id}`);
     if (el) el.classList.add('hidden');
   });
@@ -4244,11 +4326,33 @@ function toggleMangleActionFields() {
   } else if (act === 'change-ttl') {
     document.getElementById('mangle-row-newttl')?.classList.remove('hidden');
     document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
+  } else if (act === 'set-priority') {
+    document.getElementById('mangle-row-newpriority')?.classList.remove('hidden');
+    document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
+  } else if (act === 'route') {
+    document.getElementById('mangle-row-route')?.classList.remove('hidden');
+    document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
+  } else if (act === 'sniff-pc' || act === 'sniff-tzsp') {
+    document.getElementById('mangle-row-sniff')?.classList.remove('hidden');
+    document.getElementById('mangle-row-passthrough')?.classList.remove('hidden');
+  } else if (act === 'clear-df' || act === 'strip-ipv4-options') {
+    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');
   }
+
+  if (act === 'log') {
+    const logEl = document.getElementById('mangle-modal-log');
+    if (logEl) logEl.checked = true;
+    toggleMangleField('logprefix', true);
+  }
+}
+
+function toggleMangleLogPrefix() {
+  const isLog = document.getElementById('mangle-modal-log')?.checked;
+  toggleMangleField('logprefix', !!isLog);
 }
 
 const allMangleFieldKeys = [
@@ -4271,7 +4375,7 @@ function openAddMangleModal() {
   document.getElementById('mangle-modal-enabled').checked = true;
   document.getElementById('mangle-modal-comment').value = '';
   document.getElementById('mangle-modal-chain').value = 'prerouting';
-  document.getElementById('mangle-modal-action').value = 'accept';
+  document.getElementById('mangle-modal-action').value = 'mark-connection';
   
   if (document.getElementById('mangle-modal-newconnmark')) document.getElementById('mangle-modal-newconnmark').value = '';
   if (document.getElementById('mangle-modal-newroutingmark')) document.getElementById('mangle-modal-newroutingmark').value = '';
@@ -4279,6 +4383,12 @@ function openAddMangleModal() {
   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-newpriority')) document.getElementById('mangle-modal-newpriority').value = '';
+  if (document.getElementById('mangle-modal-route')) document.getElementById('mangle-modal-route').value = '';
+  if (document.getElementById('mangle-modal-sniff')) document.getElementById('mangle-modal-sniff').value = '';
+  if (document.getElementById('mangle-modal-jumptarget')) document.getElementById('mangle-modal-jumptarget').value = '';
+  if (document.getElementById('mangle-modal-action-addrlist')) document.getElementById('mangle-modal-action-addrlist').value = '';
+  if (document.getElementById('mangle-modal-action-timeout')) document.getElementById('mangle-modal-action-timeout').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;
 
@@ -4307,7 +4417,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 || 'accept';
+  document.getElementById('mangle-modal-action').value = r.action || 'mark-connection';
   
   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 || '';
@@ -4315,6 +4425,12 @@ function openEditMangleModal(id) {
   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-newpriority')) document.getElementById('mangle-modal-newpriority').value = r.new_priority || '';
+  if (document.getElementById('mangle-modal-route')) document.getElementById('mangle-modal-route').value = r.route || '';
+  if (document.getElementById('mangle-modal-sniff')) document.getElementById('mangle-modal-sniff').value = r.sniff_target || '';
+  if (document.getElementById('mangle-modal-jumptarget')) document.getElementById('mangle-modal-jumptarget').value = r.jump_target || '';
+  if (document.getElementById('mangle-modal-action-addrlist')) document.getElementById('mangle-modal-action-addrlist').value = r.address_list || '';
+  if (document.getElementById('mangle-modal-action-timeout')) document.getElementById('mangle-modal-action-timeout').value = r.address_list_timeout || '';
   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;
 
@@ -4580,6 +4696,9 @@ async function applyMangleRule(closeAfter = false) {
     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() || '') : '',
+    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 || '') : '',
@@ -4780,6 +4899,7 @@ function renderAddressLists() {
     `;
     tbody.appendChild(tr);
   });
+  populateCommonAddrListDatalist();
 }
 
 function openAddAddrListModal() {

+ 44 - 19
internal/web/static/index.html

@@ -4169,7 +4169,7 @@
               <div class="flex items-center text-xs py-1">
                 <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Address List</label>
                 <div class="flex-1">
-                  <input type="text" id="filter-modal-action-addrlist" placeholder="list name" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
+                  <input type="text" id="filter-modal-action-addrlist" list="fw-addrlist-datalist" placeholder="list name" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
                 </div>
               </div>
               <div class="flex items-center text-xs py-1">
@@ -4186,7 +4186,7 @@
             <div class="flex items-center text-xs py-1">
               <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Log</label>
               <div class="flex items-center">
-                <input type="checkbox" id="filter-modal-log" class="rounded bg-[#15202e] border-[#26374d] text-sky-500 focus:ring-0 w-4 h-4 cursor-pointer">
+                <input type="checkbox" id="filter-modal-log" onchange="toggleFwLogPrefix()" class="rounded bg-[#15202e] border-[#26374d] text-sky-500 focus:ring-0 w-4 h-4 cursor-pointer">
               </div>
             </div>
 
@@ -5036,19 +5036,19 @@
               </div>
             </div>
 
-            <!-- Target Fields (Shown for src-nat, dst-nat, redirect, netmap, same) -->
-            <div id="nat-action-to-box" class="space-y-1">
-              <div class="flex items-center text-xs py-1">
-                <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">To Addresses</label>
-                <div class="flex-1">
-                  <input type="text" id="nat-modal-toaddr" placeholder="192.168.10.50 or WAN IP" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
-                </div>
+            <!-- To Addresses (Shown for src-nat, dst-nat, netmap, same, socksify, endpoint-independent-nat) -->
+            <div id="nat-row-toaddr" class="hidden flex items-center text-xs py-1">
+              <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">To Addresses</label>
+              <div class="flex-1">
+                <input type="text" id="nat-modal-toaddr" placeholder="192.168.10.50 or WAN IP" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
               </div>
-              <div class="flex items-center text-xs py-1">
-                <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">To Ports</label>
-                <div class="flex-1">
-                  <input type="text" id="nat-modal-toport" placeholder="80 or 8080" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
-                </div>
+            </div>
+
+            <!-- To Ports (Shown for src-nat, dst-nat, redirect, masquerade, netmap, socksify, endpoint-independent-nat) -->
+            <div id="nat-row-toport" class="hidden flex items-center text-xs py-1">
+              <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">To Ports</label>
+              <div class="flex-1">
+                <input type="text" id="nat-modal-toport" placeholder="80 or 8080 or 1024-65535" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
               </div>
             </div>
 
@@ -5065,7 +5065,7 @@
               <div class="flex items-center text-xs py-1">
                 <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Address List</label>
                 <div class="flex-1">
-                  <input type="text" id="nat-modal-action-addrlist" placeholder="list name" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
+                  <input type="text" id="nat-modal-action-addrlist" list="fw-addrlist-datalist" placeholder="list name" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
                 </div>
               </div>
               <div class="flex items-center text-xs py-1">
@@ -5082,7 +5082,7 @@
             <div class="flex items-center text-xs py-1">
               <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Log</label>
               <div class="flex items-center">
-                <input type="checkbox" id="nat-modal-log" class="rounded bg-[#15202e] border-[#26374d] text-sky-500 focus:ring-0 w-4 h-4 cursor-pointer">
+                <input type="checkbox" id="nat-modal-log" onchange="toggleNATLogPrefix()" class="rounded bg-[#15202e] border-[#26374d] text-sky-500 focus:ring-0 w-4 h-4 cursor-pointer">
               </div>
             </div>
 
@@ -6114,6 +6114,30 @@
               </div>
             </div>
 
+            <!-- New Priority (when set-priority) -->
+            <div id="mangle-row-newpriority" class="hidden flex items-center text-xs py-1">
+              <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">New Priority</label>
+              <div class="flex-1">
+                <input type="number" id="mangle-modal-newpriority" min="0" max="7" placeholder="0-7" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
+              </div>
+            </div>
+
+            <!-- Route (when route) -->
+            <div id="mangle-row-route" class="hidden flex items-center text-xs py-1">
+              <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Route</label>
+              <div class="flex-1">
+                <input type="text" id="mangle-modal-route" placeholder="e.g. 192.168.1.1 or routing table" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
+              </div>
+            </div>
+
+            <!-- Sniff Target (when sniff-pc or sniff-tzsp) -->
+            <div id="mangle-row-sniff" class="hidden flex items-center text-xs py-1">
+              <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Sniff Target</label>
+              <div class="flex-1">
+                <input type="text" id="mangle-modal-sniff" placeholder="IP / Host / Port" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
+              </div>
+            </div>
+
             <!-- Passthrough Checkbox (visible for marks / changes) -->
             <div id="mangle-row-passthrough" class="hidden flex items-center text-xs py-1">
               <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Passthrough</label>
@@ -6135,7 +6159,7 @@
               <div class="flex items-center text-xs py-1">
                 <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Address List</label>
                 <div class="flex-1">
-                  <input type="text" id="mangle-modal-action-addrlist" placeholder="list name" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
+                  <input type="text" id="mangle-modal-action-addrlist" list="fw-addrlist-datalist" placeholder="list name" class="bg-[#15202e] border border-[#26374d] rounded px-2.5 py-1 text-xs text-white font-mono focus:outline-none focus:border-sky-500 w-full">
                 </div>
               </div>
               <div class="flex items-center text-xs py-1">
@@ -6152,7 +6176,7 @@
             <div class="flex items-center text-xs py-1">
               <label class="w-36 sm:w-44 text-right pr-3.5 text-slate-300 shrink-0 select-none">Log</label>
               <div class="flex items-center">
-                <input type="checkbox" id="mangle-modal-log" class="rounded bg-[#15202e] border-[#26374d] text-sky-500 focus:ring-0 w-4 h-4 cursor-pointer">
+                <input type="checkbox" id="mangle-modal-log" onchange="toggleMangleLogPrefix()" class="rounded bg-[#15202e] border-[#26374d] text-sky-500 focus:ring-0 w-4 h-4 cursor-pointer">
               </div>
             </div>
 
@@ -7606,6 +7630,7 @@
     </div>
   </div>
 
-  <script src="app.js?v=5.2"></script>
+  <datalist id="fw-addrlist-datalist"></datalist>
+  <script src="app.js?v=5.3"></script>
 </body>
 </html>