Skip to main content
RunBook Academy

OPNsenseXXXIII · Logging and Remote LoggingLog reading

Firewall log reading — what each filterlog line tells you

Intermediate⏱ ~14 minconfigctlpfctlgrepawk

What you'll learn

  • Parse an OPNsense filterlog line and identify every field by position
  • Distinguish the reason field from the action field
  • Use the rule id field to identify the rule that matched
  • Build the aggregation pattern that turns log floods into incident timelines

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-18

Not yet marked complete on this device.

The firewall log is the most-queried log on OPNsense. Every operator reads it; few read it well. It is one line per logged packet, written by the filterlog daemon, which reads pflog0 and emits a comma-separated record through syslog-ng into /var/log/filter/filter_<YYYYMMDD>.log. Knowing the record layout — every field, what it means, what is normal — is the difference between “the firewall blocked something” and “the firewall blocked an inbound RDP connection attempt from 198.51.100.23 to 192.0.2.10:3389 at 16:25:31, on rule 7 of the ruleset”.

Where the lines live

The filter log is a plain text file. cat, tail, grep, and awk all work on it directly. latest.log is a symlink to the newest daily file, maintained by the log rotation job.

Read-only / Safefilter log directory and two blocked packets
$ ls -l /var/log/filter/ | tail -4; echo '---'; grep ',block,in,' /var/log/filter/latest.log | tail -2
-rw-------  1 root  wheel  18726144 Aug 12 23:59 filter_20260812.log
-rw-------  1 root  wheel  21004288 Aug 13 23:59 filter_20260813.log
-rw-------  1 root  wheel   9437184 Aug 14 16:31 filter_20260814.log
lrwxr-xr-x  1 root  wheel        33 Aug 14 00:00 latest.log -> /var/log/filter/filter_20260814.log
---
<134>1 2026-08-14T16:25:31+00:00 opnsense.example.com filterlog 44481 - [meta sequenceId="8180"] 7,,,02f4bab031b57d1e30553ce08e0ec131,igb0,match,block,in,4,0x0,,59,54088,0,DF,6,tcp,60,198.51.100.23,192.0.2.10,40254,3389,0,S,3963160055,,64240,,mss;sackOK;TS;nop;wscale
<134>1 2026-08-14T16:25:32+00:00 opnsense.example.com filterlog 44481 - [meta sequenceId="8181"] 7,,,02f4bab031b57d1e30553ce08e0ec131,igb0,match,block,in,4,0x0,,59,54089,0,DF,6,tcp,60,198.51.100.23,192.0.2.10,40255,3389,0,S,3963160056,,64240,,mss;sackOK;TS;nop;wscale

Illustrative output

The record layout

The payload layout is positional and depends on the IP version and the protocol. The first nine fields are common to every record; IPv4 and IPv6 then diverge; the transport protocol adds its own tail.

Taking the first line above, field by field:

#ValueFieldMeaning
17Rule numberPosition of the matching rule in the loaded ruleset
2Sub-rule numberSet for anchored rules; empty at the top level
3AnchorAnchor (ruleset) name; empty at the top level
402f4…c131Rule idLabel that maps the record back to a configured rule; 0 when no label is available
5igb0InterfaceInterface the packet was logged on
6matchReasonWhy the packet was logged
7blockActionWhat pf did with the packet
8inDirectionin, out, or in/out
94IP version4 or 6

For IPv4, fields 10 to 20 carry the IP header:

#ValueField
100x0TOS
11ECN
1259TTL
1354088IP id
140Fragment offset
15DFIP flags
166Protocol number
17tcpProtocol name
1860Total length
19198.51.100.23Source address
20192.0.2.10Destination address

For TCP, fields 21 onwards carry the transport header:

#ValueField
2140254Source port
223389Destination port
230Data length
24STCP flags
253963160055Sequence number
26Acknowledgement number (only when the ACK flag is set)
2764240Window
28Urgent pointer (only when the URG flag is set)
29mss;sackOK;TS;nop;wscaleTCP options

UDP stops after field 23 (source port, destination port, data length). CARP replaces the transport tail with type, TTL, VHID, version, advskew, and advbase.

Reason is not action

Two adjacent fields are routinely conflated. They answer different questions.

Reason (field 6) is why the packet reached the log at all. match means it matched a rule that carries the log keyword. The other values are pf dropping the packet for a reason that is not a rule decision: bad-offset, fragment, short, normalize, memory, bad-timestamp, congestion, ip-option, proto-cksum, state-mismatch, state-insert, state-limit, src-limit, synproxy.

Action (field 7) is what pf did. filterlog emits pass, block, scrub, nat, binat, rdr, or synproxy-drop. There is no match action — match only ever appears in the reason field.

So reason=match, action=block is the ordinary case an operator sees all day: a rule matched, and that rule blocks. reason=state-limit, action=block is a different animal entirely — pf dropped the packet because a state limit was hit, and no rule made that decision. The first is policy working; the second is a resource ceiling that wants investigating.

Reading the aggregation pattern

A log line is a single packet. An incident is many packets. The pattern is to aggregate over a field:

  • Top sources: which addresses generate the most blocked packets? Usually a scan, a brute-force, or a misconfigured host.
  • Top destinations: which addresses are being aimed at? Often the firewall itself, a DMZ host, or a service with an exposed port.
  • Top destination ports: 22, 80, 443, 3389, 25 — the noisy ports.
  • Time pattern: when did it start, and is it constant, growing, or bursty?
Read-only / Safetop destinations of inbound IPv4 blocks
$ grep ',block,in,4,' /var/log/filter/latest.log | awk -F, '{print $20}' | sort | uniq -c | sort -rn | head -5
  1234 192.0.2.10
 567 192.0.2.5
 234 192.0.2.1
  89 192.0.2.42
  12 192.0.2.99

Illustrative output

Swap the field index to change the question: $19 for top sources, $22 for top destination ports, $5 for the busiest interface. The grep that pins the IP version stays.

For anything more than a one-liner, read the log through configd instead. It returns one JSON object per record with named keys, so identifying a rule, an interface, an address, or a port no longer depends on counting commas.

Read-only / Safeparsed filter log records
$ configctl filter read log 3
[{"rulenr":"7","subrulenr":"","anchorname":"","rid":"02f4bab031b57d1e30553ce08e0ec131","interface":"igb0","reason":"match","action":"block","dir":"in","ipversion":"4","tos":"0x0","ecn":"","ttl":"59","id":"54088","offset":"0","ipflags":"DF","protonum":"6","protoname":"tcp","length":"60","src":"198.51.100.23","dst":"192.0.2.10","srcport":"40254","dstport":"3389","datalen":"0","label":"Default deny / state violation rule","__timestamp__":"2026-08-14T16:25:31","__host__":"opnsense.example.com"}]

Illustrative output

Matching the rule

Two fields point back at the ruleset, and they are not equally useful.

Field 4, the rule id, is the reliable one. It is a label OPNsense attaches to the generated rule, and configd resolves it to the rule description. That is why the GUI can show a rule name beside a log line. It survives ruleset regeneration because it is derived from the rule itself, not from its position.

Field 1, the rule number, is the position in the loaded ruleset at the moment the packet was logged. It is useful for a live lookup with pfctl, but it shifts whenever rules are added or removed above it, so a rule number recorded in yesterday’s log may point at a different rule today.

Read-only / Safepfctl rule lookup by number
$ pfctl -vvsr | grep -A2 '^@7 '
@7 block drop in log quick on igb0 proto tcp from any to 192.0.2.10 port = 3389 label "02f4bab031b57d1e30553ce08e0ec131"
[ Evaluations: 184213   Packets: 2136    Bytes: 128160    States: 0     ]
[ Inserted: uid 0 pid 41232 State Creations: 0     ]

Illustrative output

Common patterns

Port scan: one source, one destination, sequential destination ports. Counter: rate-limit, or drop the source upstream.

Brute force: one source, one destination, the same destination port repeated. Counter: key-only authentication, an allow-list, or an automatic block list.

Return traffic dropped: outbound requests logged as passed, inbound replies logged as blocked with reason=state-mismatch. Counter: check for asymmetric routing, and check whether a state was expired or flushed mid-connection.

Configuration error: a host that should reach a service cannot, and the log shows a block whose rule id resolves to a deny rule sitting above the intended allow rule. Counter: reorder the rules.

Summary

  • Filter log records are plain text under /var/log/filter/, one file per day, with latest.log pointing at the current one.
  • The payload is positional. The first nine fields are common; IPv4 and IPv6 diverge after that, so field numbers are only meaningful once the IP version is pinned.
  • Reason says why the packet was logged; action says what pf did. match is a reason, never an action.
  • Field 4, the rule id, is the durable pointer back to a rule. Field 1, the rule number, is only a position.
  • Aggregate by field for incidents, or read parsed records with configctl filter read log.
  • NTP discipline matters. Wrong clock, wrong correlation.

Knowledge check · 3 questions

  1. Q1. A filterlog record shows reason=match, action=block, rule number 12. What does this record mean?

  2. Q2. The rule id in field 4 is a more durable way to identify the matching rule than the rule number in field 1.

  3. Q3. Which of the following appear as fields in an IPv4 TCP filterlog record? Select all that apply.

Passing score: 75%. Answers are checked in this browser.