// src/components/RuleBuilder.tsx // // Extended RuleBuilder using the user's canonical match list: // 1) Metadata & Connection Tracking (meta, ct) // 2) Layer 3 Network Headers (ip, ip6) // 3) Layer 4 Transport Headers (tcp, udp, icmp) — appear when chosen // 4) Layer 2 Ethernet & VLAN (ether, vlan) // // The UI provides rich dropdowns / placeholders / short explanations for every token subfield. // // NOTE: This file replaces the token lists and per-field UI to strictly follow the user's canonical list. import { CopyOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import { Button, Card, Checkbox, Col, Divider, Form, Input, InputNumber, message, Modal, Radio, Row, Select, Space, Tooltip, Typography, } from 'antd'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { execFirewallRaw } from '../api/apiClient'; import type { ChainOut, ExecResult, RuleOut, TableOut } from '../types/firewall'; const { Option } = Select; const { TextArea } = Input; const { Title, Text } = Typography; type FormValues = Record; /* ---------------------- Token types (canonical per user) ---------------------- */ type TokenType = | 'meta' | 'ct' | 'ip' | 'ip6' | 'tcp' | 'udp' | 'icmp' | 'ether' | 'vlan' | 'payload' | 'raw' | 'counter' | 'limit' | 'log' | 'nat' | 'queue'; /* ---------------------- TOKEN_FIELD_OPTIONS Each token lists allowed subfields (exactly the fields from the user's canonical list). The `kind` tells the UI which input widget to show (number, enum, string). ---------------------- */ const TOKEN_FIELD_OPTIONS: Record< TokenType, Array<{ value: string; label: string; kind?: 'string' | 'number' | 'enum' }> > = { /* 1) Metadata & Connection Tracking */ meta: [ { value: 'iifname', label: 'iifname (input interface)', kind: 'string' }, { value: 'oifname', label: 'oifname (output interface)', kind: 'string' }, { value: 'l4proto', label: 'l4proto (protocol L4)', kind: 'enum' }, // tcp/udp/icmp/... { value: 'day', label: 'day (day of week)', kind: 'enum' }, { value: 'hour', label: 'hour (hour of day/range)', kind: 'string' }, { value: 'pkttype', label: 'pkttype (packet type)', kind: 'enum' }, { value: 'mark', label: 'mark (packet mark)', kind: 'string' }, { value: 'skuid', label: 'skuid (socket UID)', kind: 'number' }, { value: 'skgid', label: 'skgid (socket GID)', kind: 'number' }, ], ct: [ { value: 'state', label: 'state (ct state)', kind: 'enum' }, { value: 'direction', label: 'direction (original/reply)', kind: 'enum' }, { value: 'status', label: 'status', kind: 'string' }, { value: 'mark', label: 'mark (conntrack mark)', kind: 'string' }, { value: 'expiration', label: 'expiration', kind: 'string' }, { value: 'helper', label: 'helper', kind: 'string' }, ], /* 2) Layer 3: Network Headers */ ip: [ { value: 'saddr', label: 'saddr (source IPv4)', kind: 'string' }, { value: 'daddr', label: 'daddr (destination IPv4)', kind: 'string' }, { value: 'protocol', label: 'protocol (L4) — alias to l4proto', kind: 'enum' }, { value: 'dscp', label: 'dscp (DSCP)', kind: 'enum' }, { value: 'ttl', label: 'ttl (time to live)', kind: 'number' }, { value: 'frag-off', label: 'frag-off (fragment bits)', kind: 'string' }, ], ip6: [ { value: 'saddr', label: 'saddr (source IPv6)', kind: 'string' }, { value: 'daddr', label: 'daddr (destination IPv6)', kind: 'string' }, { value: 'nexthdr', label: 'nexthdr (protocol / next header)', kind: 'enum' }, { value: 'dscp', label: 'dscp (DSCP)', kind: 'enum' }, { value: 'hoplimit', label: 'hoplimit (IPv6 hop limit)', kind: 'number' }, { value: 'flowlabel', label: 'flowlabel', kind: 'number' }, ], /* 3) Layer 4: Transport Headers (appear only when token type tcp/udp/icmp is chosen) */ tcp: [ { value: 'sport', label: 'sport (source port)', kind: 'number' }, { value: 'dport', label: 'dport (destination port)', kind: 'number' }, { value: 'flags', label: 'flags (tcp flags bitmask)', kind: 'enum' }, ], udp: [ { value: 'sport', label: 'sport (source port)', kind: 'number' }, { value: 'dport', label: 'dport (destination port)', kind: 'number' }, ], icmp: [ { value: 'type', label: 'type (icmp type)', kind: 'enum' }, { value: 'code', label: 'code (icmp code)', kind: 'enum' }, ], /* 4) Layer 2: Ethernet & VLAN */ ether: [ { value: 'saddr', label: 'saddr (src MAC)', kind: 'string' }, { value: 'daddr', label: 'daddr (dst MAC)', kind: 'string' }, { value: 'type', label: 'type (ethertype)', kind: 'enum' }, ], vlan: [ { value: 'id', label: 'id (VLAN ID)', kind: 'number' }, // CFI/DEI and PCP exist but user's list specified only VLAN ID; add PCP & DEI as optional helpers: { value: 'pcp', label: 'pcp (priority code point)', kind: 'number' }, { value: 'cfi', label: 'cfi / DEI (drop eligible)', kind: 'number' }, ], /* leftovers and statements */ payload: [{ value: 'payload', label: 'payload(protocol.field)', kind: 'string' }], raw: [{ value: 'raw', label: 'raw text', kind: 'string' }], counter: [{ value: 'counter', label: 'counter', kind: 'string' }], limit: [{ value: 'limit', label: 'limit (rate)', kind: 'string' }], log: [{ value: 'log', label: 'log', kind: 'string' }], nat: [ { value: 'dnat', label: 'dnat to', kind: 'string' }, { value: 'snat', label: 'snat to', kind: 'string' }, { value: 'masquerade', label: 'masquerade', kind: 'string' }, ], queue: [{ value: 'queue', label: 'queue num', kind: 'string' }], }; /* ---------------------- ENUM_VALUES (dropdown contents) Keep these aligned with the user's canonical lists. ---------------------- */ const ENUM_VALUES: Record = { l4proto: ['tcp', 'udp', 'icmp', 'icmpv6', 'igmp', 'esp', 'ah'], days: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], pkttype: ['unicast', 'multicast', 'broadcast', 'other'], ct_state: ['new', 'established', 'related', 'invalid', 'untracked'], ct_direction: ['original', 'reply'], // ICMP message *types* (used for e.g. echo-request/echo-reply) icmp_types: ['echo-request', 'echo-reply', 'destination-unreachable'], // IPv4 reject *reasons* (ICMPv4 codes / textual reasons used with `reject with icmp type `) icmpv4_reasons: [ 'net-unreachable', 'host-unreachable', 'prot-unreachable', 'port-unreachable', // default 'net-prohibited', 'host-prohibited', 'admin-prohibited', ], // IPv6 reject reasons (ICMPv6 textual reasons) icmpv6_reasons: ['no-route', 'admin-prohibited', 'addr-unreachable', 'port-unreachable'], dscp_values: [ 'cs0', 'cs1', 'cs2', 'cs3', 'cs4', 'cs5', 'cs6', 'cs7', 'af11', 'af12', 'af13', 'af21', 'af22', 'af23', 'af31', 'af32', 'af33', 'af41', 'af42', 'af43', 'ef', ], tcp_flags: ['fin', 'syn', 'rst', 'psh', 'ack', 'urg', 'ece', 'cwr'], ethertypes: ['ip', 'ip6', 'arp', 'vlan', 'loopback'], // top-level reject types used in select control. Note `icmpv6` spelled out. reject_types: ['icmp', 'icmpv6', 'icmpx', 'tcp-reset'], }; /* ---------------------- tokenToText: produce nft textual representation from token value (keeps command generation consistent with the UI) ---------------------- */ function tokenToText(token: any): string { if (!token || !token.type) return ''; const t = token.type as TokenType; const d = token.data || {}; // META if (t === 'meta') { const f = d.field; if (!f) return ''; // special formatting: meta l4proto if (f === 'l4proto') { return `meta l4proto ${String(d.value ?? '')}`.trim(); } if (f === 'iifname' || f === 'oifname') { return `meta ${f} ${String(d.value ?? '')}`.trim(); } if (f === 'day') { return `meta day ${String(d.value ?? '')}`.trim(); } if (f === 'hour') { return `meta hour ${String(d.value ?? '')}`.trim(); } if (f === 'pkttype') { return `meta pkttype ${String(d.value ?? '')}`.trim(); } if (f === 'mark') { return `meta mark ${String(d.value ?? '')}`.trim(); } if (f === 'skuid' || f === 'skgid') { return `meta ${f} ${String(d.value ?? '')}`.trim(); } return `meta ${f} ${String(d.value ?? '')}`.trim(); } // CT if (t === 'ct') { const f = d.field; if (!f) return ''; return `ct ${f} ${String(d.value ?? '')}`.trim(); } // IP/IPv6 if (t === 'ip' || t === 'ip6') { const f = d.field; if (!f) return ''; // saddr/daddr: allow CIDR/list/range raw text return `${t} ${f} ${String(d.value ?? '')}`.trim(); } // Transport protocols if (t === 'tcp' || t === 'udp') { const f = d.field; if (!f) return t; if (f === 'dport' || f === 'sport') { return `${t} ${f} ${String(d.value ?? '')}`.trim(); } if (f === 'flags') { // flags could be array or comma-separated const vals = Array.isArray(d.value) ? d.value : String(d.value ?? '') .split(',') .map((s: string) => s.trim()) .filter(Boolean); if (vals.length === 0) return t; // render as: tcp flags { syn, ack } return `${t} flags { ${vals.join(', ')} }`; } return `${t} ${f} ${String(d.value ?? '')}`.trim(); } if (t === 'icmp') { const f = d.field; if (!f) return 'icmp'; return `icmp ${f} ${String(d.value ?? '')}`.trim(); } // ETHER if (t === 'ether') { const f = d.field; if (!f) return ''; return `ether ${f} ${String(d.value ?? '')}`.trim(); } // VLAN if (t === 'vlan') { const f = d.field; if (!f) return 'vlan'; return `vlan ${f} ${String(d.value ?? '')}`.trim(); } // Statements if (t === 'counter') { if (d.packets || d.bytes) { return `counter${d.packets ? ` packets ${d.packets}` : ''}${d.bytes ? ` bytes ${d.bytes}` : ''}`.trim(); } return 'counter'; } if (t === 'limit') { const r = d.rate ?? d.value; return r ? `limit rate ${r}` : 'limit'; } if (t === 'log') { const parts: string[] = []; if (d.level) parts.push(`level ${d.level}`); if (d.group) parts.push(`group ${d.group}`); if (d.snaplen) parts.push(`snaplen ${d.snaplen}`); if (d.prefix) parts.push(`prefix "${d.prefix}"`); return parts.length ? `log ${parts.join(' ')}` : 'log'; } if (t === 'nat') { if (d.kind === 'dnat' && d.to) return `dnat to ${d.to}`; if (d.kind === 'snat' && d.to) return `snat to ${d.to}`; if (d.kind === 'masquerade') return d.to ? `masquerade to ${d.to}` : 'masquerade'; return 'nat'; } if (t === 'queue') { if (d.num) { // allow optional extra token words following queue num, e.g. "queue num 1 bypass" const extra = d.extra ? ` ${String(d.extra)}` : ''; return `queue num ${d.num}${extra}`.trim(); } return 'queue'; } if (t === 'raw') { return String(d.text ?? '').trim(); } if (t === 'payload') { if (d.value) return `payload(${d.value})`; return 'payload'; } return ''; } /* ---------------------- generateCommandFromValues (build textual + final nft add/insert) ---------------------- */ function generateCommandFromValues(values: FormValues) { const tokens = Array.isArray(values.tokens) ? values.tokens : []; const parts: string[] = []; for (const t of tokens) { const txt = tokenToText(t); if (txt) parts.push(txt); } if (values.advanced && typeof values.advanced === 'string' && values.advanced.trim() !== '') { parts.push(values.advanced.trim()); } // Build queue text for NFQUEUE action or queue token if (values.action === 'nfqueue' || values.action === 'queue') { const qnum = values.nfqueue ?? values.queue ?? 1; const bypass = values.nfqueue_bypass ? ' bypass' : ''; const queueText = `queue num ${Number(qnum)}${bypass}`; const combined = parts.join(' '); if (!/\bqueue(?:\s+num)?\b/i.test(combined)) { parts.push(queueText); } else { for (let i = 0; i < parts.length; i++) { if (/\bqueue(?:\s+num)?\b/i.test(parts[i])) { parts[i] = queueText; break; } } } } // Build action/reject/nfqueue textual suffix let actionText: string | null = null; if (values.action === 'accept' || values.action === 'drop') { actionText = values.action; } else if (values.action === 'reject') { // reject requires a rejectType (form enforces it) const rtype = values.rejectType; if (!rtype) { actionText = 'reject'; // fallback, though form validation should prevent this } else if (rtype === 'tcp-reset') { // nft "reject with tcp reset" actionText = 'reject with tcp reset'; } else if (rtype === 'icmp') { // IPv4: "reject with icmp type " const reason = values.rejectIcmpReason || ''; actionText = reason ? `reject with icmp type ${reason}` : 'reject'; } else if (rtype === 'icmpv6') { // IPv6: "reject with icmpv6 type " const reason = values.rejectIcmp6Reason || ''; actionText = reason ? `reject with icmpv6 type ${reason}` : 'reject'; } else if (rtype === 'icmpx') { // inet family abstraction (icmpx) const reason = values.rejectIcmpxReason || ''; actionText = reason ? `reject with icmpx type ${reason}` : 'reject'; } else { actionText = 'reject'; } } else if (values.action === 'nfqueue') { // NFQUEUE action is represented by queue token above; no extra action verb actionText = null; } const textual = (parts.join(' ') + (actionText ? ` ${actionText}` : '')).trim(); const tableSelect = values.tableSelect; const chain = values.chainSelect || 'input'; const [family = 'inet', table = 'filter'] = tableSelect ? String(tableSelect).split(':') : ['inet', 'filter']; const before = values.insertBeforeHandle; const hasBefore = before != null && String(before) !== ''; const verb = hasBefore ? 'insert' : 'add'; const positionPart = hasBefore ? ` position ${before}` : ''; const cmd = `${verb} rule ${family} ${table} ${chain}${positionPart} ${textual}`.replace(/\s+/g, ' ').trim(); return { cmd, textual, position: hasBefore ? Number(before) : undefined }; } /* ------------------------- Component ------------------------- */ interface RuleBuilderProps { onCreated?: () => Promise | void; tables?: TableOut[] | null; rulesLoading?: boolean; rulesError?: string | null; refreshRules?: () => Promise; onRulesChange?: (tables: TableOut[]) => void; } export const RuleBuilder: React.FC = (props: RuleBuilderProps) => { const [form] = Form.useForm(); const [cmdPreview, setCmdPreview] = useState(''); const [refreshing, setRefreshing] = useState(false); const [loading, setLoading] = useState(false); const tableOptions = useMemo( () => (props.tables || []).map((t) => ({ value: `${t.family}:${t.name}`, label: `${t.family}:${t.name}` })), [props.tables], ); const noTables = !(props.tables && props.tables.length > 0); const [insertBeforeOptions, setInsertBeforeOptions] = useState>([]); const updateInsertOptions = useCallback(() => { const ts = form.getFieldValue('tableSelect'); const cs = form.getFieldValue('chainSelect'); if (!ts || !cs) { setInsertBeforeOptions([]); return; } const [family, table] = String(ts).split(':'); const tbl = props.tables?.find((t) => t.family === family && t.name === table); if (!tbl) { setInsertBeforeOptions([]); return; } const ch = (tbl.chains || []).find((c: ChainOut) => c.name === cs); if (!ch || !Array.isArray(ch.rules)) { setInsertBeforeOptions([]); return; } const opts = ch.rules .filter((r: RuleOut) => r && r.handle != null) .map((r: RuleOut) => ({ value: r.handle, label: `#${r.handle} — ${r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || {}).slice(0, 120))}`, })); setInsertBeforeOptions(opts); }, [form, props.tables]); const previewTimerRef = useRef(null); const schedulePreviewUpdate = useCallback(() => { if (previewTimerRef.current) window.clearTimeout(previewTimerRef.current); previewTimerRef.current = window.setTimeout(() => { const v = form.getFieldsValue(); const { cmd } = generateCommandFromValues(v); setCmdPreview(cmd); previewTimerRef.current = null; }, 40); }, [form]); useEffect(() => { if (tableOptions.length > 0) { const first = tableOptions[0].value; form.setFieldsValue({ tableSelect: first, action: 'drop', nfqueue: 1, nfqueue_bypass: false, tokens: [], }); const [f, n] = String(first).split(':'); const tbl = props.tables?.find((t) => t.family === f && t.name === n); if (tbl && tbl.chains && tbl.chains.length > 0) { form.setFieldsValue({ chainSelect: tbl.chains[0].name }); } else { form.setFieldsValue({ chainSelect: undefined }); } setTimeout(() => { updateInsertOptions(); schedulePreviewUpdate(); }, 0); } else { form.setFieldsValue({ action: 'drop', nfqueue: 1, nfqueue_bypass: false, tableSelect: undefined, chainSelect: undefined, tokens: [], }); setInsertBeforeOptions([]); setTimeout(() => schedulePreviewUpdate(), 0); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.tables, tableOptions.length]); useEffect(() => { const cur = form.getFieldsValue(); if (cur.nfqueue == null) form.setFieldsValue({ nfqueue: 1 }); schedulePreviewUpdate(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onValuesChange = useCallback( (_: any, allValues: FormValues) => { if (allValues.action === 'nfqueue' && (allValues.nfqueue == null || allValues.nfqueue === '')) { form.setFieldsValue({ nfqueue: 1 }); allValues.nfqueue = 1; } const ts = allValues.tableSelect; if (ts) { const [f, n] = String(ts).split(':'); const tbl = props.tables?.find((t) => t.family === f && t.name === n); if (tbl) { if (tbl.chains && tbl.chains.length > 0) { if (!allValues.chainSelect) form.setFieldsValue({ chainSelect: tbl.chains[0].name }); } else { form.setFieldsValue({ chainSelect: undefined }); } } } updateInsertOptions(); schedulePreviewUpdate(); }, [form, props.tables, updateInsertOptions, schedulePreviewUpdate], ); const handleCreate = useCallback( async (values: FormValues) => { try { const validated = await form.validateFields(); const { cmd } = generateCommandFromValues(validated); Modal.confirm({ title: 'Run raw nft command', content: (
About to run nft command in {String(validated.tableSelect ?? 'inet:filter')} (see preview). Command:
{cmd}
), okText: 'Run', onOk: async () => { setLoading(true); try { const out: ExecResult = await execFirewallRaw(cmd); const stderrText = out?.stderr ? String(out.stderr).trim() : ''; if (stderrText) { message.error(`Command produced stderr: ${stderrText}`); } else if (out && (out.rc === 0 || out.rc === -1)) { message.success('Command executed (rc ' + out.rc + ')'); if (props.refreshRules) await props.refreshRules(); if (props.onCreated) await props.onCreated(); } else { const info = out ? `rc:${out.rc}` + (out.stdout ? ` stdout:${out.stdout}` : '') + (out.stderr ? ` stderr:${out.stderr}` : '') : 'unknown result'; message.error('Command failed: ' + info); } } catch (err: any) { message.error('Execution failed: ' + (err?.message ?? String(err))); } finally { setLoading(false); } }, }); } catch (err) { schedulePreviewUpdate(); } }, [form, props.refreshRules, props.onCreated, schedulePreviewUpdate], ); const chainOptions = useMemo(() => { const ts = form.getFieldValue('tableSelect'); if (!ts) return []; const [f, n] = String(ts).split(':'); const tbl = props.tables?.find((t) => t.family === f && t.name === n); if (!tbl) return []; return tbl.chains.map((c) => ( )); }, [form, props.tables]); const handleRefresh = useCallback(async () => { setRefreshing(true); try { if (props.refreshRules) { await props.refreshRules(); message.success('Rules refresh requested'); } else { message.info('No refresh function provided by parent.'); } } catch (err) { console.warn('refresh failed', err); message.error('Refresh failed'); } finally { updateInsertOptions(); setRefreshing(false); } }, [props.refreshRules, updateInsertOptions]); /* helper styles */ const tokenRowStyle: React.CSSProperties = { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'nowrap', width: '100%', }; const leftControlsStyle: React.CSSProperties = { display: 'flex', gap: 8, alignItems: 'center', minWidth: 72, flex: '0 0 72px', }; const typeSelectStyle: React.CSSProperties = { minWidth: 180, maxWidth: 260, flex: '0 0 220px' }; const fieldSelectStyle: React.CSSProperties = { minWidth: 160, maxWidth: 260, flex: '0 0 220px' }; const valueInputStyle: React.CSSProperties = { minWidth: 120, flex: '1 1 240px', maxWidth: '60%' }; const actionControlsStyle: React.CSSProperties = { minWidth: 96, flex: '0 0 96px', display: 'flex', justifyContent: 'flex-end', }; return ( Add Firewall Rule (raw) Build an nft rule using canonical match list (meta/ct/ip/ip6/tcp/udp/icmp/ether/vlan).
0 ? tableOptions[0].value : undefined, tokens: [], }} onFinish={handleCreate} onValuesChange={onValuesChange} > {/* Table / chain */} {/* Insert before */} Use when you want the new rule before a known handle. Refresh tables after changes. {/* Token builder header + add control */} Token builder Add tokens from the canonical lists (Meta/CT/IPv4/IPv6/Transport/Ether/VLAN). Each subfield has guidance. {(fields, { add }) => (
)}
{/* Tokens Form.List rendering */} {(fields, { remove, move }) => fields.length === 0 ? ( No tokens yet — add one using the selector above. ) : ( fields.map((field, idx) => (
{/* Token type select */} {/* Token field + value UI (depends on token type and subfield) */} prev.tokens?.[field.name]?.type !== cur.tokens?.[field.name]?.type || prev.tokens?.[field.name]?.data?.field !== cur.tokens?.[field.name]?.data?.field } style={{ marginBottom: 0, width: '100%' }} > {() => { const tokenType = form.getFieldValue(['tokens', field.name, 'type']) as TokenType | undefined; const options = tokenType ? TOKEN_FIELD_OPTIONS[tokenType] || [] : []; // COUNTER special-case if (tokenType === 'counter') { return (
counter Specify counters explicitly — nftables does not add counters by default.
); } // LIMIT special-case if (tokenType === 'limit') { return (
limit Rate expressions: 5/second, 400/minute,{' '} over 40/day.
); } // LOG special-case if (tokenType === 'log') { return (
log Log options — level, group, snaplen, prefix. Prefix is useful to filter logs.
); } // QUEUE special-case inside token list (separate from NFQUEUE action) if (tokenType === 'queue') { return (
queue NFQUEUE/queue options — set queue number and optional extra parameters.
); } // NAT special-case if (tokenType === 'nat') { return (
NAT target examples: 10.0.0.5:8080, :80-90, or{' '} 10.0.0.5.
); } // Generic tokens with subfield dropdown if (options.length > 0) { return (
prev.tokens?.[field.name]?.data?.field !== cur.tokens?.[field.name]?.data?.field || prev.tokens?.[field.name]?.type !== cur.tokens?.[field.name]?.type } style={{ margin: 0, flex: '1 1 auto', minWidth: 0 }} > {() => { const selField = form.getFieldValue(['tokens', field.name, 'data', 'field']); const tType = form.getFieldValue(['tokens', field.name, 'type']); const opts = tType ? TOKEN_FIELD_OPTIONS[tType as TokenType] || [] : []; const meta = opts.find((o) => o.value === selField); const kind = meta?.kind ?? 'string'; /* --- Field-specific UIs & helpers (placeholders + explanatory text) --- */ // STRING typed helpers for interface names if (tType === 'meta' && (selField === 'iifname' || selField === 'oifname')) { return (
Enter a system interface name. Use the interface list on your host (ip link).
); } // L4PROTO dropdown for meta.l4proto if (tType === 'meta' && selField === 'l4proto') { return (
Choose a transport protocol (TCP/UDP/ICMP/...)
); } // Day of week (meta.day) if (tType === 'meta' && selField === 'day') { return (
Pick a weekday. Use with meta.hour to create schedules.
); } // Hour range (meta.hour) — free text but show placeholder/range hint if (tType === 'meta' && selField === 'hour') { return (
Enter a time or range in 24-hour format. Examples: 09:00 or{' '} 08:00-17:00.
); } // Packet type (meta.pkttype) if (tType === 'meta' && selField === 'pkttype') { return (
Packet type: unicast, multicast, broadcast, other.
); } // Packet/conn mark if ((tType === 'meta' || tType === 'ct') && selField === 'mark') { return (
A 32-bit mark used for routing/classing. Hex or decimal allowed.
); } // skuid / skgid if (tType === 'meta' && (selField === 'skuid' || selField === 'skgid')) { return (
UID/GID from the originating socket. Use numeric values or system names (if supported by backend).
); } // CT state if (tType === 'ct' && selField === 'state') { return (
Connection states. Pick one or multiple: new, established, related, invalid, untracked.
); } // CT direction if (tType === 'ct' && selField === 'direction') { return (
Direction relative to connection: original or reply.
); } /* --- IP / IP6 address helpers --- */ if ( (tType === 'ip' || tType === 'ip6') && (selField === 'saddr' || selField === 'daddr') ) { if (tType === 'ip') { return (
IPv4 address, CIDR, list or range. Example: 192.168.1.0/24.
); } return (
IPv6 address or CIDR. Example: 2001:db8::/32.
); } // protocol / nexthdr / ip.protocol (L4 protocol): show l4proto list if ( (tType === 'ip' && selField === 'protocol') || (tType === 'ip6' && selField === 'nexthdr') ) { return (
Transport / next header protocol. Example: tcp,{' '} udp, esp.
); } // DSCP if ((tType === 'ip' || tType === 'ip6') && selField === 'dscp') { return (
DSCP value (cs0-cs7, af*, ef). Used for QoS marking.
); } // TTL / hoplimit numeric if ( (tType === 'ip' && selField === 'ttl') || (tType === 'ip6' && selField === 'hoplimit') ) { return (
Numeric TTL / Hop Limit (0-255).
); } // IP fragment bits (frag-off) — single string placeholder if (tType === 'ip' && selField === 'frag-off') { return (
Fragmentation flags / mask. Use known keywords or bitmasks.
); } /* --- Transport: TCP/UDP/ICMP --- */ // Ports: allow numeric or service name if ( (tType === 'tcp' || tType === 'udp') && (selField === 'dport' || selField === 'sport') ) { return (
Port number (1-65535) or well-known name (ssh/http/dns).
); } // TCP flags multi-select if (tType === 'tcp' && selField === 'flags') { return (
Pick one or more TCP flags (syn, ack, fin, rst, psh, urg, ece, cwr).
); } // ICMP type/code dropdowns if (tType === 'icmp' && selField === 'type') { return (
ICMP types: e.g. echo-request, echo-reply.
); } if (tType === 'icmp' && selField === 'code') { return (
ICMP codes such as net-unreachable or{' '} host-unreachable.
); } /* --- Layer 2: Ethernet / VLAN --- */ if (tType === 'ether') { if (selField === 'saddr' || selField === 'daddr') { return (
MAC address (colon-separated). Accepts single addresses, sets or ranges where supported.
); } if (selField === 'type') { return (
EtherType: choose IPv4, IPv6, ARP, VLAN, etc.
); } } // VLAN ID / PCP / CFI if (tType === 'vlan') { if (selField === 'id') { return (
VLAN ID (1-4094). Example: 100.
); } if (selField === 'pcp') { return (
Priority Code Point (0-7).
); } if (selField === 'cfi') { return (
Drop Eligible Indicator (0 or 1).
); } } /* --- Payload / default free text input --- */ if (tType === 'payload' || (kind === 'string' && !selField)) { return (
Raw payload selector using protocol.field syntax, e.g.{' '} tcp.dport.
); } // Default fallback free-text with helpful examples return (
Enter the matching value. Examples: port numbers (53), CIDR ( 10.0.0.0/8), sets ({'{1,2,3}'}), or ranges ( 1-1024).
); }}
); } return Unknown token type or no fields configured; }}
)) ) }
{/* Action + NFQUEUE + Reject options: render action radios, then render reject subform and nfqueue subform directly under it (same column) */} Drop Accept Reject NFQUEUE {/* Reject options (render under radios, same column) */} prev.action !== cur.action} noStyle> {() => form.getFieldValue('action') === 'reject' ? ( <> {/* IPv4 reject reasons */} prev.rejectType !== cur.rejectType} noStyle> {() => form.getFieldValue('rejectType') === 'icmp' ? ( ) : null } {/* IPv6 reject reasons */} prev.rejectType !== cur.rejectType} noStyle> {() => form.getFieldValue('rejectType') === 'icmpv6' ? ( ) : null } {/* icmpx (inet) */} prev.rejectType !== cur.rejectType} noStyle> {() => form.getFieldValue('rejectType') === 'icmpx' ? ( ) : null } ) : null } {/* NFQUEUE options (now rendered under radios in same column) */} prev.action !== cur.action} noStyle> {() => form.getFieldValue('action') === 'nfqueue' ? ( <> { // update preview immediately schedulePreviewUpdate(); }} > Bypass kernel queuing (append bypass to queue) When checked, the generated queue statement will include bypass (e.g. queue num 1 bypass). ) : null } {/* right column is free for notes / quick helpers */} Use NFQUEUE to hand packets to userspace. Full reject support requires kernel >= 3.18 — when using reject you can choose ICMP/ICMPv6/tcp-reset or the inet-level icmpx abstraction. Not supported on bridges. {/* advanced text */}