Storage Configuration¶
Storage defines how the firewall persists blocked IP addresses across requests.
Available Storage Classes¶
1. In-Memory Storage¶
Non-persistent storage that resets with each request. Useful for testing.
2. File Storage¶
Persists blocked IPs to the filesystem.
storage:
type: "Kanopi\\Firewall\\Storage\\FileStorage"
config:
storage_file: /var/log/firewall/blocked_ips.data
offense_file: /var/log/firewall/blocked_ip_offenses.data
3. Database Storage¶
Stores blocked IPs in a SQL database using Doctrine DBAL.
storage:
type: "Kanopi\\Firewall\\Storage\\DatabaseStorage"
config:
storage_table: firewall_blocked_ips
offenses_table: firewall_blocked_ip_offenses
connection:
# Option 1: Using DSN (recommended)
dsn: "mysql://user:password@localhost:3306/database?serverVersion=8.0"
# Option 2: Individual parameters
# dbname: 'my_database'
# user: 'db_user'
# password: 'db_password'
# host: 'localhost'
# port: 3306
# driver: 'pdo_mysql'
Searching and Un-blocking¶
StorageInterface gives you keyed access — get(), set(), delete() for an address you already know. That covers the firewall's own hot path, but it leaves two operational questions unanswered: who is currently blocked?, and how do I lift a block that should not have been applied?
Storages that can answer those implement Kanopi\Firewall\Storage\QueryableStorageInterface, which adds two methods:
| Method | Purpose |
|---|---|
find(string $pattern): array | Records matching a single address or a CIDR range, keyed by address |
deleteMatching(array $patterns): int | Delete everything matching any of the given addresses / ranges; returns the count |
All three shipped storages implement it. FileStorage inherits the behaviour from InMemoryStorage.
use Kanopi\Firewall\Storage\QueryableStorageInterface;
use Kanopi\Firewall\Storage\StorageFactory;
$storage = StorageFactory::create($config);
if ($storage instanceof QueryableStorageInterface) {
// Who is blocked in this range, and why?
foreach ($storage->find('203.0.113.0/24') as $address => $record) {
printf(
"%s — expires %s, %d offense(s)\n",
$address,
$record['expires_at'] ?? 'never',
$record['offenses']
);
}
// Lift a block that should not have been applied.
$lifted = $storage->deleteMatching(['203.0.113.5', '198.51.100.0/24']);
}
Why a separate interface¶
Not every backend can enumerate its own keys — Memcached, the worked example in Custom Storage Backends, cannot list keys at all. Folding these methods into StorageInterface would oblige every implementation to supply something it may be unable to implement honestly, and would break existing custom storages on upgrade. Enumeration is a capability, so it is modelled as one, and callers check with instanceof before using it.
Behaviour worth knowing¶
- Both IPv4 and IPv6 ranges are supported:
203.0.113.0/24,2001:db8::/32. - A malformed pattern matches nothing, never everything. An out-of-range prefix such as
/33on IPv4 is treated as invalid rather than silently clamped to a single host — otherwise you would clear one record believing you had cleared a range. - One bad pattern does not abort the rest. Invalid entries are skipped and logged, so a typo in one of twenty ranges still lifts the other nineteen. The return count tells you what actually happened.
find()hides expired records so you are not shown a block that lapsed an hour ago, butdeleteMatching()still removes them — otherwise an un-block would report nothing matched while the row was still on disk.- Offense history is cleared alongside the block. Left behind,
blocking_escalationwould escalate the address straight back to a longer ban on its next request, and the un-block would appear not to have worked.