bokamba / logforge / parse / iptables / netfilter

$ logforge parse iptables

Parse iptables / netfilter logs → regex, Grok, Wazuh & rsyslog

iptables logging is not really an iptables format at all — it is the Linux netfilter framework writing to the kernel ring buffer via the LOG target, so the lines surface through the kernel facility in dmesg, /var/log/kern.log, or the journal. What makes a netfilter line identifiable is the --log-prefix you set on the rule: a short literal string (commonly '[UFW BLOCK] ', 'iptables-drop: ', or your own tag) that the kernel prepends to every packet the rule matches, which is how you tell which rule fired. After the syslog/kernel header and that prefix comes the packet dump: a space-separated list of KEY=VALUE tokens that netfilter fills from the packet headers. The worked example below is deliberately reduced to that netfilter KEY=VALUE payload — the syslog/kernel header and --log-prefix stripped off — so the generated parser locks onto the packet fields (which is exactly what you want a decoder that runs after the syslog layer has already split off the header to do); prepend your header/prefix pattern when you parse the raw kernel line end to end.

The token set is well-defined but conditional. IN= and OUT= name the ingress and egress interfaces, and one of them is legitimately empty — for an inbound packet OUT= has no value (it is a bare 'OUT=' with nothing after it), and for an outbound packet IN= is empty. That empty-value case is normal and must not break your tokenizer. MAC= is the packet's link-layer header as a long colon-separated hex string (destination MAC + source MAC + ethertype concatenated). SRC= and DST= are the IPv4/IPv6 addresses, LEN= the total length, TTL= the time-to-live, and PROTO= the protocol (TCP, UDP, ICMP). For TCP and UDP you then get SPT= and DPT= (source and destination ports), and for TCP the flags appear as bare words — SYN, ACK, PSH, RST, FIN, URG — sitting between the KEY=VALUE tokens with no key of their own, which is the second thing that trips naive KEY=VALUE parsers. WINDOW=, RES=, and ID= round out the header detail.

Parsing is therefore mostly about being liberal: accept KEY=VALUE pairs in any order (netfilter's order is stable but assume it is not), tolerate empty values (OUT=), and treat the bare TCP-flag words as a separate flag set rather than expecting every token to contain an equals sign. The syslog header carries no year and the kernel timestamp may also appear as a bracketed seconds-since-boot value on some setups. For detection the fields that matter are the log-prefix (which rule/policy blocked it), SRC and SPT (attacker origin), DST and DPT (what they were reaching for), PROTO, and the TCP flags — a burst of SYN-only packets to many DPTs from one SRC is a port scan, and grouping drops by DPT shows what is being probed.

Open this in LogForge →

What an iptables / netfilter line looks like

The key=value sample below is fed verbatim into the engine to produce every parser on this page.

IN=eth0 OUT= MAC=00:1a:2b:3c:4d:5e SRC=203.0.113.45 DST=192.0.2.10 LEN=60 TTL=54 PROTO=TCP SPT=51234 DPT=22 WINDOW=1024 SYN
IN=eth0 OUT= MAC=00:1a:2b:3c:4d:5e SRC=198.51.100.77 DST=192.0.2.20 LEN=40 TTL=118 PROTO=TCP SPT=40112 DPT=3389 WINDOW=512 SYN

Detected fields

The engine classified this sample as kv and consolidated 11 fields across 2 lines. Fields marked literal were identical on every sample line, so they are baked into the pattern as anchors rather than captured.

  • in : literal · literal
  • out : literal · literal
  • mac : mac · literal
  • src : ipv4
  • dst : ipv4
  • len : number
  • ttl : number
  • proto : literal · literal
  • spt : port
  • dpt : port
  • window : number

Regex (named capture groups)

# sample: IN=eth0 OUT= MAC=00:1a:2b:3c:4d:5e SRC=203.0.113.45 DST=192.0.2.10 LEN=60 TTL=54 PROTO=TCP SPT=51234 DPT=22 WINDOW=1024 SYN
# groups: src=203.0.113.45, dst=192.0.2.10, len=60, ttl=54, spt=51234, dpt=22, window=1024
^(?=.*?(?<![\w.\-])IN=eth0)(?=.*?(?<![\w.\-])OUT=|)(?=.*?(?<![\w.\-])MAC=00:1a:2b:3c:4d:5e)(?=.*?(?<![\w.\-])SRC=(?<src>\d{1,3}(?:\.\d{1,3}){3}))(?=.*?(?<![\w.\-])DST=(?<dst>\d{1,3}(?:\.\d{1,3}){3}))(?=.*?(?<![\w.\-])LEN=(?<len>-?\d+(?:\.\d+)?))(?=.*?(?<![\w.\-])TTL=(?<ttl>-?\d+(?:\.\d+)?))(?=.*?(?<![\w.\-])PROTO=TCP)(?=.*?(?<![\w.\-])SPT=(?<spt>\d{1,5}))(?=.*?(?<![\w.\-])DPT=(?<dpt>\d{1,5}))(?=.*?(?<![\w.\-])WINDOW=(?<window>-?\d+(?:\.\d+)?)).*$
  • note a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead

Grok pattern (Logstash / Elastic)

IN=eth0(?: OUT=)? MAC=00:1a:2b:3c:4d:5e SRC=%{IPV4:src} DST=%{IPV4:dst} LEN=%{NUMBER:len} TTL=%{NUMBER:ttl} PROTO=TCP SPT=%{INT:spt} DPT=%{INT:dpt} WINDOW=%{NUMBER:window}
  • note kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note constant field "mac" embedded as literal anchor "00:1a:2b:3c:4d:5e" (varying=false)
  • note 1 optional field(s) wrapped in (?:…)? inline regex — grok has no native optional syntax

Wazuh decoder (OS_Regex XML)

<!--
  Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
  sample: IN=eth0 OUT= MAC=00:1a:2b:3c:4d:5e SRC=203.0.113.45 DST=192.0.2.10 LEN=60 TTL=54 PROTO=TCP SPT=51234 DPT=22 WINDOW=1024 SYN
  test with: /var/ossec/bin/wazuh-logtest
-->

<decoder name="iptables-kv">
  <prematch>^IN=\w+ OUT=</prematch>
</decoder>

<decoder name="iptables-kv">
  <parent>iptables-kv</parent>
  <regex offset="after_parent"> SRC=(\d+.\d+.\d+.\d+)</regex>
  <order>srcip</order>
</decoder>

<decoder name="iptables-kv">
  <parent>iptables-kv</parent>
  <regex offset="after_parent"> DST=(\d+.\d+.\d+.\d+)</regex>
  <order>dstip</order>
</decoder>

<decoder name="iptables-kv">
  <parent>iptables-kv</parent>
  <regex offset="after_parent"> LEN=(\d+)</regex>
  <order>len</order>
</decoder>

<decoder name="iptables-kv">
  <parent>iptables-kv</parent>
  <regex offset="after_parent"> TTL=(\d+)</regex>
  <order>ttl</order>
</decoder>

<decoder name="iptables-kv">
  <parent>iptables-kv</parent>
  <regex offset="after_parent"> SPT=(\d+)</regex>
  <order>srcport</order>
</decoder>

<decoder name="iptables-kv">
  <parent>iptables-kv</parent>
  <regex offset="after_parent"> DPT=(\d+)</regex>
  <order>dstport</order>
</decoder>

<decoder name="iptables-kv">
  <parent>iptables-kv</parent>
  <regex offset="after_parent"> WINDOW=(\d+)</regex>
  <order>window</order>
</decoder>
  • note constant field "in" skipped (identical in every line)
  • note constant field "out" skipped (identical in every line)
  • note constant field "mac" skipped (identical in every line)
  • note field "src" mapped to Wazuh conventional field "srcip"
  • note field "dst" mapped to Wazuh conventional field "dstip"
  • note constant field "proto" skipped (identical in every line)
  • note field "spt" mapped to Wazuh conventional field "srcport"
  • note field "dpt" mapped to Wazuh conventional field "dstport"
  • note kv fields are extracted by same-named sibling decoders (offset="after_parent"), so per-line field order/absence is tolerated — the shared name is what makes Wazuh evaluate every sibling
  • note decoder order and prematch specificity may need site-specific tuning (other decoders in your ruleset can shadow these) — validate with /var/ossec/bin/wazuh-logtest

rsyslog template / liblognorm rulebase

version=2
# iptables — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
#   module(load="mmnormalize")
#   action(type="mmnormalize" rulebase="/etc/rsyslog.d/iptables.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=iptables:IN=eth0 OUT= MAC=00:1a:2b:3c:4d:5e SRC=%src:ipv4% DST=%dst:ipv4% LEN=%len:number% TTL=%ttl:number% PROTO=TCP SPT=%spt:number% DPT=%dpt:number% WINDOW=%window:number% SYN
  • note kv structure: rsyslog offers mmfields (fast, fixed single-char separator, untyped) and mmnormalize (this rulebase, typed fields + literal anchors); mmnormalize was chosen for typed extraction
  • note trailing literal " SYN" reconstructed from line 1
  • note chosen parser types: src=ipv4, dst=ipv4, len=number, ttl=number, spt=number, dpt=number, window=number

FAQ

Why is OUT= (or IN=) empty in my iptables log line?
That is expected. For an inbound packet netfilter fills IN= with the ingress interface and leaves OUT= empty; for an outbound packet the reverse. So a bare 'OUT=' with no value is normal, not corruption — your parser must accept empty-valued keys rather than assuming every KEY= has a value.
What is the --log-prefix and why does it matter?
It is a literal string you attach to a LOG rule (e.g. '[UFW BLOCK] ') that the kernel prepends to every matching packet's log line. Since netfilter itself does not name the rule, the prefix is how you attribute a logged packet to a specific rule or policy — parse it as your rule/action label.
How are TCP flags represented in an iptables log?
As bare uppercase words (SYN, ACK, PSH, RST, FIN, URG) sitting among the KEY=VALUE tokens with no key of their own. A parser must collect these standalone flag words separately rather than expecting every token to be KEY=VALUE. A packet with only SYN set, repeated across many destination ports, is the signature of a port scan.
Where do iptables logs actually appear on disk?
They come from the kernel (netfilter LOG target), so they land wherever kernel messages go: dmesg, /var/log/kern.log (Debian/Ubuntu), /var/log/messages (RHEL), or the systemd journal. There is no dedicated iptables file; the log-prefix is what lets you filter your firewall lines out of the general kernel stream.

Try it on your own iptables / netfilter lines

Paste a few real lines, review the detected fields, and copy whichever format your stack needs. Free, no account, nothing uploaded.

Open this sample in LogForge →