OPNsenseXXXIII · Logging and Remote LoggingLog reading
Firewall log reading — what each filterlog line tells you
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
- Logging architecture on OPNsense — where the logs come from and where they go
- 11-firewall-rules-fundamentals
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
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.
$ 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:
| # | Value | Field | Meaning |
|---|---|---|---|
| 1 | 7 | Rule number | Position of the matching rule in the loaded ruleset |
| 2 | Sub-rule number | Set for anchored rules; empty at the top level | |
| 3 | Anchor | Anchor (ruleset) name; empty at the top level | |
| 4 | 02f4…c131 | Rule id | Label that maps the record back to a configured rule; 0 when no label is available |
| 5 | igb0 | Interface | Interface the packet was logged on |
| 6 | match | Reason | Why the packet was logged |
| 7 | block | Action | What pf did with the packet |
| 8 | in | Direction | in, out, or in/out |
| 9 | 4 | IP version | 4 or 6 |
For IPv4, fields 10 to 20 carry the IP header:
| # | Value | Field |
|---|---|---|
| 10 | 0x0 | TOS |
| 11 | ECN | |
| 12 | 59 | TTL |
| 13 | 54088 | IP id |
| 14 | 0 | Fragment offset |
| 15 | DF | IP flags |
| 16 | 6 | Protocol number |
| 17 | tcp | Protocol name |
| 18 | 60 | Total length |
| 19 | 198.51.100.23 | Source address |
| 20 | 192.0.2.10 | Destination address |
For TCP, fields 21 onwards carry the transport header:
| # | Value | Field |
|---|---|---|
| 21 | 40254 | Source port |
| 22 | 3389 | Destination port |
| 23 | 0 | Data length |
| 24 | S | TCP flags |
| 25 | 3963160055 | Sequence number |
| 26 | Acknowledgement number (only when the ACK flag is set) | |
| 27 | 64240 | Window |
| 28 | Urgent pointer (only when the URG flag is set) | |
| 29 | mss;sackOK;TS;nop;wscale | TCP 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?
$ 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.
$ 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.
$ 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, withlatest.logpointing 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.
matchis 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
Q1. A filterlog record shows reason=match, action=block, rule number 12. What does this record mean?
Q2. The rule id in field 4 is a more durable way to identify the matching rule than the rule number in field 1.
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.