From f989eca5be55e88ef334559fe5565d624fc57f07 Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sun, 24 Jan 2021 21:00:43 -0800 Subject: [PATCH] Rework how fg_vpn handles interfacing with the OS --- .tool-versions | 2 +- apps/fg_http/lib/fg_http/devices.ex | 14 +- apps/fg_http/lib/fg_http/devices/device.ex | 2 + apps/fg_http/lib/fg_http/repo.ex | 8 + .../controllers/device_controller.ex | 6 +- .../lib/fg_http_web/live/new_device_live.ex | 4 +- .../20200228145810_create_devices.exs | 2 + .../fg_http_web/live/new_device_live_test.exs | 4 +- apps/fg_vpn/lib/fg_vpn/application.ex | 2 +- apps/fg_vpn/lib/fg_vpn/cli.ex | 74 +-- apps/fg_vpn/lib/fg_vpn/cli/live.ex | 138 +++++ apps/fg_vpn/lib/fg_vpn/cli/sandbox.ex | 24 + apps/fg_vpn/lib/fg_vpn/config.ex | 204 +----- apps/fg_vpn/lib/fg_vpn/peer.ex | 9 + apps/fg_vpn/lib/fg_vpn/server.ex | 83 ++- apps/fg_vpn/mix.exs | 8 - apps/fg_vpn/native/fgvpn_server/.cargo/config | 5 - apps/fg_vpn/native/fgvpn_server/Cargo.lock | 584 ------------------ apps/fg_vpn/native/fgvpn_server/Cargo.toml | 14 - apps/fg_vpn/native/fgvpn_server/README.md | 22 - apps/fg_vpn/native/fgvpn_server/src/lib.rs | 6 - apps/fg_vpn/test/fg_vpn/cli/sandbox_test.exs | 54 ++ apps/fg_vpn/test/fg_vpn/cli_test.exs | 22 - apps/fg_vpn/test/fg_vpn/config_test.exs | 138 +---- apps/fg_vpn/test/fg_vpn/server_test.exs | 56 ++ config/config.exs | 5 +- config/dev.exs | 3 - config/prod.exs | 3 +- config/releases.exs | 12 +- config/test.exs | 6 +- pkg/debian/DEBIAN/postinst | 19 +- .../lib/systemd/system/fireguard.service | 1 + scripts/generate_keypairs.sh | 10 + 33 files changed, 477 insertions(+), 1067 deletions(-) create mode 100644 apps/fg_vpn/lib/fg_vpn/cli/live.ex create mode 100644 apps/fg_vpn/lib/fg_vpn/cli/sandbox.ex create mode 100644 apps/fg_vpn/lib/fg_vpn/peer.ex delete mode 100644 apps/fg_vpn/native/fgvpn_server/.cargo/config delete mode 100644 apps/fg_vpn/native/fgvpn_server/Cargo.lock delete mode 100644 apps/fg_vpn/native/fgvpn_server/Cargo.toml delete mode 100644 apps/fg_vpn/native/fgvpn_server/README.md delete mode 100644 apps/fg_vpn/native/fgvpn_server/src/lib.rs create mode 100644 apps/fg_vpn/test/fg_vpn/cli/sandbox_test.exs delete mode 100644 apps/fg_vpn/test/fg_vpn/cli_test.exs create mode 100644 apps/fg_vpn/test/fg_vpn/server_test.exs create mode 100755 scripts/generate_keypairs.sh diff --git a/.tool-versions b/.tool-versions index 8bb24cb59..38ee9f164 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ elixir 1.11.3-otp-23 -erlang 23.2.1 +erlang 23.2.3 nodejs 14.15.4 python 3.7.9 diff --git a/apps/fg_http/lib/fg_http/devices.ex b/apps/fg_http/lib/fg_http/devices.ex index 648a0d01e..7a12e3f01 100644 --- a/apps/fg_http/lib/fg_http/devices.ex +++ b/apps/fg_http/lib/fg_http/devices.ex @@ -4,9 +4,7 @@ defmodule FgHttp.Devices do """ import Ecto.Query, warn: false - alias FgHttp.Repo - - alias FgHttp.Devices.Device + alias FgHttp.{Devices.Device, Repo} def list_devices(user_id) do Repo.all(from d in Device, where: d.user_id == ^user_id) @@ -49,4 +47,14 @@ defmodule FgHttp.Devices do def change_device(%Device{} = device) do Device.changeset(device, %{}) end + + def to_peer_list do + for device <- Repo.all(Device) do + %{ + public_key: device.public_key, + allowed_ips: device.allowed_ips, + preshared_key: device.preshared_key + } + end + end end diff --git a/apps/fg_http/lib/fg_http/devices/device.ex b/apps/fg_http/lib/fg_http/devices/device.ex index 2d810d2d4..a546236aa 100644 --- a/apps/fg_http/lib/fg_http/devices/device.ex +++ b/apps/fg_http/lib/fg_http/devices/device.ex @@ -11,6 +11,8 @@ defmodule FgHttp.Devices.Device do schema "devices" do field :name, :string field :public_key, :string + field :allowed_ips, :string + field :preshared_key, :string field :server_pubkey, :string, virtual: true field :private_key, :string, virtual: true field :ifname, :string diff --git a/apps/fg_http/lib/fg_http/repo.ex b/apps/fg_http/lib/fg_http/repo.ex index 8e42d8dbe..f23c28268 100644 --- a/apps/fg_http/lib/fg_http/repo.ex +++ b/apps/fg_http/lib/fg_http/repo.ex @@ -2,4 +2,12 @@ defmodule FgHttp.Repo do use Ecto.Repo, otp_app: :fg_http, adapter: Ecto.Adapters.Postgres + + alias FgHttp.Devices + alias Phoenix.PubSub + + def init(_) do + # Notify FgVpn.Server the config has been loaded + PubSub.broadcast(:fg_http_pub_sub, "server", {:set_config, Devices.to_peer_list()}) + 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 2dc0328ec..a7ea739e4 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 @@ -31,7 +31,11 @@ defmodule FgHttpWeb.DeviceController do case Devices.create_device(all_params) do {:ok, device} -> - PubSub.broadcast(:fg_http_pub_sub, "config", {:commit_device, device.public_key}) + PubSub.broadcast(:fg_http_pub_sub, "server", { + :commit_peer, + Map.take(device, [:public_key, :allowed_ips, :preshared_key]) + }) + redirect(conn, to: Routes.device_path(conn, :index)) {:error, %Ecto.Changeset{} = changeset} -> diff --git a/apps/fg_http/lib/fg_http_web/live/new_device_live.ex b/apps/fg_http/lib/fg_http_web/live/new_device_live.ex index 45bbb9514..ea2d9e410 100644 --- a/apps/fg_http/lib/fg_http_web/live/new_device_live.ex +++ b/apps/fg_http/lib/fg_http_web/live/new_device_live.ex @@ -25,7 +25,7 @@ defmodule FgHttpWeb.NewDeviceLive do end # Fire off event to generate private key, psk, and add device to config - PubSub.broadcast(:fg_http_pub_sub, "config", {:new_device}) + PubSub.broadcast(:fg_http_pub_sub, "server", {:new_peer}) device = %Device{user_id: user_id} @@ -36,7 +36,7 @@ defmodule FgHttpWeb.NewDeviceLive do Handles device added. """ @impl true - def handle_info({:peer_generated, privkey, pubkey, server_pubkey}, socket) do + def handle_info({:device_generated, privkey, pubkey, server_pubkey}, socket) do device = %{ socket.assigns.device | public_key: pubkey, 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 6fcb7b592..17816b5cb 100644 --- a/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs +++ b/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs @@ -5,6 +5,8 @@ defmodule FgHttp.Repo.Migrations.CreateDevices do create table(:devices) do add :name, :string, null: false add :public_key, :string, null: false + add :allowed_ips, :string + add :preshared_key, :string add :ifname, :string, null: false add :last_ip, :inet add :user_id, references(:users, on_delete: :delete_all), null: false diff --git a/apps/fg_http/test/fg_http_web/live/new_device_live_test.exs b/apps/fg_http/test/fg_http_web/live/new_device_live_test.exs index c81634bbc..d73fd603b 100644 --- a/apps/fg_http/test/fg_http_web/live/new_device_live_test.exs +++ b/apps/fg_http/test/fg_http_web/live/new_device_live_test.exs @@ -12,8 +12,8 @@ defmodule FgHttpWeb.NewDeviceLiveTest do test "mount and handle_info/2", %{authed_conn: conn} do {:ok, view, html} = live_isolated(conn, FgHttpWeb.NewDeviceLive) assert html =~ "Adding new peer to WireGuard server..." - assert render(view) =~ "Adding new peer to WireGuard server..." - send(view.pid, {:peer_generated, "test-privkey", "test-pubkey", "server-pubkey"}) + assert render(view) =~ "Add the following to your WireGuard" + send(view.pid, {:device_generated, "test-privkey", "test-pubkey", "server-pubkey"}) result = render(view) assert result =~ "test-pubkey" assert result =~ "server-pubkey" diff --git a/apps/fg_vpn/lib/fg_vpn/application.ex b/apps/fg_vpn/lib/fg_vpn/application.ex index 30de337ec..1f2172ad4 100644 --- a/apps/fg_vpn/lib/fg_vpn/application.ex +++ b/apps/fg_vpn/lib/fg_vpn/application.ex @@ -7,7 +7,7 @@ defmodule FgVpn.Application do def start(_type, _args) do children = [ - FgVpn.Config + FgVpn.Server ] # See https://hexdocs.pm/elixir/Supervisor.html diff --git a/apps/fg_vpn/lib/fg_vpn/cli.ex b/apps/fg_vpn/lib/fg_vpn/cli.ex index 57c082057..ec09a1bcb 100644 --- a/apps/fg_vpn/lib/fg_vpn/cli.ex +++ b/apps/fg_vpn/lib/fg_vpn/cli.ex @@ -1,77 +1,9 @@ defmodule FgVpn.CLI do @moduledoc """ - Wraps command-line functionality of WireGuard for our purposes. - - Application startup: - - wg syncconf - - Consumed events: - - add device: - 1. start listening for new connections - 2. send pubkey when device connects - 3. when verification received from fg_http, add config entry - - - remove device: - 1. disconnect device if connected - 2. remove configuration entry - - Produced events: - - client connects: - 1. send IP, connection time to FgHttp - - change config - - Helpers: - - render_conf: re-renders configuration file (peer configurations specifically) - - sync_conf: calls "wg syncconf" + Determines adapter to use for CLI commands. """ - @default_interface_cmd "route | grep '^default' | grep -o '[^ ]*$'" - - # Outputs the privkey, then pubkey on the next line - @genkey_cmd "wg genkey | tee >(wg pubkey)" - - @doc """ - Finds default egress interface on a Linux box. - """ - def default_interface do - case :os.type() do - {:unix, :linux} -> - exec(@default_interface_cmd) - |> String.split() - |> List.first() - - {:unix, :darwin} -> - # XXX: Figure out what it means to have macOS as a host? - "en0" - end - end - - @doc """ - Calls wg genkey - """ - def genkey do - [privkey, pubkey] = - exec(@genkey_cmd) - |> String.trim() - |> String.split("\n") - - {privkey, pubkey} - end - - def pubkey(privkey) when is_nil(privkey), do: nil - - def pubkey(privkey) when is_binary(privkey) do - exec("echo #{privkey} | wg pubkey") - |> String.trim() - end - - defp exec(cmd) do - case System.cmd("bash", ["-c", cmd]) do - {result, 0} -> - result - - {error, _} -> - raise "Error executing command: #{error}" - end + def cli do + Application.get_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 new file mode 100644 index 000000000..d4f1ed869 --- /dev/null +++ b/apps/fg_vpn/lib/fg_vpn/cli/live.ex @@ -0,0 +1,138 @@ +defmodule FgVpn.CLI.Live do + @moduledoc """ + A low-level module that wraps common WireGuard CLI operations. + Currently, these are very Linux-specific. In the future, + this could be generalized to any *nix platform that supports a WireGuard + client. + + See FgVpn.Server for higher-level functionality. + """ + + @egress_interface_cmd "route | grep '^default' | grep -o '[^ ]*$'" + + # Outputs the privkey, then pubkey on the next line + @genkey_cmd "wg genkey | tee >(wg pubkey)" + + @iface_name "wg-fireguard" + + def setup do + create_interface() + setup_iptables() + up_interface() + end + + def teardown do + down_interface() + teardown_iptables() + delete_interface() + end + + @doc """ + Calls wg genkey + """ + def genkey do + [privkey, pubkey] = + exec!(@genkey_cmd) + |> String.trim() + |> String.split("\n") + + {privkey, pubkey} + end + + def pubkey(privkey) when is_nil(privkey), do: nil + + def pubkey(privkey) when is_binary(privkey) do + exec!("echo #{privkey} | wg pubkey") + |> String.trim() + end + + def set(config_str) do + exec!("wg set #{@iface_name} #{config_str}") + end + + def show_latest_handshakes do + show("latest-handshakes") + end + + def show_persistent_keepalives do + show("persistent-keepalives") + end + + def show_transfer do + show("transfer") + end + + defp egress_interface do + case :os.type() do + {:unix, :linux} -> + exec!(@egress_interface_cmd) + |> String.split() + |> List.first() + + {:unix, :darwin} -> + # XXX: Figure out what it means to have macOS as a host? + "en0" + end + end + + def exec!(cmd) do + case bash(cmd) do + {result, 0} -> + result + + {error, _} -> + raise "Error executing command: #{error}. FireGuard cannot recover from this error." + end + end + + defp show(subcommand) do + exec!("wg show #{@iface_name} #{subcommand}") + end + + defp interface_exists do + case bash("ifconfig -a | grep #{@iface_name}") do + {_result, 0} -> true + {_error, 1} -> false + end + end + + defp create_interface do + unless interface_exists() do + exec!("ip link add dev #{@iface_name} type wireguard") + end + end + + defp delete_interface do + if interface_exists() do + exec!("ip link dev delete #{@iface_name}") + end + end + + 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" + ) + end + + 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" + ) + end + + defp up_interface do + exec!("ifconfig #{@iface_name} up") + end + + 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/cli/sandbox.ex b/apps/fg_vpn/lib/fg_vpn/cli/sandbox.ex new file mode 100644 index 000000000..6783b45c6 --- /dev/null +++ b/apps/fg_vpn/lib/fg_vpn/cli/sandbox.ex @@ -0,0 +1,24 @@ +defmodule FgVpn.CLI.Sandbox do + @moduledoc """ + Sandbox CLI environment for WireGuard CLI operations. + """ + + @show_latest_handshakes "4 seconds ago" + @show_persistent_keepalives "every 25 seconds" + @show_transfer "4.60 MiB received, 59.21 MiB sent" + @default_returned "" + + def egress_interface, do: "eth0" + def setup, do: @default_returned + def teardown, do: @default_returned + def genkey, do: {rand_key(), rand_key()} + def pubkey(_privkey), do: rand_key() + def exec!(_cmd), do: @default_returned + def set(_conf_str), do: @default_returned + def show_latest_handshakes, do: @show_latest_handshakes + def show_persistent_keepalives, do: @show_persistent_keepalives + def show_transfer, do: @show_transfer + + # Generate extremely fake keys in Sandbox mode + defp rand_key, do: :crypto.strong_rand_bytes(32) |> Base.encode64() +end diff --git a/apps/fg_vpn/lib/fg_vpn/config.ex b/apps/fg_vpn/lib/fg_vpn/config.ex index 9ad477346..e16175d31 100644 --- a/apps/fg_vpn/lib/fg_vpn/config.ex +++ b/apps/fg_vpn/lib/fg_vpn/config.ex @@ -1,198 +1,34 @@ defmodule FgVpn.Config do @moduledoc """ - Functions for reading / writing the WireGuard config + Functions for managing the FireGuard configuration. """ - alias FgVpn.CLI - alias Phoenix.PubSub - use GenServer + @default_interface_ip "172.16.59.1" - @config_header """ - # This file is being managed by the fireguard systemd service. Any changes - # will be overwritten eventually. + import FgVpn.CLI - """ + defstruct interface_ip: @default_interface_ip, + listen_port: 51_820, + peers: MapSet.new([]), + uncommitted_peers: MapSet.new([]) - def start_link(_) do - # Load existing config from file then write it so we start with a clean slate. - config = read_and_rewrite_config() - GenServer.start_link(__MODULE__, config) - end - - @impl true - def init(config) do - # Subscribe to PubSub from FgHttp application - {PubSub.subscribe(:fg_http_pub_sub, "config"), config} - end - - @impl true - def handle_info({:new_device}, config) do - {_server_privkey, server_pubkey} = read_privkey() - {privkey, pubkey} = CLI.genkey() - uncommitted_peers = MapSet.put(config[:uncommitted_peers], pubkey) - new_config = Map.put(config, :uncommitted_peers, uncommitted_peers) - - PubSub.broadcast( - :fg_http_pub_sub, - "view", - {:peer_generated, privkey, pubkey, server_pubkey} - ) - - {:noreply, new_config} - end - - @impl true - def handle_info({:commit_device, pubkey}, config) do - new_config = - if MapSet.member?(config[:uncommitted_peers], pubkey) do - new_peers = MapSet.put(config[:peers], pubkey) - new_uncommitted_peers = MapSet.delete(config[:uncommitted_peers], pubkey) - - config - |> Map.put(:uncommitted_peers, new_uncommitted_peers) - |> Map.put(:peers, new_peers) - else - config - end - - write!(new_config) - - {:noreply, new_config} - end - - @impl true - def handle_info({:remove_device, pubkey}, config) do - new_peers = MapSet.delete(config[:peers], pubkey) - new_config = %{config | peers: new_peers} - write!(new_config) - {:noreply, new_config} - end - - @doc """ - Writes configuration file. - """ - def write!(config) do - Application.get_env(:fg_vpn, :wireguard_conf_path) - |> File.write!(render(config)) - end - - @doc """ - Reads PrivateKey from configuration file - - Returns a {privkey, pubkey} tuple - """ - def read_privkey do - read_config_file() - |> extract_privkey() - |> (&{&1, CLI.pubkey(&1)}).() - end - - defp extract_privkey(config_str) do - ~r/PrivateKey = (.*)/ - |> Regex.scan(config_str || "") - |> List.flatten() - |> List.last() - end - - @doc """ - Reads configuration file and generates a list of pubkeys - """ - def read_peers do - read_config_file() - |> extract_pubkeys() - end - - @doc """ - Extracts pubkeys from a configuration file snippet - """ - def extract_pubkeys(config_str) do - case config_str do - nil -> - nil - - _ -> - ~r/PublicKey = (.*)/ - |> Regex.scan(config_str) - |> Enum.map(fn match_list -> List.last(match_list) end) - |> MapSet.new() - end - end - - @doc """ - Renders WireGuard config in a deterministic way. - """ def render(config) do - @config_header <> interface_to_config(config) <> peers_to_config(config) + "private-key #{private_key()} listen-port #{config.listen_port} " <> + Enum.join( + for peer <- config.peers do + "peer #{peer.public_key} allowed-ips #{peer.allowed_ips} preshared-key #{ + peer.preshared_key + }" + end, + " " + ) end - defp listen_port do - Application.get_env(:fg_http, :vpn_endpoint) - |> String.split(":") - |> List.last() + def private_key do + Application.get_env(:fg_vpn, :private_key) end - defp interface_to_config(config) do - ~s""" - [Interface] - ListenPort = #{listen_port()} - PrivateKey = #{config[:privkey]} - PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o #{ - config[:default_int] - } -j MASQUERADE - PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o #{ - config[:default_int] - } -j MASQUERADE - - """ - end - - defp read_config_file do - path = Application.get_env(:fg_vpn, :wireguard_conf_path) - - case File.read(path) do - {:ok, str} -> - str - - {:error, reason} -> - IO.puts(:stderr, "Could not read config: #{reason}") - nil - end - end - - defp peers_to_config(config) do - Enum.map_join(config[:peers], fn pubkey -> - ~s""" - # BEGIN PEER #{pubkey} - [Peer] - PublicKey = #{pubkey} - AllowedIPs = 0.0.0.0/0, ::/0 - # END PEER #{pubkey} - """ - end) - end - - defp get_server_keys do - {privkey, pubkey} = read_privkey() - - if is_nil(privkey) do - CLI.genkey() - else - {privkey, pubkey} - end - end - - defp read_and_rewrite_config do - {privkey, _pubkey} = get_server_keys() - - config = %{ - default_int: CLI.default_interface(), - privkey: privkey, - peers: read_peers() || MapSet.new([]), - uncommitted_peers: MapSet.new([]) - } - - write!(config) - - config + def public_key do + cli().pubkey(private_key()) end end diff --git a/apps/fg_vpn/lib/fg_vpn/peer.ex b/apps/fg_vpn/lib/fg_vpn/peer.ex new file mode 100644 index 000000000..b778346eb --- /dev/null +++ b/apps/fg_vpn/lib/fg_vpn/peer.ex @@ -0,0 +1,9 @@ +defmodule FgVpn.Peer do + @moduledoc """ + Represents a WireGuard peer. + """ + + defstruct public_key: nil, + allowed_ips: "0.0.0.0/0,::/0", + preshared_key: nil +end diff --git a/apps/fg_vpn/lib/fg_vpn/server.ex b/apps/fg_vpn/lib/fg_vpn/server.ex index 07a87278d..4fe12667f 100644 --- a/apps/fg_vpn/lib/fg_vpn/server.ex +++ b/apps/fg_vpn/lib/fg_vpn/server.ex @@ -1,9 +1,86 @@ defmodule FgVpn.Server do @moduledoc """ - Module to load boringtun lib as a server + Functions for reading / writing the WireGuard config + + Startup: + Set empty config + + Received events: + - start: set config and apply it + - new_peer: gen peer pubkey, return it, but don't apply config + - commit_peer: commit pending peer to config + - remove_peer: remove peer """ - use Rustler, otp_app: :fg_vpn, crate: :fgvpn_server + alias FgVpn.{Config, Peer} + alias Phoenix.PubSub + use GenServer + import FgVpn.CLI - def add(_arg1, _arg2), do: :erlang.nif_error(:nif_not_loaded) + def start_link(_) do + cli().setup() + GenServer.start_link(__MODULE__, %Config{}) + end + + @impl true + def init(config) do + # Subscribe to PubSub from FgHttp application + {PubSub.subscribe(:fg_http_pub_sub, "server"), config} + end + + @impl true + def handle_info({:new_peer}, config) do + server_pubkey = Config.public_key() + {privkey, pubkey} = cli().genkey() + uncommitted_peers = MapSet.put(config.uncommitted_peers, pubkey) + new_config = Map.put(config, :uncommitted_peers, uncommitted_peers) + + PubSub.broadcast( + :fg_http_pub_sub, + "view", + {:device_generated, privkey, pubkey, server_pubkey} + ) + + {: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]) + + config + |> Map.put(:uncommitted_peers, new_uncommitted_peers) + |> Map.put(:peers, new_peers) + else + config + end + + apply(new_config) + + {:noreply, new_config} + end + + @impl true + def handle_info({:remove_peer, 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} + end + + @impl true + def handle_cast({:set_config, new_config}, _config) do + {:noreply, new_config} + end + + @doc """ + Apply configuration to interface. + """ + def apply(config) do + cli().set(Config.render(config)) + end end diff --git a/apps/fg_vpn/mix.exs b/apps/fg_vpn/mix.exs index c26b9d25d..ecc2f5623 100644 --- a/apps/fg_vpn/mix.exs +++ b/apps/fg_vpn/mix.exs @@ -6,19 +6,12 @@ defmodule FgVpn.MixProject do app: :fg_vpn, version: "0.1.7", build_path: "../../_build", - compilers: [:rustler] ++ Mix.compilers(), config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.11", start_permanent: Mix.env() == :prod, test_coverage: [tool: ExCoveralls], - rustler_crates: [ - fgvpn_server: [ - cargo: {:rustup, "stable"}, - mode: :release - ] - ], preferred_cli_env: [ coveralls: :test, "coveralls.detail": :test, @@ -41,7 +34,6 @@ defmodule FgVpn.MixProject do defp deps do [ {:fg_http, in_umbrella: true}, - {:rustler, "~> 0.22.0-rc.0"}, {:phoenix_pubsub, "~> 2.0"}, {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:excoveralls, "~> 0.13", only: :test} diff --git a/apps/fg_vpn/native/fgvpn_server/.cargo/config b/apps/fg_vpn/native/fgvpn_server/.cargo/config deleted file mode 100644 index a125291f6..000000000 --- a/apps/fg_vpn/native/fgvpn_server/.cargo/config +++ /dev/null @@ -1,5 +0,0 @@ -[target.x86_64-apple-darwin] -rustflags = [ - "-C", "link-arg=-undefined", - "-C", "link-arg=dynamic_lookup", -] diff --git a/apps/fg_vpn/native/fgvpn_server/Cargo.lock b/apps/fg_vpn/native/fgvpn_server/Cargo.lock deleted file mode 100644 index 3ca08d162..000000000 --- a/apps/fg_vpn/native/fgvpn_server/Cargo.lock +++ /dev/null @@ -1,584 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "addr2line" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" - -[[package]] -name = "ascii" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "backtrace" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - -[[package]] -name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "boringtun" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d62ad6ff2f841f576887ffd3e94a4ae438784dc566c44b3fbd783e5d31e04c99" -dependencies = [ - "base64", - "chrono", - "clap", - "daemonize", - "hex", - "jni", - "libc", - "ring", - "spin", - "untrusted", -] - -[[package]] -name = "boxfnonce" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426" - -[[package]] -name = "bumpalo" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" - -[[package]] -name = "byteorder" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" - -[[package]] -name = "cc" -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "time", - "winapi", -] - -[[package]] -name = "clap" -version = "2.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" -dependencies = [ - "bitflags", - "strsim", - "textwrap", - "unicode-width", -] - -[[package]] -name = "combine" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" -dependencies = [ - "ascii", - "byteorder", - "either", - "memchr", - "unreachable", -] - -[[package]] -name = "daemonize" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c24513e34f53b640819f0ac9f705b673fcf4006d7aab8778bee72ebfc89815" -dependencies = [ - "boxfnonce", - "libc", -] - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "backtrace", - "version_check", -] - -[[package]] -name = "fgvpn_server" -version = "0.1.0" -dependencies = [ - "boringtun", - "rustler", -] - -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - -[[package]] -name = "heck" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hex" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" - -[[package]] -name = "jni" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecfa3b81afc64d9a6539c4eece96ac9a93c551c713a313800dade8e33d7b5c1" -dependencies = [ - "cesu8", - "combine", - "error-chain", - "jni-sys", - "log", - "walkdir", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" - -[[package]] -name = "log" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2" -dependencies = [ - "cfg-if 0.1.10", -] - -[[package]] -name = "memchr" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" - -[[package]] -name = "miniz_oxide" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" -dependencies = [ - "adler", - "autocfg", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" - -[[package]] -name = "once_cell" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" - -[[package]] -name = "proc-macro2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ring" -version = "0.16.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - -[[package]] -name = "rustler" -version = "0.22.0-rc.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6627953c4983f7808f569d4df17d7de55893ed1e1a85ca4ef5ab95569b83832e" -dependencies = [ - "lazy_static", - "rustler_codegen", - "rustler_sys", -] - -[[package]] -name = "rustler_codegen" -version = "0.22.0-rc.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b10e7791e788906c4394c980022210fbbeb75e75f7d9166b7bd0169e194ed4d" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "rustler_sys" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb96034ff33723615fd19223d58c987c1f6476342e83557a6e467ef95f83bda" -dependencies = [ - "unreachable", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "syn" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi", - "winapi", -] - -[[package]] -name = "unicode-segmentation" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" - -[[package]] -name = "unicode-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" - -[[package]] -name = "unicode-xid" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" - -[[package]] -name = "unreachable" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -dependencies = [ - "void", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "version_check" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" - -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - -[[package]] -name = "walkdir" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasm-bindgen" -version = "0.2.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" -dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" - -[[package]] -name = "web-sys" -version = "0.3.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/apps/fg_vpn/native/fgvpn_server/Cargo.toml b/apps/fg_vpn/native/fgvpn_server/Cargo.toml deleted file mode 100644 index 075e4a2d1..000000000 --- a/apps/fg_vpn/native/fgvpn_server/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "fgvpn_server" -version = "0.1.0" -authors = [] -edition = "2018" - -[lib] -name = "fgvpn_server" -path = "src/lib.rs" -crate-type = ["dylib"] - -[dependencies] -rustler = "0.22.0-rc.0" -boringtun = "0.3.0" diff --git a/apps/fg_vpn/native/fgvpn_server/README.md b/apps/fg_vpn/native/fgvpn_server/README.md deleted file mode 100644 index 1f5e43181..000000000 --- a/apps/fg_vpn/native/fgvpn_server/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# NIF for Elixir.FgVpn.Server - -## To build the NIF module: - -- Make sure your projects `mix.exs` has the `:rustler` compiler listed in the `project` function: `compilers: [:rustler] ++ Mix.compilers()` If there already is a `:compilers` list, you should append `:rustler` to it. -- Add your crate to the `rustler_crates` attribute in the `project function. [See here](https://hexdocs.pm/rustler/basics.html#crate-configuration). -- Your NIF will now build along with your project. - -## To load the NIF: - -```elixir -defmodule FgVpn.Server do - use Rustler, otp_app: :fg_vpn, crate: "fgvpn_server" - - # When your NIF is loaded, it will override this function. - def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded) -end -``` - -## Examples - -[This](https://github.com/hansihe/NifIo) is a complete example of a NIF written in Rust. diff --git a/apps/fg_vpn/native/fgvpn_server/src/lib.rs b/apps/fg_vpn/native/fgvpn_server/src/lib.rs deleted file mode 100644 index bad03a048..000000000 --- a/apps/fg_vpn/native/fgvpn_server/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[rustler::nif] -fn add(a: i64, b: i64) -> i64 { - a + b -} - -rustler::init!("Elixir.FgVpn.Server", [add]); diff --git a/apps/fg_vpn/test/fg_vpn/cli/sandbox_test.exs b/apps/fg_vpn/test/fg_vpn/cli/sandbox_test.exs new file mode 100644 index 000000000..40e11d1c1 --- /dev/null +++ b/apps/fg_vpn/test/fg_vpn/cli/sandbox_test.exs @@ -0,0 +1,54 @@ +defmodule FgVpn.CLI.SandboxTest do + use ExUnit.Case, async: true + + import FgVpn.CLI + + @expected_returned "" + + test "egress_interface" do + assert is_binary(cli().egress_interface()) + end + + test "setup" do + assert cli().setup() == @expected_returned + end + + test "teardown" do + assert cli().teardown() == @expected_returned + end + + test "genkey" do + {privkey, pubkey} = cli().genkey() + + assert is_binary(privkey) + assert is_binary(pubkey) + end + + test "pubkey" do + {privkey, _pubkey} = cli().genkey() + pubkey = cli().pubkey(privkey) + + assert is_binary(pubkey) + assert String.length(pubkey) == 44 + end + + test "exec!" do + assert cli().exec!("dummy") == @expected_returned + end + + test "set" do + assert cli().set("dummy") == @expected_returned + end + + test "show_latest_handshakes" do + assert cli().show_latest_handshakes() == "4 seconds ago" + end + + test "show_persistent_keepalives" do + assert cli().show_persistent_keepalives() == "every 25 seconds" + end + + test "show_transfer" do + assert cli().show_transfer() == "4.60 MiB received, 59.21 MiB sent" + end +end diff --git a/apps/fg_vpn/test/fg_vpn/cli_test.exs b/apps/fg_vpn/test/fg_vpn/cli_test.exs deleted file mode 100644 index 8e6a6803d..000000000 --- a/apps/fg_vpn/test/fg_vpn/cli_test.exs +++ /dev/null @@ -1,22 +0,0 @@ -defmodule FgVpn.CLITest do - use ExUnit.Case, async: true - - alias FgVpn.CLI - - test "default_interface" do - assert is_binary(CLI.default_interface()) - end - - test "genkey" do - {privkey, pubkey} = CLI.genkey() - - assert is_binary(privkey) - assert is_binary(pubkey) - end - - test "pubkey" do - {privkey, pubkey} = CLI.genkey() - - assert pubkey == CLI.pubkey(privkey) - end -end diff --git a/apps/fg_vpn/test/fg_vpn/config_test.exs b/apps/fg_vpn/test/fg_vpn/config_test.exs index cba470bd6..9771ab69a 100644 --- a/apps/fg_vpn/test/fg_vpn/config_test.exs +++ b/apps/fg_vpn/test/fg_vpn/config_test.exs @@ -1,128 +1,32 @@ defmodule FgVpn.ConfigTest do use ExUnit.Case, async: true - alias FgVpn.Config + alias FgVpn.{Config, Peer} - @test_privkey "GMqk2P3deotcQqgqJHfLGB1JtU//f1FgX868bfPKSVc=" + @default_config "private-key UAeZoaY95pKZE1Glq28sI2GJDfGGRFtlb4KC6rjY2Gs= listen-port 51820 " + @populated_config "private-key UAeZoaY95pKZE1Glq28sI2GJDfGGRFtlb4KC6rjY2Gs= listen-port 1 peer test-pubkey allowed-ips test-allowed-ips preshared-key test-preshared-key" - @empty """ - """ + describe "render" do + test "renders default config" do + config = %Config{} - @single_peer """ - # BEGIN PEER test-pubkey - [Peer] - PublicKey = test-pubkey - AllowedIPs = 0.0.0.0/0, ::/0 - # END PEER test-pubkey - """ - - @privkey """ - [Interface] - ListenPort = 51820 - PrivateKey = GMqk2P3deotcQqgqJHfLGB1JtU//f1FgX868bfPKSVc= - """ - - @rendered_privkey "kPCNOTbBoHC/j5daxhMHcZ+PeNr6oaA8qIWcBuFlM0s=" - - @rendered_config """ - # This file is being managed by the fireguard systemd service. Any changes - # will be overwritten eventually. - - [Interface] - ListenPort = 51820 - PrivateKey = kPCNOTbBoHC/j5daxhMHcZ+PeNr6oaA8qIWcBuFlM0s= - PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o noop -j MASQUERADE - PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o noop -j MASQUERADE - - # BEGIN PEER test-pubkey - [Peer] - PublicKey = test-pubkey - AllowedIPs = 0.0.0.0/0, ::/0 - # END PEER test-pubkey - """ - - def write_config(config) do - Application.get_env(:fg_vpn, :wireguard_conf_path) - |> File.write!(config) - end - - describe "state" do - setup %{stubbed_config: config} do - write_config(config) - test_pid = start_supervised!(Config) - - on_exit(fn -> - Application.get_env(:fg_vpn, :wireguard_conf_path) - |> File.rm!() - end) - - %{test_pid: test_pid} + assert Config.render(config) == @default_config end - @tag stubbed_config: @privkey - test "parses PrivateKey from config file", %{test_pid: test_pid} do - assert %{ - uncommitted_peers: %MapSet{}, - peers: %MapSet{}, - default_int: _, - privkey: @test_privkey - } = :sys.get_state(test_pid) - end + test "renders populated config" do + config = %Config{ + listen_port: 1, + peers: + MapSet.new([ + %Peer{ + public_key: "test-pubkey", + allowed_ips: "test-allowed-ips", + preshared_key: "test-preshared-key" + } + ]), + uncommitted_peers: MapSet.new(["uncommitted-pubkey"]) + } - @tag stubbed_config: @single_peer - test "parses peers from config file", %{test_pid: test_pid} do - assert %{ - uncommitted_peers: %MapSet{}, - peers: %MapSet{}, - default_int: _, - privkey: _ - } = :sys.get_state(test_pid) - end - - @tag stubbed_config: @empty - test "generates new peer when requested", %{test_pid: test_pid} do - send(test_pid, {:new_device}) - - # XXX: Avoid sleeping - Process.sleep(100) - - assert [_] = MapSet.to_list(:sys.get_state(test_pid)[:uncommitted_peers]) - 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, {:new_device}) - Process.sleep(100) - - [pubkey | _tail] = MapSet.to_list(:sys.get_state(test_pid)[:uncommitted_peers]) - - send(test_pid, {:commit_device, pubkey}) - - # XXX: Avoid sleeping - Process.sleep(100) - - assert MapSet.to_list(:sys.get_state(test_pid)[:peers]) == [pubkey] - end - - @tag stubbed_config: @single_peer - test "removes peers from config when device is removed", %{test_pid: test_pid} do - send(test_pid, {:remove_device, "test-pubkey"}) - - # XXX: Avoid sleeping - Process.sleep(100) - - assert MapSet.to_list(:sys.get_state(test_pid)[:peers]) == [] - end - end - - describe "loading / rendering" do - test "renders config" do - assert Config.render(%{ - default_int: "noop", - privkey: @rendered_privkey, - peers: MapSet.new(["test-pubkey"]), - uncommitted_peers: MapSet.new([]) - }) == @rendered_config + assert Config.render(config) == @populated_config end end end diff --git a/apps/fg_vpn/test/fg_vpn/server_test.exs b/apps/fg_vpn/test/fg_vpn/server_test.exs new file mode 100644 index 000000000..bf05734d8 --- /dev/null +++ b/apps/fg_vpn/test/fg_vpn/server_test.exs @@ -0,0 +1,56 @@ +defmodule FgVpn.ServerTest do + use ExUnit.Case, async: true + alias FgVpn.{Config, Peer, Server} + import FgVpn.CLI + + @empty %Config{} + @single_peer %Config{peers: MapSet.new([%Peer{public_key: "test-pubkey"}])} + + describe "state" do + setup %{stubbed_config: config} do + test_pid = start_supervised!(Server) + + GenServer.cast(test_pid, {:set_config, config}) + + on_exit(fn -> cli().teardown() end) + + %{test_pid: test_pid} + end + + @tag stubbed_config: @empty + test "generates new peer when requested", %{test_pid: test_pid} do + send(test_pid, {:new_peer}) + + # XXX: Avoid sleeping + Process.sleep(100) + + assert [_peer] = MapSet.to_list(:sys.get_state(test_pid).uncommitted_peers) + 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, {:new_peer}) + Process.sleep(100) + + [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"}) + + # XXX: Avoid sleeping + Process.sleep(100) + + assert MapSet.to_list(:sys.get_state(test_pid).peers) == [] + end + end +end diff --git a/config/config.exs b/config/config.exs index 60c368b25..dd06ca2e1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -29,11 +29,12 @@ config :fg_http, # This will be changed per-env config :fg_vpn, - wireguard_conf_path: "/opt/fireguard/wg-fireguard.conf" + private_key: "UAeZoaY95pKZE1Glq28sI2GJDfGGRFtlb4KC6rjY2Gs=", + cli: FgVpn.CLI.Sandbox # This will be changed per-env by ENV vars config :fg_http, - vpn_endpoint: "localhost:51820" + vpn_endpoint: "127.0.0.1:51820" # Configures the endpoint # These will be overridden at runtime in production by config/releases.exs diff --git a/config/dev.exs b/config/dev.exs index 6ff225301..dc34047ff 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -71,9 +71,6 @@ config :fg_http, FgHttpWeb.Endpoint, ] ] -config :fg_vpn, - wireguard_conf_path: Path.expand("~/.wg-fireguard.conf") - config :fg_http, disable_signup: (case System.get_env("DISABLE_SIGNUP") do diff --git a/config/prod.exs b/config/prod.exs index 394ec821a..c6a816741 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -70,7 +70,8 @@ config :fg_http, FgHttpWeb.Endpoint, server: true, force_ssl: [rewrite_on: [:x_forwarded_proto], hsts: true, host: nil] -config :fg_vpn, wireguard_conf_path: "/opt/fireguard/wg-fireguard.conf" +config :fg_vpn, + cli: FgVpn.CLI.Live # Do not print debug messages in production config :logger, level: :info diff --git a/config/releases.exs b/config/releases.exs index f0652f726..7d3527f11 100644 --- a/config/releases.exs +++ b/config/releases.exs @@ -4,6 +4,8 @@ # remember to add this file to your .gitignore. import Config +@default_egress_address_cmd "ip route get 8.8.8.8 | grep -oP 'src \\K\\S+'" + # Required environment variables database_url = System.get_env("DATABASE_URL") || @@ -52,11 +54,16 @@ ssl_ca_cert_file = s = _ -> s end +def default_egress_address do + FgVpn.CLI.Live.exec!(@default_egress_address_cmd) + |> String.trim() +end + # Optional environment variables pool_size = String.to_integer(System.get_env("POOL_SIZE") || "10") https_listen_port = String.to_integer(System.get_env("HTTPS_LISTEN_PORT") || "8800") wg_listen_port = System.get_env("WG_LISTEN_PORT" || "51820") -wg_listen_address = System.get_env("WG_LISTEN_ADDRESS") || "localhost" +wg_endpoint_address = System.get_env("WG_ENDPOINT_ADDRESS") || default_egress_address() url_host = System.get_env("URL_HOST") || "localhost" config :fg_http, disable_signup: disable_signup @@ -86,7 +93,8 @@ config :fg_http, FgHttpWeb.Endpoint, ] config :fg_vpn, - vpn_endpoint: wg_listen_address <> ":" <> wg_listen_port + vpn_endpoint: wg_endpoint_address <> ":" <> wg_listen_port, + private_key: File.read!("/opt/fireguard/server.key") |> String.trim() # ## Using releases (Elixir v1.9+) # diff --git a/config/test.exs b/config/test.exs index c70031be7..3b1d31606 100644 --- a/config/test.exs +++ b/config/test.exs @@ -25,9 +25,6 @@ config :fg_http, FgHttp.Repo, DBConfig.config(db_url) config :fg_http, FgHttp.Mailer, adapter: Bamboo.TestAdapter -config :fg_vpn, - wireguard_conf_path: Path.expand("#{__DIR__}/../apps/fg_vpn/test/fixtures/wg-fireguard.conf") - # We don't run a server during test. If one is required, # you can enable the server option below. config :fg_http, FgHttpWeb.Endpoint, @@ -40,3 +37,6 @@ config :fg_http, FgHttpWeb.Endpoint, # Print only warnings and errors during test config :logger, level: :warn + +config :fg_vpn, + execute_iface_cmds: System.get_env("CI") === "true" diff --git a/pkg/debian/DEBIAN/postinst b/pkg/debian/DEBIAN/postinst index 9efe8422e..2e656548c 100755 --- a/pkg/debian/DEBIAN/postinst +++ b/pkg/debian/DEBIAN/postinst @@ -12,7 +12,7 @@ if id fireguard &>/dev/null; then echo "fireguard user exists... not creating." else echo "creating system user fireguard" - useradd --system -G netdev fireguard + useradd --system fireguard fi # Generate app secrets @@ -29,9 +29,6 @@ sudo -i -u postgres psql -c "CREATE ROLE ${db_user} WITH LOGIN PASSWORD '${db_pa sudo -i -u postgres psql -c "CREATE DATABASE fireguard;" || true sudo -i -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE fireguard to ${db_user};" || true -# Generate WireGuard privkey -privkey=$(wg genkey) - # Write FireGuard SSL files mkdir -p /opt/fireguard/ssl chown -R fireguard:root /opt/fireguard/ssl @@ -45,9 +42,7 @@ chmod 0644 /opt/fireguard/ssl/cert.pem # Write FireGuard config files touch /opt/fireguard/config.env -touch /opt/fireguard/wg-fireguard.conf chmod 0600 /opt/fireguard/config.env -chmod 0660 /opt/fireguard/wg-fireguard.conf chown -R fireguard:root /opt/fireguard cat <> /opt/fireguard/config.env # This file is loaded into FireGuard's Environment upon launch to configure it. @@ -69,14 +64,15 @@ DATABASE_URL="ecto://${db_user}:${db_password}@127.0.0.1/fireguard" # The HTTPS port to listen on. Defaults to 8800. HTTPS_LISTEN_PORT=8800 -# The address to bind the HTTPS server to. Defaults to "0.0.0.0" -HTTPS_LISTEN_ADDRESS=0.0.0.0 +# The address to bind the HTTPS server to. Defaults to "127.0.0.1" +HTTPS_LISTEN_ADDRESS=127.0.0.1 # The WireGuard port to listen on. Defaults to 51820. WG_LISTEN_PORT=51820 -# The address to bind the WireGuard service to. Defaults to "0.0.0.0" -WG_LISTEN_ADDRESS=0.0.0.0 +# The address for the WireGuard endpoint. Defaults to the address of the +# default egress interface if not set. +WG_ENDPOINT_ADDRESS= # SSL certificate file and key path. Self-signed certs are generated for you on # install, but it's highly recommended to replace these with valid certs. @@ -94,3 +90,6 @@ URL_HOST=${hostname} # For public-facing sites, it's recommended to leave signups disabled. DISABLE_SIGNUP=yes EOT + +umask 077 +wg genkey > /opt/fireguard/server.key diff --git a/pkg/debian/lib/systemd/system/fireguard.service b/pkg/debian/lib/systemd/system/fireguard.service index 7bd801c30..6f8b57e46 100644 --- a/pkg/debian/lib/systemd/system/fireguard.service +++ b/pkg/debian/lib/systemd/system/fireguard.service @@ -7,6 +7,7 @@ After=postgresql.service Restart=on-failure RestartSec=1 User=fireguard +CapabilityBindingSet=CAP_NET_ADMIN EnvironmentFile=/opt/fireguard/config.env ExecStartPre=/opt/fireguard/bin/fireguard eval "FgHttp.Release.migrate" ExecStart=/opt/fireguard/bin/fireguard start diff --git a/scripts/generate_keypairs.sh b/scripts/generate_keypairs.sh new file mode 100755 index 000000000..ddca882d9 --- /dev/null +++ b/scripts/generate_keypairs.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env zsh +set -e + +# Generates 10 WireGuard keypairs for use in Dev/Test environments. +# Do not use in Prod. +repeat 10 { + key=$(wg genkey | tee >(wg pubkey)) + parts=("${(f)key}") + echo $parts +}