Throttle incoming requests to 1/sec per ip

This introduces a way of throttling requests in a way that makes sense
for the purpose of the app. The app only supports redirecting to one
particular service when browsing, which would seldom be required more
than once per second for normal "human" browsing.

Without this, the service could easily be used to DOS multiple instances
at once. That being said, anyone concerned about someone DOS-ing
multiple instances at once should be aware that this would be trivial to
do with a simple bash script. This is simply a preventative measure to
hopefully deter people from trying to attack all public instances of
private frontends using farside.link.

Note that this throttling applies to all routes in the app, including
the homepage. This could be updated to exclude the homepage I guess,
but I'm not really sure what the use case would be for that.
This commit is contained in:
Ben Busby 2021-11-12 14:34:36 -07:00
parent 8ee4f308a4
commit 2d988a1239
No known key found for this signature in database
GPG key ID: 339B7B7EB5333D14
6 changed files with 38 additions and 24 deletions

View file

@ -8,7 +8,8 @@ defmodule Farside.Application do
def start(_type, _args) do
children = [
Plug.Cowboy.child_spec(scheme: :http, plug: Farside.Router, options: [port: 4001]),
{Redix, {@redis_conn, [name: :redix]}}
{Redix, {@redis_conn, [name: :redix]}},
{PlugAttack.Storage.Ets, name: Farside.Throttle.Storage, clean_period: 60_000}
]
opts = [strategy: :one_for_one, name: Farside.Supervisor]

View file

@ -3,6 +3,7 @@ defmodule Farside.Router do
use Plug.Router
plug(Farside.Throttle)
plug(:match)
plug(:dispatch)

19
lib/farside/throttle.ex Normal file
View file

@ -0,0 +1,19 @@
defmodule Farside.Throttle do
import Plug.Conn
use PlugAttack
rule "throttle per ip", conn do
# throttle to 1 request per second
throttle conn.remote_ip,
period: 1_000, limit: 1,
storage: {PlugAttack.Storage.Ets, Farside.Throttle.Storage}
end
def allow_action(conn, _data, _opts), do: conn
def block_action(conn, _data, _opts) do
conn
|> send_resp(:forbidden, "Exceeded rate limit\n")
|> halt
end
end

View file

@ -22,11 +22,12 @@ defmodule Farside.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:httpoison, "~> 1.8"},
{:jason, "~> 1.1"},
{:plug_attack, "~> 0.4.2"},
{:plug_cowboy, "~> 2.0"},
{:poison, "~> 5.0"},
{:httpoison, "~> 1.8"},
{:redix, "~> 1.1"}
{:redix, "~> 1.1"},
]
end
end

View file

@ -14,6 +14,7 @@
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
"plug_attack": {:hex, :plug_attack, "0.4.3", "88e6c464d68b1491aa083a0347d59d58ba71a7e591a7f8e1b675e8c7792a0ba8", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9ed6fb8a6f613a36040f2875130a21187126c5625092f24bc851f7f12a8cbdc1"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},

View file

@ -8,22 +8,21 @@ defmodule FarsideTest do
@opts Router.init([])
test "/" do
conn =
def test_conn(path) do
:timer.sleep(1000)
:get
|> conn("/", "")
|> conn(path, "")
|> Router.call(@opts)
end
test "/" do
conn = test_conn("/")
assert conn.state == :sent
assert conn.status == 200
end
test "/ping" do
conn =
:get
|> conn("/ping", "")
|> Router.call(@opts)
conn = test_conn("/ping")
assert conn.state == :sent
assert conn.status == 200
assert conn.resp_body == "PONG"
@ -42,24 +41,16 @@ defmodule FarsideTest do
IO.puts("")
Enum.map(service_names, fn service_name ->
conn =
:get
|> conn("/#{service_name}", "")
|> Router.call(@opts)
conn = test_conn("/#{service_name}")
first_redirect = elem(List.last(conn.resp_headers), 1)
IO.puts(" /#{service_name} (#1) -- #{first_redirect}")
assert conn.state == :set
assert conn.status == 302
conn =
:get
|> conn("/#{service_name}", "")
|> Router.call(@opts)
conn = test_conn("/#{service_name}")
second_redirect = elem(List.last(conn.resp_headers), 1)
IO.puts(" /#{service_name} (#2) -- #{second_redirect}")
assert conn.state == :set
assert conn.status == 302