0.2.1
Added
decision_log: [delivery: :inline], for tests: each sampled decision goes to the sink at once, in the process that made it, instead of in batches from the flusher. A sink writing through an Ecto sandbox then sees the test's connection, and tests can assert on what it wrote right after the request, and a sink that raises fails the test instead of logging. It puts the sink on the request path, so it is for tests only; the default,:batched, is unchanged. The testing guide has a new "Decision log sinks" section.
Fixed
- The formatter settings Limen exports (
import_deps: [:limen]) now includetrust/2, so applications' formatters no longer add parentheses totrustrules. A test keeps the export in step with the policy DSL.
0.2.0
Changes that came from running Limen in front of a real application. Several are breaking; "Upgrading from 0.1" below says what to change.
Added
- Facts:
Limen.put_facts/2(andLimen.LiveView.put_facts/2and the:factsoption ofLimen.Socket.check/3andLimen.Trap.check_form/3for sockets) state what the application knows about a request, such as whether the client is signed in. Policies read them withfact(:name), and every decision records them in its new:factsfield. trustrules:trust :signed_in, when: fact(:signed_in)allows a client before bans, limits, passes and every other rule, at the new:truststage.Limen.Plugchecks them for trap paths too, andLimen.Socket.check/3and theLimen.LiveViewhook take a:policywhose trust rules they check, so sockets agree with the HTTP gate.Limen.Plug's:policyalso takes:offor:track. The guides describe gating in a router pipeline, after the facts, with an endpoint plug (policy: :off) that only serves Limen's endpoints and trap paths.- Policy parameters:
use Limen.Policy, params: [name: default]andparam(:name)in conditions, score weights anddecide, overridden per instance with the new:paramsoption and at runtime. Decisions record the values used, including those of the matchingdecideclause in the new:clause_observedfield. Limen.update_config/3changes an option of a running instance, merging keyword options into their current values.- User agents: the new
:ai_crawlerand:link_previewfamilies, built-in lists of AI crawlers (after ai.robots.txt) and link preview services, a:ua_namesignal with the name of a known automated client, and a:user_agentsoption extending, moving and removing tokens per instance. - Substring lists:
Limen.Lists.put_substrings/3and{:substrings, [...]}in:lists, matching strings containing a member. Limen.Signal.Asn.Source: the:asn:sourceoption looks addresses up somewhere else than the iptoasn.com data, such as MaxMind databases the application already has.- The
:challenge:bindoption: what pass cookies and socket tokens are bound to, any of:prefix,:ja4and:user_agent(all three by default), for instance to keep passes across networks. - The
:challenge:pageoption and theLimen.Challenge.Pagebehaviour: the challenge page's texts per request, in the visitor's language, and its markup. - The maze's
:admitoption,{module, function, args}, refuses clients while the application is under load;Limen.Maze.held/1reads how many it holds;[:limen, :maze, :refused]telemetry reports refusals. Limen.DecisionLog.Sink: the:decision_log:sinkoption writes sampled decisions somewhere else thanLogger, in batches, from the flusher's process.Limen.Test:put_mode/2(enforce one request in an otherwise dry-run test instance),put_client_ip/2(the same client for the HTTP gate and LiveViewTest sockets),unique_ip/0,socket_params/2andput_socket_token/2, and a testing guide.
Changed
boundaryis no longer a dependency of applications: Limen's modules only declare their boundaries when its own build runs the boundary compiler.via_proxyin contexts and decisions is onlytruewhen the client address came from a forwarding header, not whenever the peer is a trusted proxy. Trusted peers are still trusted with the JA4 header.[:limen, :fcrdns, :resolved]metadata is flat::resultis:verified,:failedor:error, with:hostand:reasonalongside.- Several user agent markers moved from
:crawlerto the new families, andfacebookexternalhitis named"facebookexternalhit", not"facebook". Limen.Instance.put_configis replaced byLimen.update_config/3(andLimen.Instance.update_config/3), which merges instead of resetting a group to its defaults, and refuses options only read at startup.- The decision log's
:leveloption moved to its default sink. Limen.Decision.explain/1shows negative weights as-20, not+-20.
Fixed
Limen.Plugcould reject a valid policy, depending on compilation order, when initialised while the application compiled; it now waits for the policy to compile.Limen.LiveView.check_form/3recorded the socket's path (/live/websocket) instead of the page's.Limen.Telemetrydocumented[:limen, :fcrdns, :resolved]metadata the resolver did not send.
Upgrading from 0.1
- Runtime configuration. Replace
Limen.Instance.put_config(name, key, value)withLimen.update_config(name, key, value). Only the keys you pass change now: drop code that read the other keys back to pass them again. Changing an option read only at startup (seeLimen.Config) now raises instead of doing nothing. - Decision log level. Replace
decision_log: [level: :debug]withdecision_log: [sink: {Limen.DecisionLog.Logger, level: :debug}]. - User agent families. Rules comparing
signal(:ua_family)with:crawlerno longer match AI crawlers (GPTBot,ClaudeBot,CCBot,Bytespider,Amazonbotand others) or link previews (facebookexternalhit,Twitterbot,LinkedInBotand others); usein [:crawler, :ai_crawler, :link_preview]for all of them, or the new families on their own. To keep a token in its old family, list it under:crawlerin the:user_agentsoption. A custom:fcrdnscrawler keyed"facebook"needs the key"facebookexternalhit". - Crawler verification telemetry. Handlers matching
%{result: {:verified, host}}should match%{result: :verified, host: host}. via_proxy. Code readingdecision.identity.via_proxyto learn whether the peer was a proxy getsfalsefor a proxy's own requests and for unusable forwarding headers;evidence.client_ipstill tells how the address was found.boundary. An application that only hadboundaryfor Limen can drop the dependency.- Custom challenge pages.
Limen.Challenge.Page.render, with four arguments, is gone; seeLimen.Challenge.Pagefor pages of your own. - Skipping Limen for signed-in users. A plug that skips
Limen.Plug, or a wrapper around theLimen.LiveViewhook, can become a facts plug or hook and atrustrule, which leaves a decision behind; see the getting started guide.
0.1.0
First release.
- Instances: configured in the host application's environment
(
config :my_app, Limen) and started in its supervision tree, several per node if needed, each with its own state and selectable per plug and per route. Limen.Plug: the request gate, with per-route policies,:offand:trackroutes, dry-run and enforce modes, and an explainableLimen.Decisionfor every evaluated request.- Signals: client address behind trusted proxies, prefix aggregation, JA4 from the TLS terminator, HTTP shape against the claimed user agent, per-prefix behaviour, IP-to-ASN classification and FCrDNS crawler verification.
Limen.Policy: a compiled DSL withlimit,allow,deny,scoreanddecide, compile-time validation and per-rule explanations. Only the signals a policy refers to are collected.- Proof-of-work challenges with stateless tokens, a vendored solver, single-use tokens, pass cookies and a no-JavaScript fallback.
- IP-to-ASN data packed into a single
:persistent_termentry, about 10 MB for the full iptoasn.com data, and optionally downloaded and kept current from a URL on a configurable schedule: frequency, jitter, UTC time windows, and postponement while the node is busy. - Lock-free state: sliding windows, GCRA limits, bans, Count-Min Sketch, Bloom filters and HyperLogLog, all with bounded memory.
- LiveView and socket gating, cluster ban propagation and a LiveDashboard page.
- Honeypots (
Limen.Trap): trap paths behind hidden links androbots.txt, and form traps with a decoy field and a signed timestamp, flagging the clients that fall in. - The maze (
Limen.Maze): endless pages written by a Markov chain model of a bundled corpus and, optionally, the site's own text, seeded per site from the instance secret and dripped slowly with random chunks and pauses. Bans carry an action (:denyor:maze), and policies can send clients to the maze withmazerules or{:maze, ban: seconds}.