mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
test rendering of config
This commit is contained in:
57
apps/fg_vpn/lib/fg_vpn/cli.ex
Normal file
57
apps/fg_vpn/lib/fg_vpn/cli.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
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"
|
||||
"""
|
||||
|
||||
@default_interface_cmd "route | grep '^default' | grep -o '[^ ]*$'"
|
||||
|
||||
@doc """
|
||||
Finds default egress interface on a Linux box.
|
||||
"""
|
||||
def default_interface do
|
||||
case :os.type() do
|
||||
{:unix, :linux} ->
|
||||
case System.cmd("sh", ["-c", @default_interface_cmd]) do
|
||||
{result, 0} ->
|
||||
result
|
||||
|> String.split()
|
||||
|> List.first()
|
||||
|
||||
{_error, _} ->
|
||||
raise "Could not determine default egress interface from `#{@default_interface_cmd}`"
|
||||
end
|
||||
|
||||
{:unix, :darwin} ->
|
||||
# XXX: Figure out what it means to have macOS as a host?
|
||||
"en0"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calls wg genkey
|
||||
"""
|
||||
def gen_privkey do
|
||||
end
|
||||
end
|
||||
@@ -3,83 +3,136 @@ defmodule FgVpn.Config do
|
||||
Functions for reading / writing the WireGuard config
|
||||
"""
|
||||
|
||||
alias FgVpn.CLI
|
||||
alias Phoenix.PubSub
|
||||
use GenServer
|
||||
|
||||
@begin_sentinel "# BEGIN FIREGUARD-MANAGED PEER LIST"
|
||||
@end_sentinel "# END FIREGUARD-MANAGED PEER LIST"
|
||||
@config_header """
|
||||
# This file is being managed by the fireguard systemd service. Any changes
|
||||
# will be overwritten eventually.
|
||||
|
||||
"""
|
||||
|
||||
def start_link(_) do
|
||||
peers = read()
|
||||
GenServer.start_link(__MODULE__, peers)
|
||||
privkey = read_privkey()
|
||||
peers = read_peers() || []
|
||||
default_int = CLI.default_interface()
|
||||
|
||||
GenServer.start_link(
|
||||
__MODULE__,
|
||||
%{
|
||||
peers: peers,
|
||||
privkey: privkey,
|
||||
default_int: default_int
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(peers) do
|
||||
def init(config) do
|
||||
# Subscribe to PubSub from FgHttp application
|
||||
{PubSub.subscribe(:fg_http_pub_sub, "config"), peers}
|
||||
{PubSub.subscribe(:fg_http_pub_sub, "config"), config}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:verify_device, pubkey}, pubkeys) do
|
||||
new_peers = [pubkey | pubkeys]
|
||||
write!(new_peers)
|
||||
{:noreply, new_peers}
|
||||
def handle_info({:verify_device, pubkey}, config) do
|
||||
new_peers = [pubkey | config[:peers]]
|
||||
new_config = %{config | peers: new_peers}
|
||||
write!(new_config)
|
||||
{:noreply, new_config}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:remove_device, pubkey}, pubkeys) do
|
||||
new_peers = List.delete(pubkeys, pubkey)
|
||||
write!(new_peers)
|
||||
{:noreply, new_peers}
|
||||
def handle_info({:remove_device, pubkey}, config) do
|
||||
new_peers = List.delete(config[:peers], pubkey)
|
||||
new_config = %{config | peers: new_peers}
|
||||
write!(new_config)
|
||||
{:noreply, new_config}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes configuration file.
|
||||
"""
|
||||
def write!(peers) do
|
||||
def write!(config) do
|
||||
Application.get_env(:fg_vpn, :wireguard_conf_path)
|
||||
|> File.write!(render(peers))
|
||||
|> File.write!(render(config))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads PrivateKey from configuration file
|
||||
"""
|
||||
def read_privkey do
|
||||
read_config_file()
|
||||
|> extract_privkey()
|
||||
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 do
|
||||
def read_peers do
|
||||
read_config_file()
|
||||
|> extract_pubkeys()
|
||||
end
|
||||
|
||||
defp read_config_file do
|
||||
path = Application.get_env(:fg_vpn, :wireguard_conf_path)
|
||||
|
||||
case File.read(path) do
|
||||
{:ok, str} ->
|
||||
str
|
||||
|> String.split(@begin_sentinel)
|
||||
|> List.last()
|
||||
|> String.split(@end_sentinel)
|
||||
|> List.first()
|
||||
|> extract_pubkeys()
|
||||
|
||||
{:error, _reason} ->
|
||||
[]
|
||||
{:error, reason} ->
|
||||
IO.puts(:stderr, "Could not read config: #{reason}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts pubkeys from a configuration file snippet
|
||||
"""
|
||||
def extract_pubkeys(conf_section) do
|
||||
~r/PublicKey = (.*)/
|
||||
|> Regex.scan(conf_section)
|
||||
|> Enum.map(fn match_list -> List.last(match_list) end)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders WireGuard config in a deterministic way.
|
||||
"""
|
||||
def render(peers) do
|
||||
@begin_sentinel <> "\n" <> peers_to_config(peers) <> @end_sentinel
|
||||
def render(config) do
|
||||
@config_header <> interface_to_config(config) <> peers_to_config(config)
|
||||
end
|
||||
|
||||
defp peers_to_config(peers) do
|
||||
Enum.map_join(peers, fn pubkey ->
|
||||
defp interface_to_config(config) do
|
||||
~s"""
|
||||
[Interface]
|
||||
ListenPort = 51820
|
||||
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 peers_to_config(config) do
|
||||
Enum.map_join(config[:peers], fn pubkey ->
|
||||
~s"""
|
||||
# BEGIN PEER #{pubkey}
|
||||
[Peer]
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
defmodule FgVpn.WGCLI 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"
|
||||
"""
|
||||
end
|
||||
@@ -13,46 +13,101 @@ defmodule FgVpn.ConfigTest do
|
||||
# END PEER test-pubkey
|
||||
"""
|
||||
|
||||
@privkey """
|
||||
[Interface]
|
||||
ListenPort = 51820
|
||||
PrivateKey = test-privkey
|
||||
"""
|
||||
|
||||
@rendered_config """
|
||||
# This file is being managed by the fireguard systemd service. Any changes
|
||||
# will be overwritten eventually.
|
||||
|
||||
[Interface]
|
||||
ListenPort = 51820
|
||||
PrivateKey = rendered-privkey
|
||||
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
|
||||
|
||||
setup %{stubbed_config: config} do
|
||||
write_config(config)
|
||||
test_pid = start_supervised!(Config)
|
||||
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)
|
||||
on_exit(fn ->
|
||||
Application.get_env(:fg_vpn, :wireguard_conf_path)
|
||||
|> File.rm!()
|
||||
end)
|
||||
|
||||
%{test_pid: test_pid}
|
||||
%{test_pid: test_pid}
|
||||
end
|
||||
|
||||
@tag stubbed_config: @privkey
|
||||
test "parses PrivateKey from config file", %{test_pid: test_pid} do
|
||||
assert %{
|
||||
peers: [],
|
||||
default_int: _,
|
||||
privkey: "test-privkey"
|
||||
} = :sys.get_state(test_pid)
|
||||
end
|
||||
|
||||
@tag stubbed_config: @single_peer
|
||||
test "parses peers from config file", %{test_pid: test_pid} do
|
||||
assert %{
|
||||
peers: ["test-pubkey"],
|
||||
default_int: _,
|
||||
privkey: nil
|
||||
} = :sys.get_state(test_pid)
|
||||
end
|
||||
|
||||
@tag stubbed_config: @empty
|
||||
test "writes peers to config when device is verified", %{test_pid: test_pid} do
|
||||
send(test_pid, {:verify_device, "test-pubkey"})
|
||||
|
||||
# XXX: Avoid sleeping
|
||||
Process.sleep(100)
|
||||
|
||||
assert %{
|
||||
peers: ["test-pubkey"],
|
||||
default_int: _,
|
||||
privkey: nil
|
||||
} = :sys.get_state(test_pid)
|
||||
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 %{
|
||||
peers: [],
|
||||
default_int: _,
|
||||
privkey: nil
|
||||
} = :sys.get_state(test_pid)
|
||||
end
|
||||
end
|
||||
|
||||
@tag stubbed_config: @single_peer
|
||||
test "parses peers from config file", %{test_pid: test_pid} do
|
||||
state = :sys.get_state(test_pid)
|
||||
assert state == ["test-pubkey"]
|
||||
end
|
||||
|
||||
@tag stubbed_config: @empty
|
||||
test "writes peers to config when device is verified", %{test_pid: test_pid} do
|
||||
send(test_pid, {:verify_device, "test-pubkey"})
|
||||
|
||||
# XXX: Avoid sleeping
|
||||
Process.sleep(100)
|
||||
|
||||
assert Config.read() == ["test-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 Config.read() == []
|
||||
describe "loading / rendering" do
|
||||
test "renders config" do
|
||||
assert Config.render(%{
|
||||
default_int: "noop",
|
||||
privkey: "rendered-privkey",
|
||||
peers: ["test-pubkey"]
|
||||
}) == @rendered_config
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user