diff --git a/.tool-versions b/.tool-versions
index 0e38921e8..40504e0ed 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,4 +1,4 @@
elixir 1.11.3-otp-23
-erlang 23.2.5
+erlang 23.2.7
nodejs 14.15.5
python 3.7.9
diff --git a/apps/fg_common/.formatter.exs b/apps/fg_common/.formatter.exs
new file mode 100644
index 000000000..d2cda26ed
--- /dev/null
+++ b/apps/fg_common/.formatter.exs
@@ -0,0 +1,4 @@
+# Used by "mix format"
+[
+ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
+]
diff --git a/apps/fg_common/.gitignore b/apps/fg_common/.gitignore
new file mode 100644
index 000000000..ae9a7bc47
--- /dev/null
+++ b/apps/fg_common/.gitignore
@@ -0,0 +1,27 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where third-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+fg_common-*.tar
+
+
+# Temporary files for e.g. tests
+/tmp
diff --git a/apps/fg_common/README.md b/apps/fg_common/README.md
new file mode 100644
index 000000000..bb89d5d3c
--- /dev/null
+++ b/apps/fg_common/README.md
@@ -0,0 +1,20 @@
+# FgCommon
+
+**TODO: Add description**
+
+## Installation
+
+If [available in Hex](https://hex.pm/docs/publish), the package can be installed
+by adding `fg_common` to your list of dependencies in `mix.exs`:
+
+```elixir
+def deps do
+ [
+ {:fg_common, "~> 0.1.0"}
+ ]
+end
+```
+
+Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
+and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
+be found at [https://hexdocs.pm/fg_common](https://hexdocs.pm/fg_common).
diff --git a/apps/fg_common/lib/cli.ex b/apps/fg_common/lib/cli.ex
new file mode 100644
index 000000000..2ccfa6bbe
--- /dev/null
+++ b/apps/fg_common/lib/cli.ex
@@ -0,0 +1,22 @@
+defmodule FgCommon.CLI do
+ @moduledoc """
+ Handles low-level CLI facilities.
+ """
+
+ defp bash(cmd) do
+ System.cmd("bash", ["-c", cmd])
+ end
+
+ def exec!(cmd) do
+ case bash(cmd) do
+ {result, 0} ->
+ result
+
+ {error, _} ->
+ raise """
+ Error executing command #{cmd} with error #{error}.
+ FireGuard cannot recover from this error.
+ """
+ end
+ end
+end
diff --git a/apps/fg_common/lib/fg_common.ex b/apps/fg_common/lib/fg_common.ex
new file mode 100644
index 000000000..1d13913b9
--- /dev/null
+++ b/apps/fg_common/lib/fg_common.ex
@@ -0,0 +1,5 @@
+defmodule FgCommon do
+ @moduledoc """
+ Documentation for `FgCommon`.
+ """
+end
diff --git a/apps/fg_common/mix.exs b/apps/fg_common/mix.exs
new file mode 100644
index 000000000..72d1b2331
--- /dev/null
+++ b/apps/fg_common/mix.exs
@@ -0,0 +1,33 @@
+defmodule FgCommon.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :fg_common,
+ version: "0.1.0",
+ build_path: "../../_build",
+ config_path: "../../config/config.exs",
+ deps_path: "../../deps",
+ lockfile: "../../mix.lock",
+ elixir: "~> 1.11",
+ start_permanent: Mix.env() == :prod,
+ deps: deps()
+ ]
+ end
+
+ # Run "mix help compile.app" to learn about applications.
+ def application do
+ [
+ extra_applications: [:logger]
+ ]
+ end
+
+ # Run "mix help deps" to learn about dependencies.
+ defp deps do
+ [
+ # {:dep_from_hexpm, "~> 0.3.0"},
+ # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
+ # {:sibling_app_in_umbrella, in_umbrella: true}
+ ]
+ end
+end
diff --git a/apps/fg_common/test/fg_common_test.exs b/apps/fg_common/test/fg_common_test.exs
new file mode 100644
index 000000000..34d8fc615
--- /dev/null
+++ b/apps/fg_common/test/fg_common_test.exs
@@ -0,0 +1,8 @@
+defmodule FgCommonTest do
+ use ExUnit.Case
+ doctest FgCommon
+
+ test "greets the world" do
+ assert FgCommon.hello() == :world
+ end
+end
diff --git a/apps/fg_common/test/test_helper.exs b/apps/fg_common/test/test_helper.exs
new file mode 100644
index 000000000..869559e70
--- /dev/null
+++ b/apps/fg_common/test/test_helper.exs
@@ -0,0 +1 @@
+ExUnit.start()
diff --git a/apps/fg_http/lib/fg_http/devices.ex b/apps/fg_http/lib/fg_http/devices.ex
index 7e051e8ab..379efad77 100644
--- a/apps/fg_http/lib/fg_http/devices.ex
+++ b/apps/fg_http/lib/fg_http/devices.ex
@@ -4,7 +4,7 @@ defmodule FgHttp.Devices do
"""
import Ecto.Query, warn: false
- alias FgHttp.{Devices.Device, Repo}
+ alias FgHttp.{Devices.Device, Repo, Util.FgCrypto}
def list_devices do
Repo.all(Device)
@@ -52,6 +52,10 @@ defmodule FgHttp.Devices do
Device.changeset(device, %{})
end
+ def rand_name do
+ "Device " <> FgCrypto.rand_string(8)
+ end
+
def to_peer_list do
for device <- Repo.all(Device) do
%{
diff --git a/apps/fg_http/lib/fg_http/devices/device.ex b/apps/fg_http/lib/fg_http/devices/device.ex
index 1e65a6ec8..56736ed6a 100644
--- a/apps/fg_http/lib/fg_http/devices/device.ex
+++ b/apps/fg_http/lib/fg_http/devices/device.ex
@@ -15,7 +15,8 @@ defmodule FgHttp.Devices.Device do
field :preshared_key, FgHttp.Encrypted.Binary
field :private_key, FgHttp.Encrypted.Binary
field :server_public_key, :string
- field :last_ip, EctoNetwork.INET
+ field :remote_ip, EctoNetwork.INET
+ field :interface_address, EctoNetwork.INET
field :last_seen_at, :utc_datetime_usec
has_many :rules, Rule
@@ -27,7 +28,8 @@ defmodule FgHttp.Devices.Device do
def changeset(device, attrs) do
device
|> cast(attrs, [
- :last_ip,
+ :remote_ip,
+ :interface_address,
:server_public_key,
:private_key,
:preshared_key,
@@ -37,13 +39,16 @@ defmodule FgHttp.Devices.Device do
])
|> validate_required([
:user_id,
+ :interface_address,
:name,
:public_key,
:server_public_key,
:private_key,
:preshared_key
])
- |> unique_constraint([:name, :public_key])
- |> unique_constraint([:name, :private_key])
+ |> unique_constraint(:public_key)
+ |> unique_constraint(:private_key)
+ |> unique_constraint(:preshared_key)
+ |> unique_constraint([:user_id, :name])
end
end
diff --git a/apps/fg_http/lib/fg_http/ecto_enums.ex b/apps/fg_http/lib/fg_http/ecto_enums.ex
index f8a57bad6..d2e552c5f 100644
--- a/apps/fg_http/lib/fg_http/ecto_enums.ex
+++ b/apps/fg_http/lib/fg_http/ecto_enums.ex
@@ -1,18 +1,4 @@
import EctoEnum
# We only allow dropping or accepting packets for now
-defenum(RuleActionEnum, :action, [:drop, :allow])
-
-# See http://ipset.netfilter.org/iptables.man.html
-defenum(RuleProtocolEnum, :protocol, [
- :all,
- :tcp,
- :udp,
- :udplite,
- :icmp,
- :icmpv6,
- :esp,
- :ah,
- :sctp,
- :mh
-])
+defenum(RuleActionEnum, :action, [:block, :allow])
diff --git a/apps/fg_http/lib/fg_http/repo.ex b/apps/fg_http/lib/fg_http/repo.ex
index 0bdbfc516..bd728d7ac 100644
--- a/apps/fg_http/lib/fg_http/repo.ex
+++ b/apps/fg_http/lib/fg_http/repo.ex
@@ -5,10 +5,13 @@ defmodule FgHttp.Repo do
alias FgHttp.Devices
require Logger
- import FgHttpWeb.EventHelpers
+ import FgHttpWeb.Events
def init(_) do
- # Notify FgVpn.Server the config has been loaded
- send(vpn_pid(), {:set_config, Devices.to_peer_list()})
+ # Set firewall rules
+ set_rules()
+
+ # Set WireGuard peer config
+ set_config()
end
end
diff --git a/apps/fg_http/lib/fg_http/rules.ex b/apps/fg_http/lib/fg_http/rules.ex
index 364cf890f..7d31ced0f 100644
--- a/apps/fg_http/lib/fg_http/rules.ex
+++ b/apps/fg_http/lib/fg_http/rules.ex
@@ -6,7 +6,7 @@ defmodule FgHttp.Rules do
import Ecto.Query, warn: false
alias FgHttp.Repo
- alias FgHttp.Rules.Rule
+ alias FgHttp.{Devices.Device, Rules.Rule}
def get_rule!(id), do: Repo.get!(Rule, id)
@@ -29,4 +29,23 @@ defmodule FgHttp.Rules do
def change_rule(%Rule{} = rule) do
Rule.changeset(rule, %{})
end
+
+ def to_iptables do
+ query =
+ from d in Device,
+ join: r in Rule,
+ on: r.device_id == d.id,
+ where: r.enabled == true,
+ # :block enum is indexed 0
+ order_by: r.action,
+ select: {
+ # Need to select both ipv4 and ipv6 since we don't know which the
+ # corresponding rule is.
+ {d.interface_address4, d.interface_address6},
+ r.destination,
+ r.action
+ }
+
+ Repo.all(query)
+ end
end
diff --git a/apps/fg_http/lib/fg_http/rules/rule.ex b/apps/fg_http/lib/fg_http/rules/rule.ex
index 3d944008d..8d24b45d8 100644
--- a/apps/fg_http/lib/fg_http/rules/rule.ex
+++ b/apps/fg_http/lib/fg_http/rules/rule.ex
@@ -6,15 +6,12 @@ defmodule FgHttp.Rules.Rule do
use Ecto.Schema
import Ecto.Changeset
- alias FgHttp.{Devices.Device}
+ alias FgHttp.{Devices, Devices.Device}
schema "rules" do
field :destination, EctoNetwork.INET
- field :action, RuleActionEnum, default: "drop"
- field :priority, :integer, default: 0
+ field :action, RuleActionEnum, default: :block
field :enabled, :boolean, default: true
- field :port_number, :integer
- field :protocol, RuleProtocolEnum, default: "all"
belongs_to :device, Device
@@ -25,15 +22,36 @@ defmodule FgHttp.Rules.Rule do
rule
|> cast(attrs, [
:device_id,
- :priority,
:action,
:destination,
- :port_number,
- :protocol,
:enabled
])
- |> validate_required([:device_id, :priority, :action, :destination, :protocol, :enabled])
- |> validate_number(:priority, greater_than_or_equal_to: 0, less_than_or_equal_to: 100)
- |> validate_number(:port_number, greater_than_or_equal_to: 0, less_than_or_equal_to: 65_535)
+ |> validate_required([:device_id, :action, :destination, :enabled])
+ end
+
+ def iptables_spec(rule) do
+ device = Devices.get_device!(rule.device_id)
+
+ source =
+ if ipv4?(rule) do
+ device.interface_address4
+ else
+ device.interface_address6
+ end
+
+ {source, rule.destination, rule.action}
+ end
+
+ defp ipv4?(rule) do
+ case parse_ipv4(rule) do
+ {:ok, _} -> true
+ {:error, _} -> false
+ end
+ end
+
+ defp parse_ipv4(rule) do
+ rule.destination
+ |> String.to_charlist()
+ |> :inet.parse_ipv4_address()
end
end
diff --git a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
index 464566a06..2e822970a 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex
@@ -16,32 +16,21 @@ defmodule FgHttpWeb.DeviceController do
end
def create(conn, _params) do
- case event_module().create_device_sync() do
- {:ok, device_attrs} ->
- attributes =
- Map.merge(
- %{
- user_id: conn.assigns.session.id,
- name: "Device #{DateTime.utc_now() |> DateTime.to_unix(:microsecond)}"
- },
- device_attrs
- )
+ # XXX: Remove device from WireGuard if create isn't successful
+ {:device_created, device_attrs} = event_module().create_device()
- case Devices.create_device(attributes) do
- {:ok, device} ->
- redirect(conn, to: Routes.device_path(conn, :show, device))
+ attributes =
+ Map.merge(%{user_id: conn.assigns.session.id, name: Devices.rand_name()}, device_attrs)
- {:error, %Ecto.Changeset{} = changeset} ->
- msg = ErrorHelpers.aggregated_errors(changeset)
+ case Devices.create_device(attributes) do
+ {:ok, device} ->
+ redirect(conn, to: Routes.device_path(conn, :show, device))
- conn
- |> put_flash(:error, "Error creating device. #{msg}")
- |> redirect(to: Routes.device_path(conn, :index))
- end
+ {:error, %Ecto.Changeset{} = changeset} ->
+ msg = ErrorHelpers.aggregated_errors(changeset)
- {:error, msg} ->
conn
- |> put_flash(:error, "Error creating device: #{msg}")
+ |> put_flash(:error, "Error creating device. #{msg}")
|> redirect(to: Routes.device_path(conn, :index))
end
end
@@ -73,11 +62,20 @@ defmodule FgHttpWeb.DeviceController do
def delete(conn, %{"id" => id}) do
device = Devices.get_device!(id)
- {:ok, _device} = Devices.delete_device(device)
- conn
- |> put_flash(:info, "Device deleted successfully.")
- |> redirect(to: Routes.device_path(conn, :index))
+ case Devices.delete_device(device) do
+ {:ok, _deleted_device} ->
+ {:device_deleted, _deleted_pubkey} = event_module().delete_device(device.public_key)
+
+ conn
+ |> put_flash(:info, "Device deleted successfully.")
+ |> redirect(to: Routes.device_path(conn, :index))
+
+ {:error, msg} ->
+ conn
+ |> put_flash(:error, "Error deleting device: #{msg}")
+ |> redirect(to: Routes.device_path(conn, :index))
+ end
end
defp event_module do
diff --git a/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
index 14de105c8..e7c89c37d 100644
--- a/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
+++ b/apps/fg_http/lib/fg_http_web/controllers/rule_controller.ex
@@ -13,19 +13,15 @@ defmodule FgHttpWeb.RuleController do
render(conn, "index.html", device: device, rules: device.rules)
end
- def new(conn, %{"device_id" => device_id}) do
- device = Devices.get_device!(device_id)
-
- changeset = Rules.change_rule(%Rule{device_id: device_id})
- render(conn, "new.html", changeset: changeset, device: device)
- end
-
def create(conn, %{"device_id" => device_id, "rule" => rule_params}) do
# XXX RBAC
all_params = Map.merge(rule_params, %{"device_id" => device_id})
case Rules.create_rule(all_params) do
{:ok, rule} ->
+ # XXX: Create in after-commit
+ :rule_added = add_rule_to_firewall(rule)
+
conn
|> put_flash(:info, "Rule created successfully.")
|> redirect(to: Routes.device_rule_path(conn, :index, rule.device_id))
@@ -36,41 +32,23 @@ defmodule FgHttpWeb.RuleController do
end
end
- def show(conn, %{"id" => id}) do
- rule = Rules.get_rule!(id)
- render(conn, "show.html", rule: rule)
- end
-
- def edit(conn, %{"id" => id}) do
- rule = Rules.get_rule!(id)
- device = Devices.get_device!(rule.device_id)
- changeset = Rules.change_rule(rule)
-
- render(conn, "edit.html", rule: rule, device: device, changeset: changeset)
- end
-
- def update(conn, %{"id" => id, "rule" => rule_params}) do
- rule = Rules.get_rule!(id)
- device = Devices.get_device!(rule.device_id)
-
- case Rules.update_rule(rule, rule_params) do
- {:ok, rule} ->
- conn
- |> put_flash(:info, "Rule updated successfully.")
- |> redirect(to: Routes.device_rule_path(conn, :index, rule.device_id))
-
- {:error, changeset} ->
- render(conn, "edit.html", rule: rule, device: device, changeset: changeset)
- end
- end
-
def delete(conn, %{"id" => id}) do
rule = Rules.get_rule!(id)
device_id = rule.device_id
{:ok, _rule} = Rules.delete_rule(rule)
+ # XXX: Delete in after-commit
+ :rule_deleted = delete_rule_from_firewall(rule)
+
conn
|> put_flash(:info, "Rule deleted successfully.")
|> redirect(to: Routes.device_rule_path(conn, :index, device_id))
end
+
+ defp add_rule_to_firewall(rule) do
+ GenServer.call()
+ end
+
+ defp delete_rule_from_firewall(rule) do
+ end
end
diff --git a/apps/fg_http/lib/fg_http_web/event_helpers.ex b/apps/fg_http/lib/fg_http_web/event_helpers.ex
index e81eca3ea..705c1cc5f 100644
--- a/apps/fg_http/lib/fg_http_web/event_helpers.ex
+++ b/apps/fg_http/lib/fg_http_web/event_helpers.ex
@@ -12,4 +12,14 @@ defmodule FgHttpWeb.EventHelpers do
{:ok, vpn_pid}
end
end
+
+ def wall_pid do
+ case :global.whereis_name(:fg_wall_server) do
+ :undefined ->
+ {:error, "VPN server process not registered in global registry."}
+
+ wall_pid ->
+ {:ok, wall_pid}
+ end
+ end
end
diff --git a/apps/fg_http/lib/fg_http_web/events.ex b/apps/fg_http/lib/fg_http_web/events.ex
index 23916a1c4..27c87c85e 100644
--- a/apps/fg_http/lib/fg_http_web/events.ex
+++ b/apps/fg_http/lib/fg_http_web/events.ex
@@ -4,25 +4,29 @@ defmodule FgHttpWeb.Events do
"""
import FgHttpWeb.EventHelpers
+ alias FgHttp.Devices
- def create_device_sync do
- case vpn_pid() do
- {:ok, pid} ->
- send(pid, {:create_device, self()})
+ def create_device do
+ GenServer.call(vpn_pid(), {:create_device})
+ end
- receive do
- {:device_created, privkey, pubkey, server_pubkey, psk} ->
- {:ok,
- %{
- private_key: privkey,
- public_key: pubkey,
- server_public_key: server_pubkey,
- preshared_key: psk
- }}
- end
+ def delete_device(device_pubkey) do
+ GenServer.call(vpn_pid(), {:delete_device, device_pubkey})
+ end
- {:error, msg} ->
- {:error, msg}
- end
+ def add_rule(rule) do
+ GenServer.call(wall_pid(), {:add_rule, Rule.iptables_spec(rule)})
+ end
+
+ def delete_rule(rule) do
+ GenServer.call(wall_pid(), {:delete_rule, Rule.iptables_spec(rule)})
+ end
+
+ def set_config do
+ GenServer.call(vpn_pid(), {:set_config, Devices.to_peer_list()})
+ end
+
+ def set_rules do
+ GenServer.call(wall_pid(), {:set_rules, Rules.to_iptables()})
end
end
diff --git a/apps/fg_http/lib/fg_http_web/mock_events.ex b/apps/fg_http/lib/fg_http_web/mock_events.ex
index c89bb9896..49dbcc4e6 100644
--- a/apps/fg_http/lib/fg_http_web/mock_events.ex
+++ b/apps/fg_http/lib/fg_http_web/mock_events.ex
@@ -7,8 +7,8 @@ defmodule FgHttpWeb.MockEvents do
inside FgHttp and use that for the tests.
"""
- def create_device_sync do
- {:ok,
+ def create_device do
+ {:device_created,
%{
private_key: "privkey",
public_key: "pubkey",
@@ -16,4 +16,16 @@ defmodule FgHttpWeb.MockEvents do
preshared_key: "preshared_key"
}}
end
+
+ def delete_device(pubkey) do
+ {:device_deleted, pubkey}
+ end
+
+ def add_rule(_rule) do
+ :rule_added
+ end
+
+ def delete_rule(_rule) do
+ :rule_deleted
+ end
end
diff --git a/apps/fg_http/lib/fg_http_web/router.ex b/apps/fg_http/lib/fg_http_web/router.ex
index e67b5167e..1267b0326 100644
--- a/apps/fg_http/lib/fg_http_web/router.ex
+++ b/apps/fg_http/lib/fg_http_web/router.ex
@@ -32,10 +32,10 @@ defmodule FgHttpWeb.Router do
resources "/users", UserController, only: [:new, :create]
resources "/devices", DeviceController, except: [:new] do
- resources "/rules", RuleController, only: [:new, :index, :create]
+ resources "/rules", RuleController, only: [:index, :create]
end
- resources "/rules", RuleController, only: [:show, :update, :delete, :edit]
+ resources "/rules", RuleController, only: [:delete]
resources "/session", SessionController, singleton: true, only: [:delete]
resources "/sessions", SessionController, only: [:new, :create]
diff --git a/apps/fg_http/lib/fg_http_web/templates/device/index.html.eex b/apps/fg_http/lib/fg_http_web/templates/device/index.html.eex
index 470d3a20c..0fa6110ba 100644
--- a/apps/fg_http/lib/fg_http_web/templates/device/index.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/device/index.html.eex
@@ -7,7 +7,7 @@
Name |
Rules |
Public key |
- Last IP |
+ Remote IP |
|
@@ -17,7 +17,7 @@
<%= device.name %> |
<%= link rules_title(device), to: Routes.device_rule_path(@conn, :index, device) %> |
<%= device.public_key %> |
- <%= device.last_ip || "Never connected" %> |
+ <%= device.remote_ip || "Never connected" %> |
<%= link "Show", to: Routes.device_path(@conn, :show, device) %>
|
diff --git a/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs b/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
index 90d613bd6..d4dc553a6 100644
--- a/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
+++ b/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
@@ -9,7 +9,9 @@ defmodule FgHttp.Repo.Migrations.CreateDevices do
add :preshared_key, :bytea, null: false
add :private_key, :bytea, null: false
add :server_public_key, :string, null: false
- add :last_ip, :inet
+ add :remote_ip, :inet
+ add :interface_address, :inet, null: false
+ add :last_seen_at, :utc_datetime_usec
add :user_id, references(:users, on_delete: :delete_all), null: false
timestamps(type: :utc_datetime_usec)
@@ -18,6 +20,7 @@ defmodule FgHttp.Repo.Migrations.CreateDevices do
create index(:devices, [:user_id])
create unique_index(:devices, [:public_key])
create unique_index(:devices, [:private_key])
- create unique_index(:devices, [:name])
+ create unique_index(:devices, [:preshared_key])
+ create unique_index(:devices, [:user_id, :name])
end
end
diff --git a/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs b/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
index 1fc9af1ca..4077fd95f 100644
--- a/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
+++ b/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
@@ -1,22 +1,19 @@
-defmodule FgHttp.Repo.Migrations.CreateRules do
+defmodule FgHttp.Repo.Migrations.CreateBlacklistEntries do
use Ecto.Migration
def change do
RuleActionEnum.create_type()
- RuleProtocolEnum.create_type()
create table(:rules) do
- add :destination, :inet
- add :protocol, RuleProtocolEnum.type(), default: "all", null: false
- add :action, RuleActionEnum.type(), default: "drop", null: false
- add :priority, :integer, default: 0, null: false
- add :enabled, :boolean, default: false, null: false
- add :port_number, :integer
+ add :destination, :inet, null: false
+ add :action, RuleActionEnum.type(), default: :block, null: false
+ add :enabled, :boolean, default: true, null: false
add :device_id, references(:devices, on_delete: :delete_all), null: false
timestamps(type: :utc_datetime_usec)
end
- create index(:rules, [:device_id])
+ create index(:rules, [:device_id, :action])
+ create index(:rules, [:enabled])
end
end
diff --git a/apps/fg_http/priv/repo/migrations/20201115200531_add_last_seen_at_to_devices.exs b/apps/fg_http/priv/repo/migrations/20201115200531_add_last_seen_at_to_devices.exs
deleted file mode 100644
index 11095e29a..000000000
--- a/apps/fg_http/priv/repo/migrations/20201115200531_add_last_seen_at_to_devices.exs
+++ /dev/null
@@ -1,9 +0,0 @@
-defmodule FgHttp.Repo.Migrations.AddLastSeenAtToDevices do
- use Ecto.Migration
-
- def change do
- alter table(:devices) do
- add :last_seen_at, :utc_datetime_usec
- end
- end
-end
diff --git a/apps/fg_http/priv/repo/seeds.exs b/apps/fg_http/priv/repo/seeds.exs
index ae865e45a..9ca5c447d 100644
--- a/apps/fg_http/priv/repo/seeds.exs
+++ b/apps/fg_http/priv/repo/seeds.exs
@@ -10,7 +10,7 @@
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
-alias FgHttp.{Devices, Rules, Users}
+alias FgHttp.{Devices, BlacklistEntries, Users}
{:ok, user} =
Users.create_user(%{
@@ -27,11 +27,12 @@ alias FgHttp.{Devices, Rules, Users}
server_public_key: "QFvMfHTjlJN9cfUiK1w4XmxOomH6KRTCMrVC6z3TWFM=",
private_key: "2JSZtpSHM+69Hm7L3BSGIymbq0byw39iWLevKESd1EM=",
preshared_key: "hQS+GkbTWfEhueLM8RJ2anjC4RxzdgL4dpTIetHf6GU=",
- last_ip: %Postgrex.INET{address: {127, 0, 0, 1}}
+ remote_ip: %Postgrex.INET{address: {127, 0, 0, 1}},
+ interface_address: %Postgrex.INET{address: {10, 0, 0, 1}}
})
{:ok, _rule} =
- Rules.create_rule(%{
+ BlacklistEntries.create_entry(%{
device_id: device.id,
destination: %Postgrex.INET{address: {0, 0, 0, 0}, netmask: 0}
})
diff --git a/apps/fg_http/test/support/fixtures.ex b/apps/fg_http/test/support/fixtures.ex
index ef5a6a29a..a0cbfec4a 100644
--- a/apps/fg_http/test/support/fixtures.ex
+++ b/apps/fg_http/test/support/fixtures.ex
@@ -24,6 +24,7 @@ defmodule FgHttp.Fixtures do
attrs
|> Enum.into(%{user_id: user().id})
|> Enum.into(%{
+ interface_address: "10.0.0.1",
public_key: "test-pubkey",
name: "factory",
private_key: "test-privkey",
diff --git a/apps/fg_vpn/lib/fg_vpn/cli.ex b/apps/fg_vpn/lib/fg_vpn/cli.ex
index ec09a1bcb..7722cf46c 100644
--- a/apps/fg_vpn/lib/fg_vpn/cli.ex
+++ b/apps/fg_vpn/lib/fg_vpn/cli.ex
@@ -4,6 +4,6 @@ defmodule FgVpn.CLI do
"""
def cli do
- Application.get_env(:fg_vpn, :cli)
+ Application.fetch_env!(:fg_vpn, :cli)
end
end
diff --git a/apps/fg_vpn/lib/fg_vpn/cli/live.ex b/apps/fg_vpn/lib/fg_vpn/cli/live.ex
index 91e8e4e7c..dd042544e 100644
--- a/apps/fg_vpn/lib/fg_vpn/cli/live.ex
+++ b/apps/fg_vpn/lib/fg_vpn/cli/live.ex
@@ -17,6 +17,8 @@ defmodule FgVpn.CLI.Live do
@iface_name "wg-fireguard"
+ import FgCommon.CLI
+
def setup do
create_interface()
setup_iptables()
@@ -82,19 +84,6 @@ defmodule FgVpn.CLI.Live do
end
end
- def exec!(cmd) do
- case bash(cmd) do
- {result, 0} ->
- result
-
- {error, _} ->
- raise """
- Error executing command #{cmd} with error #{error}.
- FireGuard cannot recover from this error.
- """
- end
- end
-
defp show(subcommand) do
exec!("wg show #{@iface_name} #{subcommand}")
end
@@ -118,20 +107,22 @@ defmodule FgVpn.CLI.Live do
end
end
+ # XXX: Move to FgWall and call via PID?
defp setup_iptables do
- exec!(
- "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o #{
- egress_interface()
- } -j MASQUERADE"
- )
+ exec!("\
+ iptables -A FORWARD -i %i -j ACCEPT;\
+ iptables -A FORWARD -o %i -j ACCEPT; \
+ iptables -t nat -A POSTROUTING -o #{egress_interface()} -j MASQUERADE\
+ ")
end
+ # XXX: Move to FgWall and call via PID?
defp teardown_iptables do
- exec!(
- "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o #{
- egress_interface()
- } -j MASQUERADE"
- )
+ exec!("\
+ iptables -D FORWARD -i %i -j ACCEPT;\
+ iptables -D FORWARD -o %i -j ACCEPT;\
+ iptables -t nat -D POSTROUTING -o #{egress_interface()} -j MASQUERADE\
+ ")
end
defp up_interface do
@@ -141,8 +132,4 @@ defmodule FgVpn.CLI.Live do
defp down_interface do
exec!("ifconfig #{@iface_name} down")
end
-
- defp bash(cmd) do
- System.cmd("bash", ["-c", cmd])
- end
end
diff --git a/apps/fg_vpn/lib/fg_vpn/config.ex b/apps/fg_vpn/lib/fg_vpn/config.ex
index e16175d31..84f4a4065 100644
--- a/apps/fg_vpn/lib/fg_vpn/config.ex
+++ b/apps/fg_vpn/lib/fg_vpn/config.ex
@@ -1,6 +1,6 @@
defmodule FgVpn.Config do
@moduledoc """
- Functions for managing the FireGuard configuration.
+ Functions for managing the WireGuard configuration.
"""
@default_interface_ip "172.16.59.1"
@@ -9,8 +9,7 @@ defmodule FgVpn.Config do
defstruct interface_ip: @default_interface_ip,
listen_port: 51_820,
- peers: MapSet.new([]),
- uncommitted_peers: MapSet.new([])
+ peers: MapSet.new([])
def render(config) do
"private-key #{private_key()} listen-port #{config.listen_port} " <>
diff --git a/apps/fg_vpn/lib/fg_vpn/server.ex b/apps/fg_vpn/lib/fg_vpn/server.ex
index 96838b683..a8d5faf91 100644
--- a/apps/fg_vpn/lib/fg_vpn/server.ex
+++ b/apps/fg_vpn/lib/fg_vpn/server.ex
@@ -30,48 +30,34 @@ defmodule FgVpn.Server do
end
@impl true
- def handle_info({:create_device, sender}, config) do
+ def handle_call({:create_device}, config) do
server_pubkey = Config.public_key()
{privkey, pubkey} = cli().genkey()
psk = cli().genpsk()
- uncommitted_peers = MapSet.put(config.uncommitted_peers, pubkey)
- new_config = Map.put(config, :uncommitted_peers, uncommitted_peers)
- send(sender, {:device_created, privkey, pubkey, server_pubkey, psk})
- {:noreply, new_config}
- end
-
- @impl true
- def handle_info({:commit_peer, %{} = attrs}, config) do
new_config =
- if MapSet.member?(config.uncommitted_peers, attrs[:public_key]) do
- new_peer = Map.merge(%Peer{}, attrs)
- new_peers = MapSet.put(config.peers, new_peer)
- new_uncommitted_peers = MapSet.delete(config.uncommitted_peers, attrs[:public_key])
+ Map.put(
+ config,
+ :peers,
+ MapSet.put(config.peers, pubkey)
+ )
- config
- |> Map.put(:uncommitted_peers, new_uncommitted_peers)
- |> Map.put(:peers, new_peers)
- else
- config
- end
-
- apply(new_config)
-
- {:noreply, new_config}
+ {:reply, {:device_created, privkey, pubkey, server_pubkey, psk}, new_config}
end
@impl true
- def handle_info({:remove_peer, pubkey}, config) do
+ def handle_call({:delete_device, pubkey}, config) do
new_peers = MapSet.delete(config.peers, %Peer{public_key: pubkey})
new_config = %{config | peers: new_peers}
apply(new_config)
- {:noreply, new_config}
+
+ {:reply, {:device_deleted, pubkey}, new_config}
end
@impl true
- def handle_cast({:set_config, new_config}, _config) do
- {:noreply, new_config}
+ def handle_call({:set_config, new_config}, _config) do
+ apply(new_config)
+ {:reply, :ok, new_config}
end
@doc """
diff --git a/apps/fg_vpn/mix.exs b/apps/fg_vpn/mix.exs
index 38398eb1a..afc76f4a6 100644
--- a/apps/fg_vpn/mix.exs
+++ b/apps/fg_vpn/mix.exs
@@ -33,6 +33,7 @@ defmodule FgVpn.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
+ {:fg_common, in_umbrella: true},
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
{:excoveralls, "~> 0.13", only: :test}
]
diff --git a/apps/fg_vpn/test/fg_vpn/config_test.exs b/apps/fg_vpn/test/fg_vpn/config_test.exs
index 9771ab69a..0736ba96a 100644
--- a/apps/fg_vpn/test/fg_vpn/config_test.exs
+++ b/apps/fg_vpn/test/fg_vpn/config_test.exs
@@ -22,8 +22,7 @@ defmodule FgVpn.ConfigTest do
allowed_ips: "test-allowed-ips",
preshared_key: "test-preshared-key"
}
- ]),
- uncommitted_peers: MapSet.new(["uncommitted-pubkey"])
+ ])
}
assert Config.render(config) == @populated_config
diff --git a/apps/fg_vpn/test/fg_vpn/server_test.exs b/apps/fg_vpn/test/fg_vpn/server_test.exs
index 6d38d2459..0c468fd34 100644
--- a/apps/fg_vpn/test/fg_vpn/server_test.exs
+++ b/apps/fg_vpn/test/fg_vpn/server_test.exs
@@ -26,21 +26,6 @@ defmodule FgVpn.ServerTest do
assert [] = MapSet.to_list(:sys.get_state(test_pid).peers)
end
- @tag stubbed_config: @empty
- test "writes peers to config when device is verified", %{test_pid: test_pid} do
- send(test_pid, {:create_device, self()})
-
- assert_receive {:device_created, _, _, _, _}
- [pubkey | _tail] = MapSet.to_list(:sys.get_state(test_pid).uncommitted_peers)
-
- send(test_pid, {:commit_peer, %{public_key: pubkey}})
-
- # XXX: Avoid sleeping
- Process.sleep(100)
-
- assert MapSet.to_list(:sys.get_state(test_pid).peers) == [%Peer{public_key: pubkey}]
- end
-
@tag stubbed_config: @single_peer
test "removes peers from config when removed", %{test_pid: test_pid} do
send(test_pid, {:remove_peer, "test-pubkey"})
diff --git a/apps/fg_wall/lib/fg_wall/application.ex b/apps/fg_wall/lib/fg_wall/application.ex
index 05145dafa..d9a13edcb 100644
--- a/apps/fg_wall/lib/fg_wall/application.ex
+++ b/apps/fg_wall/lib/fg_wall/application.ex
@@ -6,7 +6,9 @@ defmodule FgWall.Application do
use Application
def start(_type, _args) do
- children = []
+ children = [
+ FgWall.Server
+ ]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
diff --git a/apps/fg_wall/lib/fg_wall/cli.ex b/apps/fg_wall/lib/fg_wall/cli.ex
new file mode 100644
index 000000000..47963f2cf
--- /dev/null
+++ b/apps/fg_wall/lib/fg_wall/cli.ex
@@ -0,0 +1,9 @@
+defmodule FgWall.CLI do
+ @moduledoc """
+ Determines adapter to use for CLI commands.
+ """
+
+ def cli do
+ Application.fetch_env!(:fg_wall, :cli)
+ end
+end
diff --git a/apps/fg_wall/lib/fg_wall/cli/live.ex b/apps/fg_wall/lib/fg_wall/cli/live.ex
new file mode 100644
index 000000000..e43055a0a
--- /dev/null
+++ b/apps/fg_wall/lib/fg_wall/cli/live.ex
@@ -0,0 +1,70 @@
+defmodule FgWall.CLI.Live do
+ @moduledoc """
+ A low-level module for interacting with the iptables CLI.
+
+ Rules operate on the iptables forward chain to block outgoing packets to
+ specified IP addresses, ports, and protocols from FireGuard device IPs.
+
+ Note that iptables chains and rules are mutually exclusive between IPv4 and IPv6.
+ """
+
+ import FgCommon.CLI
+
+ @setup_chain_cmd "iptables -N fireguard && iptables6 -N fireguard"
+ @teardown_chain_cmd "iptables -F fireguard &&\
+ iptables -X fireguard &&\
+ iptables6 -F fireguard &&\
+ iptables6 -X fireguard"
+
+ @doc """
+ Sets up the FireGuard iptables chain.
+ """
+ def setup do
+ exec!(@setup_chain_cmd)
+ end
+
+ @doc """
+ Flushes and removes the FireGuard iptables chain.
+ """
+ def teardown do
+ exec!(@teardown_chain_cmd)
+ end
+
+ @doc """
+ Adds iptables rule.
+ """
+ def add_rule({4, s, d, "block"}) do
+ exec!("iptables -A fireguard -s #{s} -d #{d} -j DROP")
+ end
+
+ def add_rule({4, s, d, "allow"}) do
+ exec!("iptables -A fireguard -s #{s} -d #{d} -j ACCEPT")
+ end
+
+ def add_rule({6, s, d, "block"}) do
+ exec!("iptables6 -A fireguard -s #{s} -d #{d} -j DROP")
+ end
+
+ def add_rule({6, s, d, "allow"}) do
+ exec!("iptables6 -A fireguard -s #{s} -d #{d} -j ACCEPT")
+ end
+
+ @doc """
+ Deletes iptables rule.
+ """
+ def delete_rule({4, s, d, "block"}) do
+ exec!("iptables -D fireguard -s #{s} -d #{d} -j DROP")
+ end
+
+ def delete_rule({4, s, d, "allow"}) do
+ exec!("iptables -D fireguard -s #{s} -d #{d} -j ACCEPT")
+ end
+
+ def delete_rule({6, s, d, "block"}) do
+ exec!("iptables6 -D fireguard -s #{s} -d #{d} -j DROP")
+ end
+
+ def delete_rule({6, s, d, "allow"}) do
+ exec!("iptables6 -D fireguard -s #{s} -d #{d} -j ACCEPT")
+ end
+end
diff --git a/apps/fg_wall/lib/fg_wall/cli/sandbox.ex b/apps/fg_wall/lib/fg_wall/cli/sandbox.ex
new file mode 100644
index 000000000..b1cffdf62
--- /dev/null
+++ b/apps/fg_wall/lib/fg_wall/cli/sandbox.ex
@@ -0,0 +1,7 @@
+defmodule FgWall.CLI.Sandbox do
+ @default_returned ""
+
+ def add_rule(_rule_spec), do: @default_returned
+ def delete_rule(_rule_spec), do: @default_returned
+ def restore(_fg_http_rules), do: @default_returned
+end
diff --git a/apps/fg_wall/lib/fg_wall/server.ex b/apps/fg_wall/lib/fg_wall/server.ex
new file mode 100644
index 000000000..a728a69a3
--- /dev/null
+++ b/apps/fg_wall/lib/fg_wall/server.ex
@@ -0,0 +1,40 @@
+defmodule FgWall.Server do
+ @moduledoc """
+ Functions for applying firewall rules.
+
+ Startup:
+ Clear firewall rules.
+
+ Received events:
+ - set_rules: apply rules
+ """
+
+ use GenServer
+ import FgWall.CLI
+
+ @process_opts Application.compile_env(:fg_wall, :server_process_opts)
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, %{}, @process_opts)
+ end
+
+ @impl true
+ def init(rules) do
+ {:ok, rules}
+ end
+
+ def handle_call({:add_rule, rule_spec}, rules) do
+ cli().add_rule(rule_spec)
+ {:reply, :rule_added, rules}
+ end
+
+ def handle_call({:delete_rule, rule_spec}, rules) do
+ cli().delete_rule(rule_spec)
+ {:reply, :rule_deleted, rules}
+ end
+
+ def handle_call({:set_rules, fg_http_rules}, rules) do
+ cli().restore(fg_http_rules)
+ {:reply, :rules_set, rules}
+ end
+end
diff --git a/apps/fg_wall/mix.exs b/apps/fg_wall/mix.exs
index 345b9bd1c..0d4b28945 100644
--- a/apps/fg_wall/mix.exs
+++ b/apps/fg_wall/mix.exs
@@ -33,6 +33,7 @@ defmodule FgWall.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
+ {:fg_common, in_umbrella: true},
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
{:excoveralls, "~> 0.13", only: :test}
]
diff --git a/apps/fg_wall/test/fg_wall_test.exs b/apps/fg_wall/test/fg_wall_test.exs
index 671a9ae28..4cdae77bb 100644
--- a/apps/fg_wall/test/fg_wall_test.exs
+++ b/apps/fg_wall/test/fg_wall_test.exs
@@ -1,8 +1,4 @@
defmodule FgWallTest do
use ExUnit.Case
doctest FgWall
-
- test "greets the world" do
- assert FgWall.hello() == :world
- end
end
diff --git a/config/config.exs b/config/config.exs
index e0fb536f8..07d4470b3 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -25,17 +25,25 @@ import Config
config :phoenix, :json_library, Jason
config :fg_http,
- ecto_repos: [FgHttp.Repo]
+ ecto_repos: [FgHttp.Repo],
+ vpn_endpoint: "127.0.0.1:51820",
+ admin_user_email: "fireguard@localhost",
+ event_helpers_module: FgHttpWeb.Events.Device,
+ disable_signup:
+ (case System.get_env("DISABLE_SIGNUP") do
+ d when d in ["1", "yes"] -> true
+ _ -> false
+ end)
+
+config :fg_wall,
+ cli: FgWall.CLI.Sandbox,
+ server_process_opts: []
# This will be changed per-env
config :fg_vpn,
private_key: "UAeZoaY95pKZE1Glq28sI2GJDfGGRFtlb4KC6rjY2Gs=",
- cli: FgVpn.CLI.Sandbox
-
-# This will be changed per-env by ENV vars
-config :fg_http,
- vpn_endpoint: "127.0.0.1:51820",
- admin_user_email: "fireguard@localhost"
+ cli: FgVpn.CLI.Sandbox,
+ server_process_opts: []
# Configures the endpoint
# These will be overridden at runtime in production by config/releases.exs
@@ -44,10 +52,6 @@ config :fg_http, FgHttpWeb.Endpoint,
render_errors: [view: FgHttpWeb.ErrorView, accepts: ~w(html json)],
pubsub_server: FgHttp.PubSub
-config :fg_http, :event_helpers_module, FgHttpWeb.Events.Device
-
-config :fg_vpn, :server_process_opts, []
-
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
@@ -57,6 +61,7 @@ config :logger, :console,
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
+# Configures the vault
config :fg_http, FgHttp.Vault,
ciphers: [
default: {
@@ -72,10 +77,3 @@ config :fg_http, FgHttp.Vault,
iv_length: 12
}
]
-
-config :fg_http,
- disable_signup:
- (case System.get_env("DISABLE_SIGNUP") do
- d when d in ["1", "yes"] -> true
- _ -> false
- end)
diff --git a/config/dev.exs b/config/dev.exs
index bae01a2aa..9f0f1a61a 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -81,3 +81,4 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
config :fg_vpn, :server_process_opts, name: {:global, :fg_vpn_server}
+config :fg_wall, :server_process_opts, name: {:global, :fg_wall_server}
diff --git a/config/prod.exs b/config/prod.exs
index 9b4716063..72daf9b3c 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -9,7 +9,13 @@ import Config
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
-config :fg_vpn, :server_process_opts, name: :fg_vpn_server
+config :fg_vpn,
+ cli: FgVpn.CLI.Live,
+ server_process_opts: [name: {:global, :fg_vpn_server}]
+
+config :fg_wall,
+ cli: FgWall.CLI.Live,
+ server_process_opts: [name: {:global, :fg_wall_server}]
config :fg_http, FgHttpWeb.Endpoint,
cache_static_manifest: "priv/static/cache_manifest.json",
@@ -72,9 +78,6 @@ config :fg_http, FgHttpWeb.Endpoint,
server: true,
force_ssl: [rewrite_on: [:x_forwarded_proto], hsts: true, host: nil]
-config :fg_vpn,
- cli: FgVpn.CLI.Live
-
# Do not print debug messages in production
config :logger, level: :info
|