mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-28 10:18:51 +00:00
checkpoint
This commit is contained in:
@@ -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
|
||||
|
||||
4
apps/fg_common/.formatter.exs
Normal file
4
apps/fg_common/.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
27
apps/fg_common/.gitignore
vendored
Normal file
27
apps/fg_common/.gitignore
vendored
Normal file
@@ -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
|
||||
20
apps/fg_common/README.md
Normal file
20
apps/fg_common/README.md
Normal file
@@ -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).
|
||||
22
apps/fg_common/lib/cli.ex
Normal file
22
apps/fg_common/lib/cli.ex
Normal file
@@ -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
|
||||
5
apps/fg_common/lib/fg_common.ex
Normal file
5
apps/fg_common/lib/fg_common.ex
Normal file
@@ -0,0 +1,5 @@
|
||||
defmodule FgCommon do
|
||||
@moduledoc """
|
||||
Documentation for `FgCommon`.
|
||||
"""
|
||||
end
|
||||
33
apps/fg_common/mix.exs
Normal file
33
apps/fg_common/mix.exs
Normal file
@@ -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
|
||||
8
apps/fg_common/test/fg_common_test.exs
Normal file
8
apps/fg_common/test/fg_common_test.exs
Normal file
@@ -0,0 +1,8 @@
|
||||
defmodule FgCommonTest do
|
||||
use ExUnit.Case
|
||||
doctest FgCommon
|
||||
|
||||
test "greets the world" do
|
||||
assert FgCommon.hello() == :world
|
||||
end
|
||||
end
|
||||
1
apps/fg_common/test/test_helper.exs
Normal file
1
apps/fg_common/test/test_helper.exs
Normal file
@@ -0,0 +1 @@
|
||||
ExUnit.start()
|
||||
@@ -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
|
||||
%{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<th>Name</th>
|
||||
<th>Rules</th>
|
||||
<th>Public key</th>
|
||||
<th>Last IP</th>
|
||||
<th>Remote IP</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -17,7 +17,7 @@
|
||||
<td><%= device.name %></td>
|
||||
<td><%= link rules_title(device), to: Routes.device_rule_path(@conn, :index, device) %></td>
|
||||
<td><%= device.public_key %></td>
|
||||
<td><%= device.last_ip || "Never connected" %></td>
|
||||
<td><%= device.remote_ip || "Never connected" %></td>
|
||||
<td class="has-text-right">
|
||||
<span><%= link "Show", to: Routes.device_path(@conn, :show, device) %></span>
|
||||
|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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}
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} " <>
|
||||
|
||||
@@ -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 """
|
||||
|
||||
@@ -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}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
|
||||
9
apps/fg_wall/lib/fg_wall/cli.ex
Normal file
9
apps/fg_wall/lib/fg_wall/cli.ex
Normal file
@@ -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
|
||||
70
apps/fg_wall/lib/fg_wall/cli/live.ex
Normal file
70
apps/fg_wall/lib/fg_wall/cli/live.ex
Normal file
@@ -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
|
||||
7
apps/fg_wall/lib/fg_wall/cli/sandbox.ex
Normal file
7
apps/fg_wall/lib/fg_wall/cli/sandbox.ex
Normal file
@@ -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
|
||||
40
apps/fg_wall/lib/fg_wall/server.ex
Normal file
40
apps/fg_wall/lib/fg_wall/server.ex
Normal file
@@ -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
|
||||
@@ -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}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
defmodule FgWallTest do
|
||||
use ExUnit.Case
|
||||
doctest FgWall
|
||||
|
||||
test "greets the world" do
|
||||
assert FgWall.hello() == :world
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user