mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
@@ -11,6 +11,8 @@ defmodule FgHttp.Devices.Device do
|
||||
schema "devices" do
|
||||
field :name, :string
|
||||
field :public_key, :string
|
||||
field :server_pubkey, :string, virtual: true
|
||||
field :private_key, :string, virtual: true
|
||||
field :ifname, :string
|
||||
field :last_ip, EctoNetwork.INET
|
||||
field :last_seen_at, :utc_datetime_usec
|
||||
@@ -25,5 +27,6 @@ defmodule FgHttp.Devices.Device do
|
||||
device
|
||||
|> cast(attrs, [:last_ip, :ifname, :user_id, :name, :public_key])
|
||||
|> validate_required([:user_id, :ifname, :name, :public_key])
|
||||
|> unique_constraint([:name])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,6 @@ defmodule FgHttpWeb.DeviceController do
|
||||
|
||||
use FgHttpWeb, :controller
|
||||
alias FgHttp.{Devices, Devices.Device}
|
||||
alias Phoenix.PubSub
|
||||
|
||||
plug FgHttpWeb.Plugs.SessionLoader
|
||||
|
||||
@@ -20,22 +19,17 @@ defmodule FgHttpWeb.DeviceController do
|
||||
end
|
||||
|
||||
def create(conn, %{"device" => %{"public_key" => _public_key} = device_params}) do
|
||||
# XXX: Get device name from browser
|
||||
our_params = %{
|
||||
"user_id" => conn.assigns.session.id,
|
||||
"name" => "Default",
|
||||
"ifname" => "wg0"
|
||||
"ifname" => "wg0",
|
||||
"name" => "Device #{DateTime.utc_now() |> DateTime.to_unix(:microsecond)}"
|
||||
}
|
||||
|
||||
all_params = Map.merge(device_params, our_params)
|
||||
|
||||
case Devices.create_device(all_params) do
|
||||
{:ok, device} ->
|
||||
PubSub.broadcast(
|
||||
:fg_http_pub_sub,
|
||||
"config",
|
||||
{:verify_device, device.public_key}
|
||||
)
|
||||
|
||||
{:ok, _device} ->
|
||||
redirect(conn, to: Routes.device_path(conn, :index))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
|
||||
@@ -11,32 +11,47 @@ defmodule FgHttpWeb.NewDeviceLive do
|
||||
|
||||
@doc """
|
||||
Called when the view mounts. The for a device being added goes like follows:
|
||||
1. Present QR code to user
|
||||
2. User's device connects, :device_connected is received from the FgVpn application
|
||||
3. User confirms device, :verify_device message is broadcasted
|
||||
4. FgVpn receives :verify_device and adds the device to the config file
|
||||
1. :add_device is broadcasted
|
||||
2. FgVpn picks this up and creates a new peer, adds to config, broadcasts :peer_added
|
||||
3. :peer_added is handled here which confirms the details to the user
|
||||
4. User confirms device, clicks create
|
||||
# XXX: Add ttl to device creation that removes stale devices
|
||||
"""
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id}, socket) do
|
||||
if connected?(socket) do
|
||||
# Subscribe to :device_connected events
|
||||
PubSub.subscribe(:fg_http_pub_sub, "view")
|
||||
# :timer.send_after(@mocked_timer, self(), {:pubkey, FgCrypto.rand_string()})
|
||||
end
|
||||
|
||||
# Fire off event to generate private key, psk, and add device to config
|
||||
PubSub.broadcast(:fg_http_pub_sub, "config", {:add_device, self()})
|
||||
|
||||
device = %Device{user_id: user_id}
|
||||
|
||||
{:ok, assign(socket, :device, device)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles device connect.
|
||||
Handles device added.
|
||||
"""
|
||||
@impl true
|
||||
def handle_info({:device_connected, pubkey}, socket) do
|
||||
device = %{socket.assigns.device | public_key: pubkey, last_ip: "127.0.0.1"}
|
||||
def handle_info({:peer_added, caller_pid, privkey, pubkey, server_pubkey}, socket) do
|
||||
if caller_pid == self() do
|
||||
device = %{
|
||||
socket.assigns.device
|
||||
| public_key: pubkey,
|
||||
private_key: privkey,
|
||||
server_pubkey: server_pubkey,
|
||||
last_ip: "127.0.0.1",
|
||||
name: "Device #{pubkey}"
|
||||
}
|
||||
|
||||
# Updates @device in the LiveView and causes re-render
|
||||
{:noreply, assign(socket, :device, device)}
|
||||
# Updates @device in the LiveView and causes re-render if the intended target is this pid
|
||||
{:noreply, assign(socket, :device, device)}
|
||||
else
|
||||
# Noop, let the correct pid handle this broadcast
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<h6 class="is-6 title">
|
||||
Add the following to your WireGuard™ configuration file:
|
||||
</h6>
|
||||
<%= unless @device.public_key do %>
|
||||
<h4 class="subtitle is-4">
|
||||
Adding new peer to WireGuard server...
|
||||
</h4>
|
||||
|
||||
<pre>
|
||||
<code id="wg-conf">
|
||||
[Peer]
|
||||
PublicKey = <%= Application.fetch_env!(:fg_vpn, :pubkey) %>
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = <%= Application.fetch_env!(:fg_http, :vpn_endpoint) %>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<progress class="progress is-small" max="100"></progress>
|
||||
|
||||
<p>
|
||||
<%= link "Back to Devices", to: Routes.device_path(@socket, :index) %>
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<h6 class="is-6 title">
|
||||
Or scan the QR code with your mobile phone:
|
||||
Add the following to your WireGuard™ configuration file:
|
||||
</h6>
|
||||
<canvas id="qr-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<pre>
|
||||
<code id="wg-conf">
|
||||
[Interface]
|
||||
PrivateKey = <%= @device.private_key %>
|
||||
|
||||
<div phx-hook="QrEncode" class="columns">
|
||||
<div class="column is-6">
|
||||
<%= unless @device.public_key do %>
|
||||
<h4 class="subtitle is-4">
|
||||
Waiting for device connection...
|
||||
</h4>
|
||||
|
||||
<progress class="progress is-small" max="100"></progress>
|
||||
|
||||
<h6 class="title is-6">
|
||||
When we receive a connection from your device, we'll prompt you verify it here.
|
||||
[Peer]
|
||||
PublicKey = <%= @device.server_pubkey %>
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = <%= Application.fetch_env!(:fg_http, :vpn_endpoint) %>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<h6 class="is-6 title">
|
||||
Or scan the QR code with your mobile phone:
|
||||
</h6>
|
||||
<canvas id="qr-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= link "Back to Devices", to: Routes.device_path(@socket, :index) %>
|
||||
</p>
|
||||
<% else %>
|
||||
<h4 class="subtitle is-4">Device connected!</h4>
|
||||
<hr>
|
||||
|
||||
<div phx-hook="QrEncode" class="columns">
|
||||
<div class="column is-6">
|
||||
<h4 class="subtitle is-4">Device added!</h4>
|
||||
|
||||
<dl class="content">
|
||||
<dt>Device Public Key:</dt>
|
||||
@@ -49,7 +48,6 @@ Endpoint = <%= Application.fetch_env!(:fg_http, :vpn_endpoint) %>
|
||||
<dd><strong><%= @device.last_ip %></strong></dd>
|
||||
</dl>
|
||||
|
||||
<%# XXX: Use the public key sent by the actual device and not the one here %>
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<%=
|
||||
@@ -65,6 +63,6 @@ Endpoint = <%= Application.fetch_env!(:fg_http, :vpn_endpoint) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -11,9 +11,11 @@ defmodule FgHttpWeb.NewDeviceLiveTest do
|
||||
|
||||
test "mount and handle_info/2", %{authed_conn: conn} do
|
||||
{:ok, view, html} = live_isolated(conn, FgHttpWeb.NewDeviceLive)
|
||||
assert html =~ "When we receive a connection from your device, we'll prompt"
|
||||
assert render(view) =~ "When we receive a connection from your device, we'll prompt"
|
||||
send(view.pid, {:device_connected, "test pubkey"})
|
||||
assert render(view) =~ "test pubkey"
|
||||
assert html =~ "Adding new peer to WireGuard server..."
|
||||
assert render(view) =~ "Adding new peer to WireGuard server..."
|
||||
send(view.pid, {:peer_added, view.pid, "test-privkey", "test-pubkey", "server-pubkey"})
|
||||
assert render(view) =~ "test-pubkey"
|
||||
assert render(view) =~ "server-pubkey"
|
||||
assert render(view) =~ "test-privkey"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,10 +26,19 @@ defmodule FgVpn.Config do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:verify_device, pubkey}, config) do
|
||||
def handle_info({:add_device, caller_pid}, config) do
|
||||
{_server_privkey, server_pubkey} = read_privkey()
|
||||
{privkey, pubkey} = CLI.genkey()
|
||||
new_peers = [pubkey | config[:peers]]
|
||||
new_config = %{config | peers: new_peers}
|
||||
write!(new_config)
|
||||
|
||||
PubSub.broadcast(
|
||||
:fg_http_pub_sub,
|
||||
"view",
|
||||
{:peer_added, caller_pid, privkey, pubkey, server_pubkey}
|
||||
)
|
||||
|
||||
{:noreply, new_config}
|
||||
end
|
||||
|
||||
@@ -51,6 +60,8 @@ defmodule FgVpn.Config do
|
||||
|
||||
@doc """
|
||||
Reads PrivateKey from configuration file
|
||||
|
||||
Returns a {privkey, pubkey} tuple
|
||||
"""
|
||||
def read_privkey do
|
||||
read_config_file()
|
||||
@@ -95,15 +106,16 @@ defmodule FgVpn.Config do
|
||||
@config_header <> interface_to_config(config) <> peers_to_config(config)
|
||||
end
|
||||
|
||||
defp interface_to_config(config) do
|
||||
listen_port =
|
||||
Application.get_env(:fg_http, :vpn_endpoint)
|
||||
|> String.split(":")
|
||||
|> List.last()
|
||||
defp listen_port do
|
||||
Application.get_env(:fg_http, :vpn_endpoint)
|
||||
|> String.split(":")
|
||||
|> List.last()
|
||||
end
|
||||
|
||||
defp interface_to_config(config) do
|
||||
~s"""
|
||||
[Interface]
|
||||
ListenPort = #{listen_port}
|
||||
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]
|
||||
@@ -140,8 +152,18 @@ defmodule FgVpn.Config do
|
||||
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} = read_privkey() || CLI.genkey()
|
||||
{privkey, _pubkey} = get_server_keys()
|
||||
|
||||
config = %{
|
||||
default_int: CLI.default_interface(),
|
||||
@@ -150,7 +172,6 @@ defmodule FgVpn.Config do
|
||||
}
|
||||
|
||||
write!(config)
|
||||
Application.put_env(:fg_vpn, :pubkey, pubkey)
|
||||
|
||||
config
|
||||
end
|
||||
|
||||
@@ -78,13 +78,13 @@ defmodule FgVpn.ConfigTest do
|
||||
|
||||
@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"})
|
||||
send(test_pid, {:add_device, test_pid})
|
||||
|
||||
# XXX: Avoid sleeping
|
||||
Process.sleep(100)
|
||||
|
||||
assert %{
|
||||
peers: ["test-pubkey"],
|
||||
peers: [_],
|
||||
default_int: _,
|
||||
privkey: _
|
||||
} = :sys.get_state(test_pid)
|
||||
|
||||
@@ -71,9 +71,6 @@ config :fg_http, FgHttpWeb.Endpoint,
|
||||
]
|
||||
]
|
||||
|
||||
config :fg_vpn,
|
||||
pubkey: "JId8GN8iPmdQXOLSdcsSkaW4i60e1/rpHB/03rsaKBk="
|
||||
|
||||
config :fg_vpn,
|
||||
wireguard_conf_path: Path.expand("~/.wg-fireguard.conf")
|
||||
|
||||
|
||||
@@ -59,9 +59,6 @@ wg_listen_port = System.get_env("WG_LISTEN_PORT" || "51820")
|
||||
wg_listen_address = System.get_env("WG_LISTEN_ADDRESS") || "localhost"
|
||||
url_host = System.get_env("URL_HOST") || "localhost"
|
||||
|
||||
# Will be replaced when FgVpn starts up and config is rewritten
|
||||
config :fg_vpn, pubkey: nil
|
||||
|
||||
config :fg_http, disable_signup: disable_signup
|
||||
|
||||
config :fg_http, FgHttp.Repo,
|
||||
|
||||
@@ -38,9 +38,5 @@ config :fg_http, FgHttpWeb.Endpoint,
|
||||
],
|
||||
server: false
|
||||
|
||||
config :fg_vpn,
|
||||
privkey: "cAM9MY5NrQ067ZgOkE3NX3h7cMSOBRjj/w4acCuMknk=",
|
||||
pubkey: "DcAqEvFtS0wuvrpvOYi0ncDTcZKpdFq7LKHQcMuAzSw="
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
Reference in New Issue
Block a user