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>
<!-- ============================================================
ALERT RULE (starter) — put this in a RULES file, e.g.
/var/ossec/etc/rules/local_rules.xml. Decoders and rules live
in SEPARATE files. The rule matches the decoder above through
<decoded_as>; set <level> and add <field>/<match> conditions so
it alerts only on the events you care about. Rule ids 100000+
are the user range — change them if they collide with yours.
============================================================ -->
<group name="iptables,">
<rule id="100000" level="3">
<decoded_as>iptables-kv</decoded_as>
<description>iptables: srcip=$(srcip) dstip=$(dstip)</description>
</rule>
<!-- Example — a higher-level alert gated on one field (uncomment and edit):
<rule id="100001" level="10">
<if_sid>100000</if_sid>
<field name="srcip">^CHANGE_ME$</field>
<description>iptables: a value you care about from $(srcip)</description>
</rule>
-->
</group>
- 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 added a starter alert <rule> (level 3, matched to the decoder via <decoded_as>) — put it in a RULES file (not the decoders file), set the level, and add <field>/<match> conditions; the commented example child rule shows the pattern
- 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
Wazuh's OS_Regex is not PCRE — a bare . is a literal dot and \. matches any character.
Test Wazuh OS_Regex patterns →
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
Splunk
# props.conf (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = (?=.*?(?<![\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+)?)).*
# Quick search-time test in SPL:
# | rex field=_raw "(?=.*?(?<![\\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 regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
- note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype
ES ingest
PUT _ingest/pipeline/iptables
{
"description": "LogForge-generated ingest pipeline for iptables",
"processors": [
{
"kv": {
"field": "message",
"field_split": " ",
"value_split": "="
}
}
]
} - note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
- note grok: constant field "mac" embedded as literal anchor "00:1a:2b:3c:4d:5e" (varying=false)
- note grok: 1 optional field(s) wrapped in (?:…)? inline regex — grok has no native optional syntax
- note kv structure: emitted a { kv: { field: "message", field_split: " ", value_split: "=" } } processor — it extracts key=value pairs natively; adjust field_split/value_split if your delimiter differs
- note a grok alternative is available too (the reused grok pattern is shown in the notes above); prefer the kv processor unless you need typed/anchored extraction
- note test in Kibana Dev Tools with: POST _ingest/pipeline/iptables/_simulate (supply a docs[] array whose _source.message holds a sample line)
Graylog
# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "iptables-parse"
when
has_field("message")
then
let gp = grok(pattern: "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}", value: to_string($message.message), only_named_captures: true);
set_fields(gp);
end
# --- Graylog import-ready extractor JSON (secondary) ---
# Save as a .json file and import under System > Inputs > (input) > Manage extractors > Actions > Import extractors.
{
"extractors": [
{
"title": "iptables",
"extractor_type": "grok",
"converters": [],
"order": 0,
"cursor_strategy": "copy",
"source_field": "message",
"target_field": "",
"extractor_config": {
"grok_pattern": "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}",
"named_captures_only": true
},
"condition_type": "none",
"condition_value": ""
}
],
"version": "5.0.0"
} - note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
- note grok: constant field "mac" embedded as literal anchor "00:1a:2b:3c:4d:5e" (varying=false)
- note grok: 1 optional field(s) wrapped in (?:…)? inline regex — grok has no native optional syntax
- note primary artifact is the processing-pipeline rule; the extractor JSON is an equivalent import-ready alternative for the classic extractor UI
Datadog
logforge_rule 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=%{integer:spt} DPT=%{integer:dpt} WINDOW=%{number:window} - note emitted rule name is "logforge_rule"; rename it to match your "iptables" convention if desired
- note kv input — consider Datadog's key-value/`keyvalue()` filter in the Grok Parser instead of anchoring each key by hand
- note constant field "mac" embedded as literal anchor "00:1a:2b:3c:4d:5e" (varying=false)
- note paste this line into a Grok Parser processor in a Datadog Log Pipeline; matchers are anchored left-to-right and rule whitespace matches log whitespace. Complex or multi-shape logs may need Helper Rules.
Fluent Bit
[PARSER]
Name iptables
Format regex
Regex ^(?=.*?(?<![\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+)?)).*$
# Time_Key <name of a timestamp capture group, if any>
# Time_Format <strptime format, e.g. %Y-%m-%dT%H:%M:%S>
# Fluentd <parse> block:
# <parse>
# @type regexp
# expression /^(?=.*?(?<![\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+)?)).*$/
# </parse>
- note regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
Vector
[transforms.iptables_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. = parse_key_value!(.message)
''' - note the reused regex needs lookahead, which the Rust regex crate behind parse_regex rejects; used parse_key_value! on .message (also the idiomatic Vector parser for key=value logs)
Loki
# promtail pipeline for "iptables" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
# scrape_configs:
# - job_name: iptables
# pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
# NOTE: the extraction regex below uses lookahead, which Go RE2
# (promtail's regex engine) CANNOT compile. It is commented out.
# - regex:
# expression: '^(?=.*?(?<![\w.\-])IN=eth0)(?=.*?(?<![\w.\-])OUT=|)(?=.*?(?<![\w.\-])MAC=00:1a:2b:3c:4d:5e)(?=.*?(?<![\w.\-])SRC=(?P<src>\d{1,3}(?:\.\d{1,3}){3}))(?=.*?(?<![\w.\-])DST=(?P<dst>\d{1,3}(?:\.\d{1,3}){3}))(?=.*?(?<![\w.\-])LEN=(?P<len>-?\d+(?:\.\d+)?))(?=.*?(?<![\w.\-])TTL=(?P<ttl>-?\d+(?:\.\d+)?))(?=.*?(?<![\w.\-])PROTO=TCP)(?=.*?(?<![\w.\-])SPT=(?P<spt>\d{1,5}))(?=.*?(?<![\w.\-])DPT=(?P<dpt>\d{1,5}))(?=.*?(?<![\w.\-])WINDOW=(?P<window>-?\d+(?:\.\d+)?)).*$'
# Use a `- logfmt:` stage instead for key=value logs, e.g.:
# - logfmt:
# mapping:
# src: 'SRC'
# dst: 'DST'
# len: 'LEN'
# ttl: 'TTL'
# spt: 'SPT'
# dpt: 'DPT'
# window: 'WINDOW'
- note regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
- note kv input: the reused regex needs lookahead (Go RE2 cannot do it) because key order varies per line — use a `- logfmt:` stage (shown commented) which parses key=value pairs order-independently, then a `- labels:` stage for the low-cardinality keys
syslog-ng
parser p_iptables {
regexp-parser(
prefix(".iptables.")
patterns("(?=.*?(?<![\\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 regex: a single linear template could not reproduce every input line — fields are captured with order-independent lookaheads instead
- note captured fields are stored as name-value pairs under the prefix ".iptables." (e.g. a group (?<srcip>…) becomes ".iptables.srcip")
- note kv structure: syslog-ng has a dedicated kv-parser() that is simpler and more robust than regexp-parser for key=value logs — consider kv-parser(prefix(".logforge.")) instead of the emitted regexp-parser
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 →