test rendering of config

This commit is contained in:
Jamil Bou Kheir
2020-12-04 09:48:04 -08:00
parent 117d9376c9
commit a8204b7988
4 changed files with 229 additions and 91 deletions

View 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

View File

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

View File

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

View File

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