Skip to content

Stateful Firewalls & Connection State

A stateful firewall tracks each permitted connection rather than judging every packet in isolation. This page covers the state table, the direction rules have to be written in, and why changing a rule often appears to do nothing. It underpins Firewall Rules & Aliases and the per-interface rule model described in VLAN Basics in pfSense.

What State Means

  • When a packet is evaluated against the rule set and matches a pass rule, the firewall creates a state table entry recording the connection: interface, protocol, source and destination address, source and destination port, and the connection's current phase.
  • Every subsequent packet belonging to that connection — in either direction — is checked against the state table first.
  • A packet matching an existing state is passed immediately. It is never evaluated against the rule set again.
  • Only packets with no matching state fall through to the rules.

The rule set therefore only ever sees the first packet of a connection. Everything after that is a table lookup, which is also why stateful filtering is fast: one lookup is far cheaper than walking a long ordered rule list for every packet.

pfSense is built on the FreeBSD pf packet filter, which is stateful by default — a pass rule keeps state unless it is explicitly told not to. Individual rules override this under Advanced Options (revealed by Display Advanced in the rule editor), where State Type offers:

Option Behaviour
Keep Normal state tracking. The default, and correct for almost every rule
Sloppy State Relaxed sequence checking, for asymmetric paths where the firewall sees only one direction
Synproxy The firewall completes the TCP handshake itself before opening one to the destination, absorbing SYN floods
None No state is created. On interface-tab rules this affects inbound packets only, so it needs a matching floating rule outbound to be useful

Rules Follow the Direction of Initiation

Rules are only ever written for the direction in which a connection is initiated. In pfSense, rules on interface tabs are applied inbound on that interface.

  • Blocking segment A from initiating connections to segment B does not stop segment B initiating connections to segment A.
  • It also does not break the replies to connections that B started, because those replies match B's existing states and bypass the rule set entirely.
  • A rule governing traffic sourced from segment A therefore belongs on segment A's interface tab, not on the tab of the segment being reached.

Worked Illustration

A client on one segment opens a TCP session to a service hosted on another — for example a management interface on a wireless controller.

  • The client's SYN arrives inbound on the client segment's interface, matches a pass rule there, and a state is created.
  • The controller's SYN/ACK arrives inbound on the infrastructure segment's interface. It matches the existing state and is passed without the infrastructure segment's rules being consulted at all.
  • A block rule on the infrastructure segment preventing it from initiating connections towards the client segment therefore has no effect on this session.

An isolation rule stops a segment reaching out; it does not stop it answering. A segment can therefore be prevented from initiating anything towards the rest of the network while still serving every client permitted to reach it.

State Entries Persist Across Rule Changes

Changing or removing a rule does not terminate connections already established under the old rule.

  • An established session continues until its state expires on idle, is cleared manually, or one end closes the connection.
  • Rule changes only affect the next connection attempt, because only new connections are evaluated against rules.
  • Long-lived sessions — SSH, database connections, management and control channels — can survive for hours after the rule that permitted them has gone.

Common symptom: a rule is changed to block something, the traffic carries on regardless, and the rule looks broken. The rule is usually fine; the traffic belongs to a state that pre-dates it.

pfSense exposes the state table under Diagnostics → States, filterable by address or port, with a Reset States tab alongside it offering separate State Table and Source Tracking checkboxes. Confirm the tab labels on screen — Netgate's published documentation lags the shipping GUI.

  • Killing a single state forces the next packet of that connection back through the rule set, where the new rules apply. For TCP this normally drops the session.
  • Flushing the whole table drops every tracked connection at once. Everything re-evaluates cleanly, but every session in progress is interrupted, including the administrative session doing the flushing.
  • States are also flushed on reboot. When a dynamic WAN address changes, states tied to the old address are killed automatically; an option under System → Advanced (in the networking settings — confirm the label on screen) widens that to resetting the entire table.

From the pfSense shell (Diagnostics → Command Prompt, or an SSH session — see Enabling SSH):

pfctl -s state     # list current state table entries
pfctl -si          # state table counters (searches, inserts, removals)
pfctl -sm          # configured limits, including the state table maximum
pfctl -F state     # flush every state — disruptive, drops all sessions

TCP, UDP and ICMP

Protocol How state is derived Typical lifetime
TCP Follows the real handshake and teardown — the firewall tracks which phase the connection is in, plus sequence numbers Long once established; short during setup and close
UDP Connectionless, so a pseudo-state is inferred from the first outbound datagram Short idle timer, extended once traffic is seen in both directions
ICMP An echo request creates a state keyed on the ICMP identifier; the matching echo reply consumes it Seconds
  • TCP state is genuine protocol state. The firewall knows whether a connection is being established, is established, or is closing, and can drop packets that do not fit the expected sequence. See TCP vs UDP & Common Ports for the handshake and teardown themselves.
  • UDP has no concept of a connection, so the firewall invents one. The first outbound datagram creates a state, and datagrams returning from the same address and port pair match it.
  • That UDP state expires on a timer. A reply arriving after its state has expired has nothing to match, falls through to the rule set, and is dropped by the default deny rule — appearing in the firewall log as a block on the reply itself. This is a routine cause of confusing log entries for slow-responding DNS, NTP and SNMP servers, rather than evidence of a broken rule.
  • ICMP echo is handled the same way, which is why a ping reply returns without any explicit rule permitting it. Only the outbound echo request needs to be allowed.

Where a logged block needs explaining, capture the traffic on both interfaces and compare the timing against the state's lifetime — see Packet Capture Method.

Stateless Filtering and the Cisco Equivalents

A stateless packet filter evaluates every packet independently against an ordered list, with no memory of what came before.

  • Permitting a client to reach an external service requires two entries: one for the outbound request, one for the inbound reply.
  • The return entry has to be written broadly — typically "any high port back towards the client" — because the reply's source port is not known in advance. That is a far wider opening than a state entry, which is pinned to one exact address/port/protocol combination.

Cisco IOS ACLs, as used for VLAN isolation in Access Control Lists, are stateless. Two long-standing partial workarounds exist:

  • The established keyword on an extended TCP ACL matches packets whose ACK or RST flags are set — packets that claim to belong to an existing session. It is TCP-only and inspects flags rather than tracking a connection, so it is straightforward to spoof.
  • Reflexive ACLs go further: an outbound entry marked reflect dynamically creates a temporary mirrored inbound entry, which an inbound ACL then references with evaluate. This is real, if limited, state tracking on the router.

From Cisco IOS global configuration mode (conf t) — see Cisco IOS CLI Reference:

! Stateless approximation: permit only TCP packets that look like replies
ip access-list extended INBOUND_FILTER
 permit tcp any 10.0.10.0 0.0.0.255 established
 deny   ip any any log

Reflexive ACLs are a router feature and are generally unavailable on Catalyst switch platforms; established is supported in extended ACLs but remains a flag check, not state tracking. Genuine state inspection on the Cisco side comes from router-class platforms running a zone-based firewall, which is the usual reason segment-to-segment policy is enforced at the firewall rather than the switch.

The State Table Is a Finite Resource

  • Each state consumes memory, and the firewall enforces a maximum number of them.
  • Firewall Maximum States under System → Advanced → Firewall & NAT defaults to a figure calculated from roughly 10% of installed RAM. The same page carries Firewall Optimization OptionsNormal, High Latency, Aggressive, Conservative — which set the timeout profile governing how quickly idle states are reaped.
  • When the table fills, new connections are refused while existing ones keep working — presenting as a network that is plainly up but will not open anything new.
  • High state counts come from many simultaneous connections, not from high throughput. One saturated file transfer is a single state; a host scanning a subnet creates thousands.
  • NAT translations are tracked in the same table, so port forwards and outbound NAT sessions count towards the total — see NAT & Private IP Ranges.
  • Current usage against that limit appears on the dashboard's System Information widget as State table size, or via pfctl -si from the pfSense shell. Diagnostics → States Summary instead breaks the table down by source, destination and address pair — useful for finding the host consuming the states, though it is slow to render on a large table.