diff --git a/frontend/src/components/FirewallRuleBuilder.tsx b/frontend/src/components/FirewallRuleBuilder.tsx index f69c9bb..bc10ae1 100644 --- a/frontend/src/components/FirewallRuleBuilder.tsx +++ b/frontend/src/components/FirewallRuleBuilder.tsx @@ -1,8 +1,20 @@ // 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, @@ -27,23 +39,19 @@ const { Title, Text } = Typography; type FormValues = Record; -/* --- Token types and field maps --- */ - +/* ---------------------- + Token types (canonical per user) + ---------------------- */ type TokenType = + | 'meta' + | 'ct' | 'ip' | 'ip6' | 'tcp' | 'udp' - | 'udplite' - | 'sctp' - | 'dccp' | 'icmp' - | 'icmpv6' - | 'meta' - | 'ct' | 'ether' | 'vlan' - | 'frag' | 'payload' | 'raw' | 'counter' @@ -52,163 +60,83 @@ type TokenType = | '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' }> > = { - ip: [ - { value: 'dscp', label: 'dscp', kind: 'string' }, - { value: 'length', label: 'length', kind: 'string' }, - { value: 'id', label: 'id', kind: 'number' }, - { value: 'frag-off', label: 'frag-off', kind: 'string' }, - { value: 'ttl', label: 'ttl', kind: 'number' }, - { value: 'protocol', label: 'protocol', kind: 'string' }, - { value: 'checksum', label: 'checksum', kind: 'number' }, - { value: 'saddr', label: 'saddr', kind: 'string' }, - { value: 'daddr', label: 'daddr', kind: 'string' }, - { value: 'version', label: 'version', kind: 'number' }, - { value: 'hdrlength', label: 'hdrlength', kind: 'number' }, - ], - ip6: [ - { value: 'dscp', label: 'dscp', kind: 'string' }, - { value: 'flowlabel', label: 'flowlabel', kind: 'number' }, - { value: 'length', label: 'length', kind: 'number' }, - { value: 'nexthdr', label: 'nexthdr', kind: 'string' }, - { value: 'hoplimit', label: 'hoplimit', kind: 'number' }, - { value: 'saddr', label: 'saddr', kind: 'string' }, - { value: 'daddr', label: 'daddr', kind: 'string' }, - { value: 'version', label: 'version', kind: 'number' }, - ], - tcp: [ - { value: 'dport', label: 'dport', kind: 'number' }, - { value: 'sport', label: 'sport', kind: 'number' }, - { value: 'sequence', label: 'sequence', kind: 'number' }, - { value: 'ackseq', label: 'ackseq', kind: 'number' }, - { value: 'flags', label: 'flags', kind: 'string' }, - { value: 'window', label: 'window', kind: 'number' }, - { value: 'checksum', label: 'checksum', kind: 'number' }, - { value: 'urgptr', label: 'urgptr', kind: 'number' }, - { value: 'doff', label: 'doff', kind: 'number' }, - ], - udp: [ - { value: 'dport', label: 'dport', kind: 'number' }, - { value: 'sport', label: 'sport', kind: 'number' }, - { value: 'length', label: 'length', kind: 'number' }, - { value: 'checksum', label: 'checksum', kind: 'number' }, - ], - udplite: [ - { value: 'dport', label: 'dport', kind: 'number' }, - { value: 'sport', label: 'sport', kind: 'number' }, - { value: 'checksum', label: 'checksum', kind: 'number' }, - ], - sctp: [ - { value: 'dport', label: 'dport', kind: 'number' }, - { value: 'sport', label: 'sport', kind: 'number' }, - { value: 'checksum', label: 'checksum', kind: 'number' }, - { value: 'vtag', label: 'vtag', kind: 'number' }, - ], - dccp: [ - { value: 'dport', label: 'dport', kind: 'number' }, - { value: 'sport', label: 'sport', kind: 'number' }, - { value: 'type', label: 'type', kind: 'string' }, - ], - icmp: [ - { value: 'type', label: 'type', kind: 'string' }, - { value: 'code', label: 'code', kind: 'number' }, - { value: 'checksum', label: 'checksum', kind: 'number' }, - { value: 'id', label: 'id', kind: 'number' }, - { value: 'sequence', label: 'sequence', kind: 'number' }, - { value: 'mtu', label: 'mtu', kind: 'number' }, - { value: 'gateway', label: 'gateway', kind: 'number' }, - ], - icmpv6: [ - { value: 'type', label: 'type', kind: 'string' }, - { value: 'code', label: 'code', kind: 'number' }, - { value: 'checksum', label: 'checksum', kind: 'number' }, - { value: 'id', label: 'id', kind: 'number' }, - { value: 'sequence', label: 'sequence', kind: 'number' }, - { value: 'mtu', label: 'mtu', kind: 'number' }, - { value: 'max-delay', label: 'max-delay', kind: 'number' }, - ], - // META token includes many selectors — expanded from your reference + /* 1) Metadata & Connection Tracking */ meta: [ - // packet info selectors - { value: 'pkttype', label: 'pkttype', kind: 'string' }, - { value: 'length', label: 'length', kind: 'number' }, - { value: 'protocol', label: 'protocol', kind: 'string' }, - { value: 'nfproto', label: 'nfproto', kind: 'string' }, - { value: 'l4proto', label: 'l4proto', kind: 'string' }, - - // interface selectors - { value: 'iif', label: 'iif (input ifindex)', kind: 'string' }, - { value: 'iifname', label: 'iifname', kind: 'string' }, - { value: 'iiftype', label: 'iiftype', kind: 'string' }, - { value: 'iifkind', label: 'iifkind', kind: 'string' }, - { value: 'iifgroup', label: 'iifgroup', kind: 'string' }, - { value: 'oif', label: 'oif (output ifindex)', kind: 'string' }, - { value: 'oifname', label: 'oifname', kind: 'string' }, - { value: 'oiftype', label: 'oiftype', kind: 'string' }, - { value: 'oifkind', label: 'oifkind', kind: 'string' }, - { value: 'oifgroup', label: 'oifgroup', kind: 'string' }, - { value: 'ibrname', label: 'ibrname (bridge in)', kind: 'string' }, - { value: 'obrname', label: 'obrname (bridge out)', kind: 'string' }, - { value: 'ibrvproto', label: 'ibrvproto', kind: 'string' }, - { value: 'ibrpvid', label: 'ibrpvid', kind: 'number' }, - { value: 'sdif', label: 'sdif (slave ifindex)', kind: 'number' }, - { value: 'sdifname', label: 'sdifname', kind: 'string' }, - - // mark / routing / priority - { value: 'mark', label: 'mark', kind: 'string' }, - { value: 'priority', label: 'priority', kind: 'string' }, - { value: 'rtclassid', label: 'rtclassid', kind: 'string' }, - - // socket UID / GID - { value: 'skuid', label: 'skuid', kind: 'string' }, - { value: 'skgid', label: 'skgid', kind: 'string' }, - - // time selectors - { value: 'time', label: 'time', kind: 'string' }, - { value: 'day', label: 'day', kind: 'string' }, - { value: 'hour', label: 'hour', kind: 'string' }, - - // security / misc - { value: 'cpu', label: 'cpu', kind: 'number' }, - { value: 'cgroup', label: 'cgroup', kind: 'number' }, - { value: 'secmark', label: 'secmark', kind: 'string' }, - { value: 'ipsec', label: 'ipsec', kind: 'string' }, - - // miscellaneous - { value: 'nftrace', label: 'nftrace', kind: 'string' }, - { value: 'random', label: 'random', kind: 'number' }, - - // older/general misc - { value: 'pkttype_alias', label: 'pkttype (alias)', kind: 'string' }, // kept for backwards/aliasing if needed + { 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', kind: 'string' }, - { value: 'direction', label: 'direction', kind: 'string' }, + { 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', 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', kind: 'string' }, - { value: 'daddr', label: 'daddr', kind: 'string' }, - { value: 'type', label: 'type', kind: 'string' }, + { 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', kind: 'number' }, - { value: 'cfi', label: 'cfi', kind: 'number' }, - { value: 'pcp', label: 'pcp', kind: 'number' }, - ], - frag: [ - { value: 'nexthdr', label: 'nexthdr', kind: 'string' }, - { value: 'reserved', label: 'reserved', kind: 'number' }, - { value: 'frag-off', label: 'frag-off', kind: 'number' }, - { value: 'more-fragments', label: 'more-fragments', kind: 'number' }, - { value: 'id', label: 'id', kind: 'number' }, + { 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' }], @@ -222,73 +150,155 @@ const TOKEN_FIELD_OPTIONS: Record< 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 = { - // layer 4 protocols supported by nft (exposed to protocol selectors) - ip_protocols: ['ah', 'esp', 'udp', 'udplite', 'tcp', 'dccp', 'sctp', 'ipcomp', 'icmp'], - icmp_types: [ - 'echo-reply', - 'destination-unreachable', - 'source-quench', - 'redirect', - 'echo-request', - 'time-exceeded', - 'parameter-problem', - 'timestamp-request', - 'timestamp-reply', - 'info-request', - 'info-reply', + 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', ], - icmpv6_types: ['destination-unreachable', 'packet-too-big', 'time-exceeded', 'echo-request', 'echo-reply'], - meta_pkttype: ['unicast', 'broadcast', 'multicast', 'other'], - ct_state: ['new', 'established', 'related', 'untracked'], - days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - boolean_choices: ['true', 'false'], + // 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'], }; -/* --- token -> textual conversion --- */ +/* ---------------------- + 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 || {}; - if (t === 'ip' || t === 'ip6') { - const field = d.field; - if (!field) return ''; - return `${t} ${field} ${String(d.value ?? '').trim()}`.trim(); - } - if (t === 'tcp' || t === 'udp' || t === 'udplite' || t === 'sctp' || t === 'dccp') { - if (d.dport) return `${t} dport ${d.dport}`; - if (d.sport) return `${t} sport ${d.sport}`; - if (d.field && d.value !== undefined) return `${t} ${d.field} ${String(d.value)}`.trim(); - return t; - } - if (t === 'icmp' || t === 'icmpv6') { - const field = d.field; - if (!field) return ''; - return `${t} ${field} ${String(d.value ?? '').trim()}`.trim(); - } + + // META if (t === 'meta') { - const field = d.field; - if (!field) return ''; - const val = d.value !== undefined ? String(d.value).trim() : ''; - return `meta ${field} ${val}`.trim(); + 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 field = d.field; - if (!field) return ''; - return `ct ${field} ${String(d.value ?? '').trim()}`.trim(); + 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 field = d.field; - if (!field) return ''; - return `ether ${field} ${String(d.value ?? '').trim()}`.trim(); + const f = d.field; + if (!f) return ''; + return `ether ${f} ${String(d.value ?? '')}`.trim(); } - if (t === 'vlan' || t === 'frag' || t === 'payload') { - const field = d.field; - if (!field) return ''; - if (d.value !== undefined) return `${t} ${field} ${String(d.value)}`.trim(); - if (t === 'payload' && d.protocol && d.field) return `payload(${d.protocol}.${d.field})`; - return `${t} ${field}`.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(); @@ -296,8 +306,8 @@ function tokenToText(token: any): string { return 'counter'; } if (t === 'limit') { - const rate = d.rate ?? d.value; - return rate ? `limit rate ${rate}` : 'limit'; + const r = d.rate ?? d.value; + return r ? `limit rate ${r}` : 'limit'; } if (t === 'log') { const parts: string[] = []; @@ -305,28 +315,36 @@ function tokenToText(token: any): string { if (d.group) parts.push(`group ${d.group}`); if (d.snaplen) parts.push(`snaplen ${d.snaplen}`); if (d.prefix) parts.push(`prefix "${d.prefix}"`); - if (parts.length === 0) return 'log'; - return `log ${parts.join(' ')}`.trim(); + 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') { - if (d.to) return `masquerade to ${d.to}`; - return 'masquerade'; - } + if (d.kind === 'masquerade') return d.to ? `masquerade to ${d.to}` : 'masquerade'; return 'nat'; } if (t === 'queue') { - if (d.num) return `queue num ${d.num}`; + 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 (d.text ?? '').trim(); + 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[] = []; @@ -340,9 +358,11 @@ function generateCommandFromValues(values: FormValues) { 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 queueText = `queue num ${Number(qnum)}`; + 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); @@ -356,9 +376,36 @@ function generateCommandFromValues(values: FormValues) { } } + // Build action/reject/nfqueue textual suffix let actionText: string | null = null; - if (values.action === 'accept' || values.action === 'drop' || values.action === 'reject') { + 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(); @@ -426,9 +473,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) .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)) - }`, + label: `#${r.handle} — ${r.text ?? (typeof r.expr === 'string' ? r.expr : JSON.stringify(r.expr || {}).slice(0, 120))}`, })); setInsertBeforeOptions(opts); }, [form, props.tables]); @@ -451,6 +496,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) tableSelect: first, action: 'drop', nfqueue: 1, + nfqueue_bypass: false, tokens: [], }); @@ -470,6 +516,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) form.setFieldsValue({ action: 'drop', nfqueue: 1, + nfqueue_bypass: false, tableSelect: undefined, chainSelect: undefined, tokens: [], @@ -629,7 +676,9 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) Add Firewall Rule (raw) - Build a raw nft command using token builder and execute via raw endpoint. + + Build an nft rule using canonical match list (meta/ct/ip/ip6/tcp/udp/icmp/ether/vlan). + @@ -649,6 +698,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) initialValues={{ action: 'drop', nfqueue: 1, + nfqueue_bypass: false, tableSelect: tableOptions.length > 0 ? tableOptions[0].value : undefined, tokens: [], }} @@ -684,7 +734,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) { + // sensible defaults per token type const defaultData = - val === 'ip' || val === 'ip6' - ? { field: 'saddr', value: '' } - : val === 'tcp' || val === 'udp' || val === 'udplite' || val === 'sctp' - ? { field: 'dport' } - : val === 'meta' - ? { field: 'iifname', value: '' } - : val === 'ct' - ? { field: 'state', value: '' } - : val === 'payload' - ? { protocol: 'ip', field: 'protocol' } - : val === 'raw' - ? { text: '' } - : {}; + val === 'meta' + ? { field: 'iifname', value: '' } + : val === 'ct' + ? { field: 'state', value: 'new' } + : val === 'ip' + ? { field: 'saddr', value: '' } + : val === 'ip6' + ? { field: 'saddr', value: '' } + : val === 'tcp' + ? { field: 'dport', value: '' } + : val === 'udp' + ? { field: 'dport', value: '' } + : val === 'icmp' + ? { field: 'type', value: '' } + : val === 'ether' + ? { field: 'daddr', value: '' } + : val === 'vlan' + ? { field: 'id', value: '' } + : val === 'payload' + ? { field: 'payload', value: '' } + : val === 'raw' + ? { text: '' } + : {}; add({ type: val, data: defaultData }); setTimeout(() => schedulePreviewUpdate(), 40); }} - style={{ width: 260 }} + style={{ width: 320 }} dropdownMatchSelectWidth={false} disabled={noTables} > - - - - - - - - - - - - - - + + + + + + + + + - + - - + + )} @@ -797,6 +853,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
+ {/* Token type select */} + {/* Token field + value UI (depends on token type and subfield) */} prev.tokens?.[field.name]?.type !== cur.tokens?.[field.name]?.type} + shouldUpdate={(prev, cur) => + 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 @@ -829,30 +889,35 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) + + 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 @@ -878,17 +943,39 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) name={[field.name, 'data', 'prefix']} style={{ margin: 0, flex: '1 1 auto' }} > - + + + 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 ( -
+
- + -
- ); - } - - if (tokenType === 'queue') { - return ( -
- - queue + + 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 (
= (props: RuleBuilderProps) const meta = opts.find((o) => o.value === selField); const kind = meta?.kind ?? 'string'; - // numeric field - if (kind === 'number' || selField === 'random') { + /* --- 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). + +
); } - // protocol enum (layer4) — includes AH, ESP, UDP, UDPlite, TCP, DCCP, SCTP, IPComp + // 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 ( - selField === 'protocol' && - (tType === 'ip' || tType === 'ip6' || tType === 'meta' || tType === 'payload') + (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 ( - - - - ); - } - - // pkttype enum - if (selField === 'pkttype' || selField === 'pkttype_alias') { - return ( - - - - ); - } - - // day enum - if (selField === 'day') { - return ( - - - - ); - } - - // boolean-ish selectors: ipsec, nftrace, secmark - if (selField === 'ipsec' || selField === 'nftrace' || selField === 'secmark') { - return ( - - - - ); - } - - // time: free text — allow ISO or ns since epoch - if (selField === 'time') { return (
- + - - Use ISO timestamp (recommended) or integer nanoseconds since epoch. + + Transport / next header protocol. Example: tcp,{' '} + udp, esp.
); } - // hour: HH:MM[:SS] - if (selField === 'hour') { + // DSCP + if ((tType === 'ip' || tType === 'ip6') && selField === 'dscp') { return (
- + - - Use 24-hour format. You may specify ranges like "09:00-17:00". + + DSCP value (cs0-cs7, af*, ef). Used for QoS marking.
); } - // ETHER fields: MAC addresses and EtherType + // 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 in hex (colon-separated). You can use broadcast - ff:ff:ff:ff:ff:ff or a specific MAC. Sets accept single MAC - or ranges/sets. + + MAC address (colon-separated). Accepts single addresses, sets or ranges + where supported.
); @@ -1084,49 +1405,35 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) return (
- + - - EtherType value (hex) — e.g. 0x0800 for IPv4,{' '} - 0x86dd for IPv6. - + EtherType: choose IPv4, IPv6, ARP, VLAN, etc.
); } } - // VLAN fields: numeric with ranges + // VLAN ID / PCP / CFI if (tType === 'vlan') { if (selField === 'id') { return (
- - VLAN ID (0-4095). Example: 100. - -
- ); - } - if (selField === 'cfi') { - return ( -
- - - - - Drop Eligible Indicator (0 or 1). + + VLAN ID (1-4094). Example: 100.
); @@ -1136,79 +1443,58 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)
- - Priority Code Point (0-7). - + Priority Code Point (0-7).
); } - } - - // IP addresses: examples for ip / ip6 saddr/daddr - if ( - (tType === 'ip' || tType === 'ip6') && - (selField === 'saddr' || selField === 'daddr') - ) { - if (tType === 'ip') { + if (selField === 'cfi') { return (
- + - - IPv4 address, CIDR, list or range — e.g. 192.168.1.100 or{' '} - 192.168.1.0/24. - + Drop Eligible Indicator (0 or 1).
); } - return ( -
- - - - - IPv6 address or CIDR — e.g. abcd::100 or{' '} - 2001:db8::/32. - -
- ); } - // payload(protocol.field) helper example - if (tType === 'payload' || selField === 'payload') { + /* --- Payload / default free text input --- */ + if (tType === 'payload' || (kind === 'string' && !selField)) { return (
- + - - Use protocol.field syntax — e.g. ip.protocol or{' '} + + Raw payload selector using protocol.field syntax, e.g.{' '} tcp.dport.
); } - // default free-text input with example hint + // 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). @@ -1240,8 +1526,9 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) - {/* Action + NFQUEUE */} - + {/* Action + NFQUEUE + Reject options: render action radios, then render + reject subform and nfqueue subform directly under it (same column) */} + @@ -1251,26 +1538,150 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps) NFQUEUE - - + {/* Reject options (render under radios, same column) */} prev.action !== cur.action} noStyle> {() => - form.getFieldValue('action') === 'nfqueue' ? ( - - - + 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. + @@ -1280,7 +1691,7 @@ export const RuleBuilder: React.FC = (props: RuleBuilderProps)