Native Elixir L7 bot protection.
Limen (Latin): threshold. The point every request crosses before it enters.
Limen classifies requests, rate-limits, challenges and blocks automated
traffic entirely inside the BEAM: no sidecar, no external service, all state
in ETS, :atomics and :persistent_term. Nothing on the request path calls
a process or sends a message.
Instances
Limen runs as one or more instances, each with its own configuration and state, started in your supervision tree. Configuration lives in your application's environment:
# config/runtime.exs
config :my_app, Limen, mode: :dry_run
# application.ex
children = [{Limen, otp_app: :my_app}, MyAppWeb.Endpoint]
# endpoint
plug Limen.Plug, otp_app: :my_appThat starts one instance, named after the application. To run several,
list them under :instances; top-level options are shared by all of them:
config :my_app, Limen,
mode: :dry_run,
instances: [public: [], admin: [mode: :enforce]]
plug Limen.Plug, instance: :publicInstance names are node-wide. An instance can also be started from options alone, which is how tests get isolated instances:
start_supervised!({Limen, name: :my_test, config: [mode: :enforce]})The request path finds its instance with a single :persistent_term read.
Start with Limen.Plug and the options in Limen.Config.
Summary
Functions
Bans a client for ttl seconds.
Returns the active ban covering target, if any.
Returns the decision Limen made for conn, if it went through Limen.Plug.
States facts about the request that policies can use, such as whether the client is signed in.
Switches an instance between :dry_run and :enforce at runtime.
Starts the instances configured for :otp_app, or a single instance from
:name and :config.
Lifts a ban. Accepts the same targets as ban/4.
Changes an option of a running instance.
Types
@type instance() :: atom()
Functions
@spec ban( instance(), :inet.ip_address() | String.t() | Limen.IP.prefix(), pos_integer(), keyword() ) :: :ok | {:error, :full}
Bans a client for ttl seconds.
target is an address (tuple or string), which is aggregated to its prefix
like any request would be, or a prefix in CIDR notation whose length matches
the instance's aggregation (:ipv4_prefix or :ipv6_prefix). To block
arbitrary ranges, use a policy rule with a list instead.
Options are those of Limen.State.BanList.ban/4; :origin defaults to
:admin. Pass action: :maze to send the client to the maze instead of
denying it:
Limen.ban(:my_app, "203.0.113.7", 86_400, action: :maze, reason: :scraper)
@spec banned(instance(), :inet.ip_address() | String.t() | Limen.IP.prefix()) :: Limen.State.BanList.ban() | nil
Returns the active ban covering target, if any.
@spec decision(Plug.Conn.t()) :: Limen.Decision.t() | nil
Returns the decision Limen made for conn, if it went through Limen.Plug.
@spec put_facts(Plug.Conn.t(), map() | keyword()) :: Plug.Conn.t()
States facts about the request that policies can use, such as whether the client is signed in.
Limen knows the request, not your application's users. Facts fill that
gap: a plug that runs before Limen.Plug states them, and policies read
them with fact/1 like any other value, so what they change shows up in
every decision:
# router.ex, after the session is fetched
plug :limen_facts
plug Limen.Plug, otp_app: :my_app, policy: MyApp.BotPolicy
defp limen_facts(conn, _opts) do
Limen.put_facts(conn, signed_in: get_session(conn, :user_token) != nil)
end
# the policy
trust :signed_in, when: fact(:signed_in)Facts are merged into those already stated, and recorded in the
decision. They are only as trustworthy as the code stating them: derive
them from what your application verified, never from what the client
sent. For sockets, see Limen.LiveView.put_facts/2 and the :facts
option of Limen.Socket.check/3.
@spec set_mode(instance(), :dry_run | :enforce) :: :ok
Switches an instance between :dry_run and :enforce at runtime.
Routes and plugs with an explicit :mode keep theirs.
@spec start_link(keyword()) :: Supervisor.on_start()
Starts the instances configured for :otp_app, or a single instance from
:name and :config.
@spec unban(instance(), :inet.ip_address() | String.t() | Limen.IP.prefix()) :: :ok
Lifts a ban. Accepts the same targets as ban/4.
Changes an option of a running instance.
Options given as keyword lists are merged into their current values, so only the given keys change:
Limen.update_config(:my_app, :trap, min_fill_time: 1_000)
Limen.update_config(:my_app, :maze, max_concurrent: 50)Any other value replaces the option's. The new configuration is validated
like the one the instance started with, and published at once; see
"Changing options at runtime" in Limen.Config for the few options that
can only change with a restart. Publishing triggers a global GC scan in
the runtime, so treat it as an operator action, not a per-request one.