checkpoint

This commit is contained in:
Jamil Bou Kheir
2021-03-14 19:51:53 -07:00
parent 098951f569
commit cde4bfb875
44 changed files with 478 additions and 244 deletions

View File

@@ -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

View 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
View 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
View 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
View 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

View File

@@ -0,0 +1,5 @@
defmodule FgCommon do
@moduledoc """
Documentation for `FgCommon`.
"""
end

33
apps/fg_common/mix.exs Normal file
View 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

View File

@@ -0,0 +1,8 @@
defmodule FgCommonTest do
use ExUnit.Case
doctest FgCommon
test "greets the world" do
assert FgCommon.hello() == :world
end
end

View File

@@ -0,0 +1 @@
ExUnit.start()

View File

@@ -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
%{

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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>
|

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}
})

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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} " <>

View File

@@ -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 """

View File

@@ -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}
]

View File

@@ -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

View File

@@ -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"})

View File

@@ -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

View 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

View 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

View 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

View 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

View File

@@ -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}
]

View File

@@ -1,8 +1,4 @@
defmodule FgWallTest do
use ExUnit.Case
doctest FgWall
test "greets the world" do
assert FgWall.hello() == :world
end
end

View File

@@ -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)

View File

@@ -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}

View File

@@ -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