Add `remote_ip` plug
authorminibikini <egor@kislitsyn.com>
Fri, 27 Sep 2019 21:59:23 +0000 (21:59 +0000)
committerHaelwenn <contact+git.pleroma.social@hacktivis.me>
Fri, 27 Sep 2019 21:59:23 +0000 (21:59 +0000)
CHANGELOG.md
config/config.exs
config/description.exs
docs/config.md
installation/pleroma.nginx
lib/pleroma/plugs/remote_ip.ex [new file with mode: 0644]
lib/pleroma/web/endpoint.ex
mix.exs
mix.lock
test/plugs/remote_ip_test.exs [new file with mode: 0644]

index 0a8163135ca82594ce46388e64b7d83ba0f2ed76..1d307f0e150f8a3664ca9ea57d7df703dca5f5dc 100644 (file)
@@ -113,6 +113,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
 - Pleroma API: Email change endpoint.
 - Admin API: Added moderation log
+- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
 - Web response cache (currently, enabled for ActivityPub)
 - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
 - ActivityPub: Add ActivityPub actor's `discoverable` parameter.
index 403ade60d1a771739cf3c3dc8d2d0089409fb0cd..36bea19a089a51d035c696d2446ba8cf24e426df 100644 (file)
@@ -591,6 +591,8 @@ config :pleroma, :rate_limit, nil
 
 config :pleroma, Pleroma.ActivityExpiration, enabled: true
 
+config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
+
 config :pleroma, :web_cache_ttl,
   activity_pub: nil,
   activity_pub_question: 30_000
index 38b30bbf690a692217fa86680ed3aae753a0a629..4547ea36839e5d04ff6a017f209ecba47adce4cd 100644 (file)
@@ -2687,6 +2687,42 @@ config :pleroma, :config_description, [
       }
     ]
   },
+  %{
+    group: :pleroma,
+    key: Pleroma.Plugs.RemoteIp,
+    type: :group,
+    description: """
+    **If your instance is not behind at least one reverse proxy, you should not enable this plug.**
+
+    `Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+    """,
+    children: [
+      %{
+        key: :enabled,
+        type: :boolean,
+        description: "Enable/disable the plug. Defaults to `false`.",
+        suggestions: [true, false]
+      },
+      %{
+        key: :headers,
+        type: {:list, :string},
+        description:
+          "A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`."
+      },
+      %{
+        key: :proxies,
+        type: {:list, :string},
+        description:
+          "A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`."
+      },
+      %{
+        key: :reserved,
+        type: {:list, :string},
+        description:
+          "Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network)."
+      }
+    ]
+  },
   %{
     group: :pleroma,
     key: :web_cache_ttl,
index ed119fd324c97b63cfeadb59eafa854eca94a017..262d15bba2dbda961b09ff5408f85c6e83aaf0f0 100644 (file)
@@ -730,6 +730,8 @@ This will probably take a long time.
 
 This is an advanced feature and disabled by default.
 
+If your instance is behind a reverse proxy you must enable and configure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip).
+
 A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:
 
 * The first element: `scale` (Integer). The time scale in milliseconds.
@@ -756,3 +758,16 @@ Available caches:
 
 * `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
 * `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).
+
+## Pleroma.Plugs.RemoteIp
+
+**If your instance is not behind at least one reverse proxy, you should not enable this plug.**
+
+`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+
+Available options:
+
+* `enabled` - Enable/disable the plug. Defaults to `false`.
+* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`.
+* `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`.
+* `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network).
index 4da9918ca895ec4797272155841f123f472f511a..7f48b614b4c006e47bdb2e5c01924308ce463bed 100644 (file)
@@ -70,6 +70,7 @@ server {
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
         proxy_set_header Host $http_host;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
         # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
         # and `localhost.` resolves to [::0] on some systems: see issue #930
diff --git a/lib/pleroma/plugs/remote_ip.ex b/lib/pleroma/plugs/remote_ip.ex
new file mode 100644 (file)
index 0000000..fdedc27
--- /dev/null
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RemoteIp do
+  @moduledoc """
+  This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
+  """
+
+  @behaviour Plug
+
+  @headers ~w[
+    forwarded
+    x-forwarded-for
+    x-client-ip
+    x-real-ip
+  ]
+
+  # https://en.wikipedia.org/wiki/Localhost
+  # https://en.wikipedia.org/wiki/Private_network
+  @reserved ~w[
+    127.0.0.0/8
+    ::1/128
+    fc00::/7
+    10.0.0.0/8
+    172.16.0.0/12
+    192.168.0.0/16
+  ]
+
+  def init(_), do: nil
+
+  def call(conn, _) do
+    config = Pleroma.Config.get(__MODULE__, [])
+
+    if Keyword.get(config, :enabled, false) do
+      RemoteIp.call(conn, remote_ip_opts(config))
+    else
+      conn
+    end
+  end
+
+  defp remote_ip_opts(config) do
+    headers = config |> Keyword.get(:headers, @headers) |> MapSet.new()
+    reserved = Keyword.get(config, :reserved, @reserved)
+
+    proxies =
+      config
+      |> Keyword.get(:proxies, [])
+      |> Enum.concat(reserved)
+      |> Enum.map(&InetCidr.parse/1)
+
+    {headers, proxies}
+  end
+end
index eb805e853b3fdb349a0c3c4485336451192b1485..2212e93f417ed4ee329ab6e0928c346be18ed373 100644 (file)
@@ -97,10 +97,7 @@ defmodule Pleroma.Web.Endpoint do
     extra: extra
   )
 
-  # Note: the plug and its configuration is compile-time this can't be upstreamed yet
-  if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
-    plug(RemoteIp, proxies: proxies)
-  end
+  plug(Pleroma.Plugs.RemoteIp)
 
   defmodule Instrumenter do
     use Prometheus.PhoenixInstrumenter
diff --git a/mix.exs b/mix.exs
index 861b94ad0af1e5ad92bfef4dd8e3de875a0d7b9c..3a605b4553d41c74ede5cd731007555f28736342 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -159,6 +159,9 @@ defmodule Pleroma.Mixfile do
       {:plug_static_index_html, "~> 1.0.0"},
       {:excoveralls, "~> 0.11.1", only: :test},
       {:flake_id, "~> 0.1.0"},
+      {:remote_ip,
+       git: "https://git.pleroma.social/pleroma/remote_ip.git",
+       ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"},
       {:mox, "~> 0.5", only: :test}
     ] ++ oauth_deps()
   end
index 32443fb51e428f29315180bb0fcb4f72939c734f..5f740638ccfa4b3b2aa596659e01bdaeac09808b 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -48,6 +48,7 @@
   "http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
   "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
   "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
+  "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm"},
   "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
   "joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
   "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
@@ -87,6 +88,7 @@
   "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"},
   "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
   "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
+  "remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]},
   "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
   "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"},
   "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
diff --git a/test/plugs/remote_ip_test.exs b/test/plugs/remote_ip_test.exs
new file mode 100644 (file)
index 0000000..d120c58
--- /dev/null
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Plugs.RemoteIpTest do
+  use ExUnit.Case, async: true
+  use Plug.Test
+
+  alias Pleroma.Plugs.RemoteIp
+
+  test "disabled" do
+    Pleroma.Config.put(RemoteIp, enabled: false)
+
+    %{remote_ip: remote_ip} = conn(:get, "/")
+
+    conn =
+      conn(:get, "/")
+      |> put_req_header("x-forwarded-for", "1.1.1.1")
+      |> RemoteIp.call(nil)
+
+    assert conn.remote_ip == remote_ip
+  end
+
+  test "enabled" do
+    Pleroma.Config.put(RemoteIp, enabled: true)
+
+    conn =
+      conn(:get, "/")
+      |> put_req_header("x-forwarded-for", "1.1.1.1")
+      |> RemoteIp.call(nil)
+
+    assert conn.remote_ip == {1, 1, 1, 1}
+  end
+
+  test "custom headers" do
+    Pleroma.Config.put(RemoteIp, enabled: true, headers: ["cf-connecting-ip"])
+
+    conn =
+      conn(:get, "/")
+      |> put_req_header("x-forwarded-for", "1.1.1.1")
+      |> RemoteIp.call(nil)
+
+    refute conn.remote_ip == {1, 1, 1, 1}
+
+    conn =
+      conn(:get, "/")
+      |> put_req_header("cf-connecting-ip", "1.1.1.1")
+      |> RemoteIp.call(nil)
+
+    assert conn.remote_ip == {1, 1, 1, 1}
+  end
+
+  test "custom proxies" do
+    Pleroma.Config.put(RemoteIp, enabled: true)
+
+    conn =
+      conn(:get, "/")
+      |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
+      |> RemoteIp.call(nil)
+
+    refute conn.remote_ip == {1, 1, 1, 1}
+
+    Pleroma.Config.put([RemoteIp, :proxies], ["173.245.48.0/20"])
+
+    conn =
+      conn(:get, "/")
+      |> put_req_header("x-forwarded-for", "173.245.48.1, 1.1.1.1, 173.245.48.2")
+      |> RemoteIp.call(nil)
+
+    assert conn.remote_ip == {1, 1, 1, 1}
+  end
+end