Repository navigation
Conversation
Fail2Ban (and Allow2Ban) now store the ban expiration time as the value of the ban key instead of 1, so the remaining ban time can be read with plain cache reads on every supported store, including memcached, without a TTL API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
There is no way to know when a
Fail2Ban/Allow2Banban ends. An API that wants to send an accurateRetry-Afterheader for a long ban (e.g.Allow2Banwithbantime: 1.hour) can only send the constantbantime. A client that is told this late in the ban waits much longer than it needs to.#170 proposed
banned_untilbased on the cache TTL. It was closed because memcached cannot read a key's TTL. This PR does not use TTL at all.Change
ban!writes the ban expiration time (Time.now.to_f + bantime.to_f) as the value of the ban key instead of1. The key name, the TTL (bantime), the counters and thefilter/banned?/resetbehavior are unchanged.Fail2Ban.retry_after(discriminator), inherited byAllow2Ban. It reads the key with the regularcache.readand returns:1while the key still exists);nilif the discriminator is not banned (no key, expired, orreset);nilfor bans written by previous versions (value1), whose expiration time is unknown.banned?is stilltruefor them.filterreturns early onbanned?as before.retry_afterwith ablocklisted_responderexample that setsRetry-After.Store portability and compatibility
cache.read/cache.writeare used, so this works on every supported store: MemoryStore,Dalli::Client(plain andConnectionPool),ActiveSupport::Cache::MemCacheStore,Redis,Redis::StoreandActiveSupport::Cache::RedisCacheStore. Raw stores return the value as a String (Float#to_sround-trips exactly), andretry_afterreads both forms.cache.read(key) ? true : false, and the new value is truthy. New code treats an old1/"1"as banned with an unknown expiration time.retry_afterby the same amount.Dalli::Clientat all (Fix Fail2Ban and Allow2Ban not banning with dalli 4 and 5 #721). That PR'svalue.to_sfix also covers the Float written here. Instrument fail2ban and allow2ban bans #720 (a notification when a ban is created) is separate as well.Tests
spec/fail2ban_retry_after_spec.rb, run for bothFail2BanandAllow2Banon MemoryStore with Timecop. It covers: the stored value, the exact remaining time as time moves, partial seconds rounded up, no extension on further requests,nilafter expiry and afterreset, String values from raw stores, the minimum of 1, and legacy1/"1".spec/support/cache_store_helper.rb) now checkretry_afterafter a ban for both strategies. They also run a real-clock "expires bans" case (bantime: 1, then sleep), so every real backend covers write → read → expiry.1now expect a future timestamp.bundle exec rake testpasses for every appraisal gemfile on Ruby 4.0.7 (16 gemfiles) and on Ruby 3.4.8 (the 14 gemfiles CI runs on 3.4). On dalli3 the memcached specs (Dalli::Client,ConnectionPool+ Dalli,MemCacheStore) actually ran against the memcached server.rubocopis clean onlibandspec.maintoo, because of Fix Fail2Ban and Allow2Ban not banning with dalli 4 and 5 #721. With Fix Fail2Ban and Allow2Ban not banning with dalli 4 and 5 #721's one-line fix applied locally, they all pass with this change.🤖 Generated with Claude Code