From 2d988a1239bd9988d453df5baba75a610f979eb3 Mon Sep 17 00:00:00 2001 From: Ben Busby Date: Fri, 12 Nov 2021 14:34:36 -0700 Subject: [PATCH] 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. --- lib/farside/application.ex | 3 ++- lib/farside/router.ex | 1 + lib/farside/throttle.ex | 19 +++++++++++++++++++ mix.exs | 5 +++-- mix.lock | 1 + test/farside_test.exs | 33 ++++++++++++--------------------- 6 files changed, 38 insertions(+), 24 deletions(-) create mode 100644 lib/farside/throttle.ex diff --git a/lib/farside/application.ex b/lib/farside/application.ex index 708ff0d..fd199e1 100644 --- a/lib/farside/application.ex +++ b/lib/farside/application.ex @@ -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] diff --git a/lib/farside/router.ex b/lib/farside/router.ex index 7f31e04..e2014b2 100644 --- a/lib/farside/router.ex +++ b/lib/farside/router.ex @@ -3,6 +3,7 @@ defmodule Farside.Router do use Plug.Router + plug(Farside.Throttle) plug(:match) plug(:dispatch) diff --git a/lib/farside/throttle.ex b/lib/farside/throttle.ex new file mode 100644 index 0000000..fc8b591 --- /dev/null +++ b/lib/farside/throttle.ex @@ -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 diff --git a/mix.exs b/mix.exs index fbbbcc4..f6f91cd 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index bb98ecc..26a5cc7 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/farside_test.exs b/test/farside_test.exs index 786c382..b5561e0 100644 --- a/test/farside_test.exs +++ b/test/farside_test.exs @@ -8,22 +8,21 @@ defmodule FarsideTest do @opts Router.init([]) - test "/" do - conn = - :get - |> conn("/", "") + def test_conn(path) do + :timer.sleep(1000) + :get + |> 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