mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
GeoIP routing and load-balancing for traffic (#2517)
This commit is contained in:
@@ -182,7 +182,13 @@ defmodule API.Client.Channel do
|
||||
{:ok, [_ | _] = gateways} <-
|
||||
Gateways.list_connected_gateways_for_resource(resource),
|
||||
{:ok, [_ | _] = relays} <- Relays.list_connected_relays_for_resource(resource) do
|
||||
gateway = Gateways.load_balance_gateways(gateways, connected_gateway_ids)
|
||||
location = {
|
||||
socket.assigns.client.last_seen_remote_ip_location_lat,
|
||||
socket.assigns.client.last_seen_remote_ip_location_lon
|
||||
}
|
||||
|
||||
relays = Relays.load_balance_relays(location, relays)
|
||||
gateway = Gateways.load_balance_gateways(location, gateways, connected_gateway_ids)
|
||||
|
||||
reply =
|
||||
{:ok,
|
||||
|
||||
@@ -23,7 +23,19 @@ defmodule API.Client.Socket do
|
||||
|
||||
real_ip = API.Sockets.real_ip(x_headers, peer_data)
|
||||
|
||||
with {:ok, subject} <- Auth.sign_in(token, user_agent, real_ip),
|
||||
{location_region, location_city, {location_lat, location_lon}} =
|
||||
API.Sockets.load_balancer_ip_location(x_headers)
|
||||
|
||||
context = %Auth.Context{
|
||||
user_agent: user_agent,
|
||||
remote_ip: real_ip,
|
||||
remote_ip_location_region: location_region,
|
||||
remote_ip_location_city: location_city,
|
||||
remote_ip_location_lat: location_lat,
|
||||
remote_ip_location_lon: location_lon
|
||||
}
|
||||
|
||||
with {:ok, subject} <- Auth.sign_in(token, context),
|
||||
{:ok, client} <- Clients.upsert_client(attrs, subject) do
|
||||
OpenTelemetry.Tracer.set_attributes(%{
|
||||
client_id: client.id,
|
||||
|
||||
@@ -23,10 +23,12 @@ defmodule API.Client.Views.Relay do
|
||||
} = Relays.generate_username_and_password(relay, expires_at)
|
||||
|
||||
[
|
||||
%{
|
||||
type: :stun,
|
||||
uri: "stun:#{format_address(address)}:#{relay.port}"
|
||||
},
|
||||
# WebRTC automatically falls back to STUN if TURN fails,
|
||||
# so no need to send it explicitly
|
||||
# %{
|
||||
# type: :stun,
|
||||
# uri: "stun:#{format_address(address)}:#{relay.port}"
|
||||
# },
|
||||
%{
|
||||
type: :turn,
|
||||
uri: "turn:#{format_address(address)}:#{relay.port}",
|
||||
|
||||
@@ -23,11 +23,18 @@ defmodule API.Gateway.Socket do
|
||||
|
||||
real_ip = API.Sockets.real_ip(x_headers, peer_data)
|
||||
|
||||
{location_region, location_city, {location_lat, location_lon}} =
|
||||
API.Sockets.load_balancer_ip_location(x_headers)
|
||||
|
||||
attrs =
|
||||
attrs
|
||||
|> Map.take(~w[external_id name_suffix public_key])
|
||||
|> Map.put("last_seen_user_agent", user_agent)
|
||||
|> Map.put("last_seen_remote_ip", real_ip)
|
||||
|> Map.put("last_seen_remote_ip_location_region", location_region)
|
||||
|> Map.put("last_seen_remote_ip_location_city", location_city)
|
||||
|> Map.put("last_seen_remote_ip_location_lat", location_lat)
|
||||
|> Map.put("last_seen_remote_ip_location_lon", location_lon)
|
||||
|
||||
with {:ok, token} <- Gateways.authorize_gateway(encrypted_secret),
|
||||
{:ok, gateway} <- Gateways.upsert_gateway(token, attrs) do
|
||||
|
||||
@@ -23,11 +23,18 @@ defmodule API.Relay.Socket do
|
||||
|
||||
real_ip = API.Sockets.real_ip(x_headers, peer_data)
|
||||
|
||||
{location_region, location_city, {location_lat, location_lon}} =
|
||||
API.Sockets.load_balancer_ip_location(x_headers)
|
||||
|
||||
attrs =
|
||||
attrs
|
||||
|> Map.take(~w[ipv4 ipv6])
|
||||
|> Map.put("last_seen_user_agent", user_agent)
|
||||
|> Map.put("last_seen_remote_ip", real_ip)
|
||||
|> Map.put("last_seen_remote_ip_location_region", location_region)
|
||||
|> Map.put("last_seen_remote_ip_location_city", location_city)
|
||||
|> Map.put("last_seen_remote_ip_location_lat", location_lat)
|
||||
|> Map.put("last_seen_remote_ip_location_lon", location_lon)
|
||||
|
||||
with {:ok, token} <- Relays.authorize_relay(encrypted_secret),
|
||||
{:ok, relay} <- Relays.upsert_relay(token, attrs) do
|
||||
|
||||
@@ -40,22 +40,35 @@ defmodule API.Sockets do
|
||||
real_ip || peer_data.address
|
||||
end
|
||||
|
||||
# if Mix.env() == :test do
|
||||
# defp maybe_allow_sandbox_access(%{user_agent: user_agent}) do
|
||||
# %{owner: owner_pid, repo: repos} =
|
||||
# metadata = Phoenix.Ecto.SQL.Sandbox.decode_metadata(user_agent)
|
||||
def load_balancer_ip_location(x_headers) do
|
||||
location_region =
|
||||
case API.Sockets.get_header(x_headers, "x-geo-location-region") do
|
||||
{"x-geo-location-region", location_region} -> location_region
|
||||
_other -> nil
|
||||
end
|
||||
|
||||
# repos
|
||||
# |> List.wrap()
|
||||
# |> Enum.each(fn repo ->
|
||||
# Ecto.Adapters.SQL.Sandbox.allow(repo, owner_pid, self())
|
||||
# end)
|
||||
location_city =
|
||||
case API.Sockets.get_header(x_headers, "x-geo-location-city") do
|
||||
{"x-geo-location-city", location_city} -> location_city
|
||||
_other -> nil
|
||||
end
|
||||
|
||||
# {:ok, metadata}
|
||||
# end
|
||||
{location_lat, location_lon} =
|
||||
case API.Sockets.get_header(x_headers, "x-geo-location-coordinates") do
|
||||
{"x-geo-location-coordinates", coordinates} ->
|
||||
[lat, lon] = String.split(coordinates, ",", parts: 2)
|
||||
lat = String.to_float(lat)
|
||||
lon = String.to_float(lon)
|
||||
{lat, lon}
|
||||
|
||||
# defp maybe_allow_sandbox_access(_), do: {:ok, %{}}
|
||||
# else
|
||||
# defp maybe_allow_sandbox_access(_), do: {:ok, %{}}
|
||||
# end
|
||||
_other ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
{location_region, location_city, {location_lat, location_lon}}
|
||||
end
|
||||
|
||||
def get_header(x_headers, key) do
|
||||
List.keyfind(x_headers, key, 0)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -258,7 +258,15 @@ defmodule API.Client.ChannelTest do
|
||||
} do
|
||||
# Online Relay
|
||||
global_relay_group = Fixtures.Relays.create_global_group()
|
||||
global_relay = Fixtures.Relays.create_relay(group: global_relay_group, ipv6: nil)
|
||||
|
||||
global_relay =
|
||||
Fixtures.Relays.create_relay(
|
||||
group: global_relay_group,
|
||||
ipv6: nil,
|
||||
last_seen_remote_ip_location_lat: 37,
|
||||
last_seen_remote_ip_location_lon: -120
|
||||
)
|
||||
|
||||
relay = Fixtures.Relays.create_relay(account: account)
|
||||
stamp_secret = Ecto.UUID.generate()
|
||||
:ok = Domain.Relays.connect_relay(relay, stamp_secret)
|
||||
@@ -279,16 +287,10 @@ defmodule API.Client.ChannelTest do
|
||||
assert gateway_id == gateway.id
|
||||
assert gateway_last_seen_remote_ip == gateway.last_seen_remote_ip
|
||||
|
||||
ipv4_stun_uri = "stun:#{relay.ipv4}:#{relay.port}"
|
||||
ipv4_turn_uri = "turn:#{relay.ipv4}:#{relay.port}"
|
||||
ipv6_stun_uri = "stun:[#{relay.ipv6}]:#{relay.port}"
|
||||
ipv6_turn_uri = "turn:[#{relay.ipv6}]:#{relay.port}"
|
||||
|
||||
assert [
|
||||
%{
|
||||
type: :stun,
|
||||
uri: ^ipv4_stun_uri
|
||||
},
|
||||
%{
|
||||
type: :turn,
|
||||
expires_at: expires_at_unix,
|
||||
@@ -296,10 +298,6 @@ defmodule API.Client.ChannelTest do
|
||||
username: username1,
|
||||
uri: ^ipv4_turn_uri
|
||||
},
|
||||
%{
|
||||
type: :stun,
|
||||
uri: ^ipv6_stun_uri
|
||||
},
|
||||
%{
|
||||
type: :turn,
|
||||
expires_at: expires_at_unix,
|
||||
@@ -320,9 +318,10 @@ defmodule API.Client.ChannelTest do
|
||||
assert is_binary(salt)
|
||||
|
||||
:ok = Domain.Relays.connect_relay(global_relay, stamp_secret)
|
||||
|
||||
ref = push(socket, "prepare_connection", %{"resource_id" => resource.id})
|
||||
assert_reply ref, :ok, %{relays: relays}
|
||||
assert length(relays) == 6
|
||||
assert length(relays) == 3
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,10 +4,19 @@ defmodule API.Client.SocketTest do
|
||||
alias API.Client.Socket
|
||||
alias Domain.Auth
|
||||
|
||||
@geo_headers [
|
||||
{"x-geo-location-region", "Ukraine"},
|
||||
{"x-geo-location-city", "Kyiv"},
|
||||
{"x-geo-location-coordinates", "50.4333,30.5167"}
|
||||
]
|
||||
|
||||
@connect_info %{
|
||||
user_agent: "iOS/12.7 (iPhone) connlib/0.1.1",
|
||||
peer_data: %{address: {189, 172, 73, 001}},
|
||||
x_headers: [{"x-forwarded-for", "189.172.73.153"}],
|
||||
x_headers:
|
||||
[
|
||||
{"x-forwarded-for", "189.172.73.153"}
|
||||
] ++ @geo_headers,
|
||||
trace_context_headers: []
|
||||
}
|
||||
|
||||
@@ -34,6 +43,10 @@ defmodule API.Client.SocketTest do
|
||||
assert client.public_key == attrs["public_key"]
|
||||
assert client.last_seen_user_agent == subject.context.user_agent
|
||||
assert client.last_seen_remote_ip.address == subject.context.remote_ip
|
||||
assert client.last_seen_remote_ip_location_region == "Ukraine"
|
||||
assert client.last_seen_remote_ip_location_city == "Kyiv"
|
||||
assert client.last_seen_remote_ip_location_lat == 50.4333
|
||||
assert client.last_seen_remote_ip_location_lon == 30.5167
|
||||
assert client.last_seen_version == "0.7.412"
|
||||
end
|
||||
|
||||
@@ -82,7 +95,7 @@ defmodule API.Client.SocketTest do
|
||||
%{
|
||||
user_agent: subject.context.user_agent,
|
||||
peer_data: %{address: subject.context.remote_ip},
|
||||
x_headers: [],
|
||||
x_headers: @geo_headers,
|
||||
trace_context_headers: []
|
||||
}
|
||||
end
|
||||
|
||||
@@ -171,16 +171,10 @@ defmodule API.Gateway.ChannelTest do
|
||||
assert payload.flow_id == flow_id
|
||||
assert payload.actor == %{id: client.actor_id}
|
||||
|
||||
ipv4_stun_uri = "stun:#{relay.ipv4}:#{relay.port}"
|
||||
ipv4_turn_uri = "turn:#{relay.ipv4}:#{relay.port}"
|
||||
ipv6_stun_uri = "stun:[#{relay.ipv6}]:#{relay.port}"
|
||||
ipv6_turn_uri = "turn:[#{relay.ipv6}]:#{relay.port}"
|
||||
|
||||
assert [
|
||||
%{
|
||||
type: :stun,
|
||||
uri: ^ipv4_stun_uri
|
||||
},
|
||||
%{
|
||||
type: :turn,
|
||||
expires_at: expires_at_unix,
|
||||
@@ -188,10 +182,6 @@ defmodule API.Gateway.ChannelTest do
|
||||
username: username1,
|
||||
uri: ^ipv4_turn_uri
|
||||
},
|
||||
%{
|
||||
type: :stun,
|
||||
uri: ^ipv6_stun_uri
|
||||
},
|
||||
%{
|
||||
type: :turn,
|
||||
expires_at: expires_at_unix,
|
||||
|
||||
@@ -9,7 +9,12 @@ defmodule API.Gateway.SocketTest do
|
||||
@connect_info %{
|
||||
user_agent: "iOS/12.7 (iPhone) connlib/#{@connlib_version}",
|
||||
peer_data: %{address: {189, 172, 73, 001}},
|
||||
x_headers: [{"x-forwarded-for", "189.172.73.153"}],
|
||||
x_headers: [
|
||||
{"x-forwarded-for", "189.172.73.153"},
|
||||
{"x-geo-location-region", "Ukraine"},
|
||||
{"x-geo-location-city", "Kyiv"},
|
||||
{"x-geo-location-coordinates", "50.4333,30.5167"}
|
||||
],
|
||||
trace_context_headers: []
|
||||
}
|
||||
|
||||
@@ -31,6 +36,10 @@ defmodule API.Gateway.SocketTest do
|
||||
assert gateway.public_key == attrs["public_key"]
|
||||
assert gateway.last_seen_user_agent == @connect_info.user_agent
|
||||
assert gateway.last_seen_remote_ip.address == {189, 172, 73, 153}
|
||||
assert gateway.last_seen_remote_ip_location_region == "Ukraine"
|
||||
assert gateway.last_seen_remote_ip_location_city == "Kyiv"
|
||||
assert gateway.last_seen_remote_ip_location_lat == 50.4333
|
||||
assert gateway.last_seen_remote_ip_location_lon == 30.5167
|
||||
assert gateway.last_seen_version == @connlib_version
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,12 @@ defmodule API.Relay.SocketTest do
|
||||
@connect_info %{
|
||||
user_agent: "iOS/12.7 (iPhone) connlib/#{@connlib_version}",
|
||||
peer_data: %{address: {189, 172, 73, 001}},
|
||||
x_headers: [{"x-forwarded-for", "189.172.73.153"}],
|
||||
x_headers: [
|
||||
{"x-forwarded-for", "189.172.73.153"},
|
||||
{"x-geo-location-region", "Ukraine"},
|
||||
{"x-geo-location-city", "Kyiv"},
|
||||
{"x-geo-location-coordinates", "50.4333,30.5167"}
|
||||
],
|
||||
trace_context_headers: []
|
||||
}
|
||||
|
||||
@@ -31,6 +36,10 @@ defmodule API.Relay.SocketTest do
|
||||
assert relay.ipv6.address == attrs["ipv6"]
|
||||
assert relay.last_seen_user_agent == @connect_info.user_agent
|
||||
assert relay.last_seen_remote_ip.address == {189, 172, 73, 153}
|
||||
assert relay.last_seen_remote_ip_location_region == "Ukraine"
|
||||
assert relay.last_seen_remote_ip_location_city == "Kyiv"
|
||||
assert relay.last_seen_remote_ip_location_lat == 50.4333
|
||||
assert relay.last_seen_remote_ip_location_lon == 30.5167
|
||||
assert relay.last_seen_version == @connlib_version
|
||||
end
|
||||
|
||||
|
||||
@@ -419,7 +419,8 @@ defmodule Domain.Auth do
|
||||
|
||||
with {:ok, identity} <- Repo.fetch(identity_queryable),
|
||||
{:ok, identity, expires_at} <- Adapters.verify_secret(provider, identity, secret) do
|
||||
{:ok, build_subject(identity, expires_at, user_agent, remote_ip)}
|
||||
context = %Context{remote_ip: remote_ip, user_agent: user_agent}
|
||||
{:ok, build_subject(identity, expires_at, context)}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :unauthorized}
|
||||
{:error, :invalid_secret} -> {:error, :unauthorized}
|
||||
@@ -429,7 +430,8 @@ defmodule Domain.Auth do
|
||||
|
||||
def sign_in(%Provider{} = provider, payload, user_agent, remote_ip) do
|
||||
with {:ok, identity, expires_at} <- Adapters.verify_and_update_identity(provider, payload) do
|
||||
{:ok, build_subject(identity, expires_at, user_agent, remote_ip)}
|
||||
context = %Context{remote_ip: remote_ip, user_agent: user_agent}
|
||||
{:ok, build_subject(identity, expires_at, context)}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :unauthorized}
|
||||
{:error, :invalid} -> {:error, :unauthorized}
|
||||
@@ -437,9 +439,9 @@ defmodule Domain.Auth do
|
||||
end
|
||||
end
|
||||
|
||||
def sign_in(token, user_agent, remote_ip) when is_binary(token) do
|
||||
with {:ok, identity, expires_at} <- verify_token(token, user_agent, remote_ip) do
|
||||
{:ok, build_subject(identity, expires_at, user_agent, remote_ip)}
|
||||
def sign_in(token, %Context{} = context) when is_binary(token) do
|
||||
with {:ok, identity, expires_at} <- verify_token(token, context.user_agent, context.remote_ip) do
|
||||
{:ok, build_subject(identity, expires_at, context)}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :unauthorized}
|
||||
{:error, :invalid_token} -> {:error, :unauthorized}
|
||||
@@ -468,11 +470,10 @@ defmodule Domain.Auth do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def build_subject(%Identity{} = identity, expires_at, user_agent, remote_ip)
|
||||
when is_binary(user_agent) and is_tuple(remote_ip) do
|
||||
def build_subject(%Identity{} = identity, expires_at, context) do
|
||||
identity =
|
||||
identity
|
||||
|> Identity.Changeset.sign_in_identity(user_agent, remote_ip)
|
||||
|> Identity.Changeset.sign_in_identity(context)
|
||||
|> Repo.update!()
|
||||
|
||||
identity_with_preloads = Repo.preload(identity, [:account, :actor])
|
||||
@@ -484,7 +485,7 @@ defmodule Domain.Auth do
|
||||
permissions: permissions,
|
||||
account: identity_with_preloads.account,
|
||||
expires_at: build_subject_expires_at(identity_with_preloads.actor, expires_at),
|
||||
context: %Context{remote_ip: remote_ip, user_agent: user_agent}
|
||||
context: context
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -7,9 +7,17 @@ defmodule Domain.Auth.Context do
|
||||
"""
|
||||
@type t :: %__MODULE__{
|
||||
remote_ip: :inet.ip_address(),
|
||||
remote_ip_location_region: String.t(),
|
||||
remote_ip_location_city: String.t(),
|
||||
remote_ip_location_lat: float(),
|
||||
remote_ip_location_lon: float(),
|
||||
user_agent: String.t()
|
||||
}
|
||||
|
||||
defstruct remote_ip: nil,
|
||||
remote_ip_location_region: nil,
|
||||
remote_ip_location_city: nil,
|
||||
remote_ip_location_lat: nil,
|
||||
remote_ip_location_lon: nil,
|
||||
user_agent: nil
|
||||
end
|
||||
|
||||
@@ -11,6 +11,10 @@ defmodule Domain.Auth.Identity do
|
||||
|
||||
field :last_seen_user_agent, :string
|
||||
field :last_seen_remote_ip, Domain.Types.IP
|
||||
field :last_seen_remote_ip_location_region, :string
|
||||
field :last_seen_remote_ip_location_city, :string
|
||||
field :last_seen_remote_ip_location_lat, :float
|
||||
field :last_seen_remote_ip_location_lon, :float
|
||||
field :last_seen_at, :utc_datetime_usec
|
||||
|
||||
belongs_to :account, Domain.Accounts.Account
|
||||
|
||||
@@ -74,12 +74,17 @@ defmodule Domain.Auth.Identity.Changeset do
|
||||
|> put_change(:provider_virtual_state, virtual_state)
|
||||
end
|
||||
|
||||
def sign_in_identity(identity_or_changeset, user_agent, remote_ip) do
|
||||
def sign_in_identity(identity_or_changeset, context) do
|
||||
identity_or_changeset
|
||||
|> change()
|
||||
|> put_change(:last_seen_user_agent, user_agent)
|
||||
|> put_change(:last_seen_remote_ip, %Postgrex.INET{address: remote_ip})
|
||||
|> put_change(:last_seen_user_agent, context.user_agent)
|
||||
|> put_change(:last_seen_remote_ip, %Postgrex.INET{address: context.remote_ip})
|
||||
|> put_change(:last_seen_remote_ip_location_region, context.remote_ip_location_region)
|
||||
|> put_change(:last_seen_remote_ip_location_city, context.remote_ip_location_city)
|
||||
|> put_change(:last_seen_remote_ip_location_lat, context.remote_ip_location_lat)
|
||||
|> put_change(:last_seen_remote_ip_location_lon, context.remote_ip_location_lon)
|
||||
|> put_change(:last_seen_at, DateTime.utc_now())
|
||||
|> validate_required(~w[last_seen_user_agent last_seen_remote_ip]a)
|
||||
end
|
||||
|
||||
def delete_identity(%Identity{} = identity) do
|
||||
|
||||
@@ -13,6 +13,10 @@ defmodule Domain.Clients.Client do
|
||||
|
||||
field :last_seen_user_agent, :string
|
||||
field :last_seen_remote_ip, Domain.Types.IP
|
||||
field :last_seen_remote_ip_location_region, :string
|
||||
field :last_seen_remote_ip_location_city, :string
|
||||
field :last_seen_remote_ip_location_lat, :float
|
||||
field :last_seen_remote_ip_location_lon, :float
|
||||
field :last_seen_version, :string
|
||||
field :last_seen_at, :utc_datetime_usec
|
||||
|
||||
|
||||
@@ -5,8 +5,14 @@ defmodule Domain.Clients.Client.Changeset do
|
||||
|
||||
@upsert_fields ~w[external_id name public_key]a
|
||||
@conflict_replace_fields ~w[public_key
|
||||
last_seen_user_agent last_seen_remote_ip
|
||||
last_seen_version last_seen_at
|
||||
last_seen_user_agent
|
||||
last_seen_remote_ip
|
||||
last_seen_remote_ip_location_region
|
||||
last_seen_remote_ip_location_city
|
||||
last_seen_remote_ip_location_lat
|
||||
last_seen_remote_ip_location_lon
|
||||
last_seen_version
|
||||
last_seen_at
|
||||
updated_at]a
|
||||
@update_fields ~w[name]a
|
||||
@required_fields @upsert_fields
|
||||
@@ -28,6 +34,10 @@ defmodule Domain.Clients.Client.Changeset do
|
||||
|> put_change(:account_id, identity.account_id)
|
||||
|> put_change(:last_seen_user_agent, context.user_agent)
|
||||
|> put_change(:last_seen_remote_ip, %Postgrex.INET{address: context.remote_ip})
|
||||
|> put_change(:last_seen_remote_ip_location_region, context.remote_ip_location_region)
|
||||
|> put_change(:last_seen_remote_ip_location_city, context.remote_ip_location_city)
|
||||
|> put_change(:last_seen_remote_ip_location_lat, context.remote_ip_location_lat)
|
||||
|> put_change(:last_seen_remote_ip_location_lon, context.remote_ip_location_lon)
|
||||
|> changeset()
|
||||
|> validate_required(@required_fields)
|
||||
|> validate_base64(:public_key)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Domain.Gateways do
|
||||
use Supervisor
|
||||
alias Domain.{Repo, Auth, Validator}
|
||||
alias Domain.{Repo, Auth, Validator, Geo}
|
||||
alias Domain.{Accounts, Resources}
|
||||
alias Domain.Gateways.{Authorizer, Gateway, Group, Token, Presence}
|
||||
|
||||
@@ -335,16 +335,37 @@ defmodule Domain.Gateways do
|
||||
end
|
||||
end
|
||||
|
||||
def load_balance_gateways(gateways) do
|
||||
def load_balance_gateways({_lat, _lon}, []) do
|
||||
nil
|
||||
end
|
||||
|
||||
def load_balance_gateways({lat, lon}, gateways) when is_nil(lat) or is_nil(lon) do
|
||||
Enum.random(gateways)
|
||||
end
|
||||
|
||||
def load_balance_gateways(gateways, preferred_gateway_ids) do
|
||||
def load_balance_gateways({lat, lon}, gateways) do
|
||||
gateways
|
||||
# This allows to group gateways that are running at the same location so
|
||||
# we are using at least 2 locations to build ICE candidates
|
||||
|> Enum.group_by(fn gateway ->
|
||||
{gateway.last_seen_remote_ip_location_lat, gateway.last_seen_remote_ip_location_lon}
|
||||
end)
|
||||
|> Enum.map(fn {{gateway_lat, gateway_lon}, gateway} ->
|
||||
distance = Geo.distance({lat, lon}, {gateway_lat, gateway_lon})
|
||||
{distance, gateway}
|
||||
end)
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
|> Enum.at(0)
|
||||
|> elem(1)
|
||||
|> Enum.random()
|
||||
end
|
||||
|
||||
def load_balance_gateways({lat, lon}, gateways, preferred_gateway_ids) do
|
||||
gateways
|
||||
|> Enum.filter(&(&1.id in preferred_gateway_ids))
|
||||
|> case do
|
||||
[] -> load_balance_gateways(gateways)
|
||||
preferred_gateways -> load_balance_gateways(preferred_gateways)
|
||||
[] -> load_balance_gateways({lat, lon}, gateways)
|
||||
preferred_gateways -> load_balance_gateways({lat, lon}, preferred_gateways)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ defmodule Domain.Gateways.Gateway do
|
||||
|
||||
field :last_seen_user_agent, :string
|
||||
field :last_seen_remote_ip, Domain.Types.IP
|
||||
field :last_seen_remote_ip_location_region, :string
|
||||
field :last_seen_remote_ip_location_city, :string
|
||||
field :last_seen_remote_ip_location_lat, :float
|
||||
field :last_seen_remote_ip_location_lon, :float
|
||||
field :last_seen_version, :string
|
||||
field :last_seen_at, :utc_datetime_usec
|
||||
|
||||
|
||||
@@ -4,13 +4,25 @@ defmodule Domain.Gateways.Gateway.Changeset do
|
||||
alias Domain.Gateways
|
||||
|
||||
@upsert_fields ~w[external_id name_suffix public_key
|
||||
last_seen_user_agent last_seen_remote_ip]a
|
||||
last_seen_user_agent
|
||||
last_seen_remote_ip
|
||||
last_seen_remote_ip_location_region
|
||||
last_seen_remote_ip_location_city
|
||||
last_seen_remote_ip_location_lat
|
||||
last_seen_remote_ip_location_lon]a
|
||||
@conflict_replace_fields ~w[public_key
|
||||
last_seen_user_agent last_seen_remote_ip
|
||||
last_seen_version last_seen_at
|
||||
last_seen_user_agent
|
||||
last_seen_remote_ip
|
||||
last_seen_remote_ip_location_region
|
||||
last_seen_remote_ip_location_city
|
||||
last_seen_remote_ip_location_lat
|
||||
last_seen_remote_ip_location_lon
|
||||
last_seen_version
|
||||
last_seen_at
|
||||
updated_at]a
|
||||
@update_fields ~w[name_suffix]a
|
||||
@required_fields @upsert_fields
|
||||
@required_fields ~w[external_id name_suffix public_key
|
||||
last_seen_user_agent last_seen_remote_ip]a
|
||||
|
||||
# WireGuard base64-encoded string length
|
||||
@key_length 44
|
||||
|
||||
21
elixir/apps/domain/lib/domain/geo.ex
Normal file
21
elixir/apps/domain/lib/domain/geo.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule Domain.Geo do
|
||||
@radius_of_earth_km 6371.0
|
||||
|
||||
def distance({lat1, lon1}, {lat2, lon2}) do
|
||||
d_lat = degrees_to_radians(lat2 - lat1)
|
||||
d_lon = degrees_to_radians(lon2 - lon1)
|
||||
|
||||
a =
|
||||
:math.sin(d_lat / 2) * :math.sin(d_lat / 2) +
|
||||
:math.cos(degrees_to_radians(lat1)) * :math.cos(degrees_to_radians(lat2)) *
|
||||
:math.sin(d_lon / 2) * :math.sin(d_lon / 2)
|
||||
|
||||
c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a))
|
||||
|
||||
@radius_of_earth_km * c
|
||||
end
|
||||
|
||||
defp degrees_to_radians(deg) do
|
||||
deg * :math.pi() / 180
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Domain.Relays do
|
||||
use Supervisor
|
||||
alias Domain.{Repo, Auth, Validator}
|
||||
alias Domain.{Repo, Auth, Validator, Geo}
|
||||
alias Domain.{Accounts, Resources}
|
||||
alias Domain.Relays.{Authorizer, Relay, Group, Token, Presence}
|
||||
|
||||
@@ -313,6 +313,31 @@ defmodule Domain.Relays do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Selects 3 nearest relays to the given location and then picks one of them randomly.
|
||||
"""
|
||||
def load_balance_relays({lat, lon}, relays) when is_nil(lat) or is_nil(lon) do
|
||||
relays
|
||||
|> Enum.shuffle()
|
||||
|> Enum.take(2)
|
||||
end
|
||||
|
||||
def load_balance_relays({lat, lon}, relays) do
|
||||
relays
|
||||
# This allows to group relays that are running at the same location so
|
||||
# we are using at least 2 locations to build ICE candidates
|
||||
|> Enum.group_by(fn relay ->
|
||||
{relay.last_seen_remote_ip_location_lat, relay.last_seen_remote_ip_location_lon}
|
||||
end)
|
||||
|> Enum.map(fn {{relay_lat, relay_lon}, relay} ->
|
||||
distance = Geo.distance({lat, lon}, {relay_lat, relay_lon})
|
||||
{distance, relay}
|
||||
end)
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
|> Enum.take(2)
|
||||
|> Enum.map(&Enum.random(elem(&1, 1)))
|
||||
end
|
||||
|
||||
def encode_token!(%Token{value: value} = token) when not is_nil(value) do
|
||||
body = {token.id, token.value}
|
||||
config = fetch_config!()
|
||||
|
||||
@@ -9,6 +9,10 @@ defmodule Domain.Relays.Relay do
|
||||
|
||||
field :last_seen_user_agent, :string
|
||||
field :last_seen_remote_ip, Domain.Types.IP
|
||||
field :last_seen_remote_ip_location_region, :string
|
||||
field :last_seen_remote_ip_location_city, :string
|
||||
field :last_seen_remote_ip_location_lat, :float
|
||||
field :last_seen_remote_ip_location_lon, :float
|
||||
field :last_seen_version, :string
|
||||
field :last_seen_at, :utc_datetime_usec
|
||||
|
||||
|
||||
@@ -4,10 +4,21 @@ defmodule Domain.Relays.Relay.Changeset do
|
||||
alias Domain.Relays
|
||||
|
||||
@upsert_fields ~w[ipv4 ipv6 port
|
||||
last_seen_user_agent last_seen_remote_ip]a
|
||||
last_seen_user_agent
|
||||
last_seen_remote_ip
|
||||
last_seen_remote_ip_location_region
|
||||
last_seen_remote_ip_location_city
|
||||
last_seen_remote_ip_location_lat
|
||||
last_seen_remote_ip_location_lon]a
|
||||
@conflict_replace_fields ~w[ipv4 ipv6 port
|
||||
last_seen_user_agent last_seen_remote_ip
|
||||
last_seen_version last_seen_at
|
||||
last_seen_user_agent
|
||||
last_seen_remote_ip
|
||||
last_seen_remote_ip_location_region
|
||||
last_seen_remote_ip_location_city
|
||||
last_seen_remote_ip_location_lat
|
||||
last_seen_remote_ip_location_lon
|
||||
last_seen_version
|
||||
last_seen_at
|
||||
updated_at]a
|
||||
|
||||
def upsert_conflict_target(%{account_id: nil}) do
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
defmodule Domain.Repo.Migrations.AddGeoipLocationFields do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:clients) do
|
||||
add(:last_seen_remote_ip_location_region, :text)
|
||||
add(:last_seen_remote_ip_location_city, :text)
|
||||
add(:last_seen_remote_ip_location_lat, :float)
|
||||
add(:last_seen_remote_ip_location_lon, :float)
|
||||
end
|
||||
|
||||
alter table(:relays) do
|
||||
add(:last_seen_remote_ip_location_region, :text)
|
||||
add(:last_seen_remote_ip_location_city, :text)
|
||||
add(:last_seen_remote_ip_location_lat, :float)
|
||||
add(:last_seen_remote_ip_location_lon, :float)
|
||||
end
|
||||
|
||||
alter table(:gateways) do
|
||||
add(:last_seen_remote_ip_location_region, :text)
|
||||
add(:last_seen_remote_ip_location_city, :text)
|
||||
add(:last_seen_remote_ip_location_lat, :float)
|
||||
add(:last_seen_remote_ip_location_lon, :float)
|
||||
end
|
||||
|
||||
alter table(:auth_identities) do
|
||||
add(:last_seen_remote_ip_location_region, :text)
|
||||
add(:last_seen_remote_ip_location_city, :text)
|
||||
add(:last_seen_remote_ip_location_lat, :float)
|
||||
add(:last_seen_remote_ip_location_lon, :float)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -190,16 +190,14 @@ unprivileged_subject =
|
||||
Auth.build_subject(
|
||||
unprivileged_actor_userpass_identity,
|
||||
DateTime.utc_now() |> DateTime.add(365, :day),
|
||||
"Debian/11.0.0 connlib/0.1.0",
|
||||
{172, 28, 0, 100}
|
||||
%Auth.Context{user_agent: "Debian/11.0.0 connlib/0.1.0", remote_ip: {172, 28, 0, 100}}
|
||||
)
|
||||
|
||||
admin_subject =
|
||||
Auth.build_subject(
|
||||
admin_actor_email_identity,
|
||||
nil,
|
||||
"iOS/12.5 (iPhone) connlib/0.7.412",
|
||||
{100, 64, 100, 58}
|
||||
%Auth.Context{user_agent: "iOS/12.5 (iPhone) connlib/0.7.412", remote_ip: {100, 64, 100, 58}}
|
||||
)
|
||||
|
||||
IO.puts("Created users: ")
|
||||
|
||||
@@ -2326,7 +2326,7 @@ defmodule Domain.AuthTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "sign_in/3" do
|
||||
describe "sign_in/2" do
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
Domain.Config.put_system_env_override(:outbound_email_adapter, Swoosh.Adapters.Postmark)
|
||||
@@ -2350,45 +2350,51 @@ defmodule Domain.AuthTest do
|
||||
identity: identity,
|
||||
subject: subject,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
remote_ip: remote_ip,
|
||||
context: %Auth.Context{
|
||||
remote_ip: remote_ip,
|
||||
remote_ip_location_region: "UA",
|
||||
remote_ip_location_city: "Kyiv",
|
||||
remote_ip_location_lat: 50.4501,
|
||||
remote_ip_location_lon: 30.5234,
|
||||
user_agent: user_agent
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error when token is invalid", %{user_agent: user_agent, remote_ip: remote_ip} do
|
||||
assert sign_in(Ecto.UUID.generate(), user_agent, remote_ip) ==
|
||||
test "returns error when token is invalid", %{context: context} do
|
||||
assert sign_in(Ecto.UUID.generate(), context) ==
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
test "returns subject on success for session token", %{
|
||||
subject: subject,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
context: context
|
||||
} do
|
||||
{:ok, token} = create_session_token_from_subject(subject)
|
||||
|
||||
assert {:ok, reconstructed_subject} = sign_in(token, user_agent, remote_ip)
|
||||
assert {:ok, reconstructed_subject} = sign_in(token, context)
|
||||
assert reconstructed_subject.identity.id == subject.identity.id
|
||||
assert reconstructed_subject.actor.id == subject.actor.id
|
||||
assert reconstructed_subject.account.id == subject.account.id
|
||||
assert reconstructed_subject.permissions == subject.permissions
|
||||
assert reconstructed_subject.context == subject.context
|
||||
assert reconstructed_subject.context.remote_ip == subject.context.remote_ip
|
||||
assert reconstructed_subject.context.user_agent == subject.context.user_agent
|
||||
assert DateTime.diff(reconstructed_subject.expires_at, subject.expires_at) <= 1
|
||||
end
|
||||
|
||||
test "returns subject on success for client token", %{
|
||||
subject: subject,
|
||||
user_agent: user_agent
|
||||
context: context
|
||||
} do
|
||||
{:ok, token} = create_client_token_from_subject(subject)
|
||||
|
||||
<<a::size(8), b::size(8), c::size(8), d::size(8)>> =
|
||||
<<System.unique_integer([:positive])::32>>
|
||||
|
||||
# Client sessions are not binded to a specific user agent or remote ip
|
||||
remote_ip = {a, b, c, d}
|
||||
user_agent = user_agent <> "+b1"
|
||||
remote_ip = Domain.Fixture.unique_ipv4()
|
||||
user_agent = context.user_agent <> "+b1"
|
||||
context = %{context | remote_ip: remote_ip, user_agent: user_agent}
|
||||
|
||||
assert {:ok, reconstructed_subject} = sign_in(token, user_agent, remote_ip)
|
||||
assert {:ok, reconstructed_subject} = sign_in(token, context)
|
||||
|
||||
assert reconstructed_subject.identity.id == subject.identity.id
|
||||
assert reconstructed_subject.actor.id == subject.actor.id
|
||||
@@ -2402,8 +2408,7 @@ defmodule Domain.AuthTest do
|
||||
|
||||
test "returns subject on success for service account token", %{
|
||||
account: account,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip,
|
||||
context: context,
|
||||
subject: subject
|
||||
} do
|
||||
one_day = DateTime.utc_now() |> DateTime.add(1, :day)
|
||||
@@ -2413,8 +2418,8 @@ defmodule Domain.AuthTest do
|
||||
Fixtures.Auth.create_identity(
|
||||
account: account,
|
||||
provider: provider,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip,
|
||||
user_agent: context.user_agent,
|
||||
remote_ip: context.remote_ip,
|
||||
provider_virtual_state: %{
|
||||
"expires_at" => one_day
|
||||
}
|
||||
@@ -2422,24 +2427,24 @@ defmodule Domain.AuthTest do
|
||||
|
||||
{:ok, token} = create_access_token_for_identity(identity)
|
||||
|
||||
assert {:ok, reconstructed_subject} = sign_in(token, user_agent, remote_ip)
|
||||
assert {:ok, reconstructed_subject} = sign_in(token, context)
|
||||
assert reconstructed_subject.identity.id == identity.id
|
||||
assert reconstructed_subject.actor.id == identity.actor_id
|
||||
assert reconstructed_subject.account.id == identity.account_id
|
||||
assert reconstructed_subject.permissions == subject.permissions
|
||||
assert reconstructed_subject.context == subject.context
|
||||
assert reconstructed_subject.context.remote_ip == subject.context.remote_ip
|
||||
assert reconstructed_subject.context.user_agent == subject.context.user_agent
|
||||
assert DateTime.diff(reconstructed_subject.expires_at, one_day) <= 1
|
||||
end
|
||||
|
||||
test "updates last signed in fields for identity on success", %{
|
||||
identity: identity,
|
||||
subject: subject,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
context: context
|
||||
} do
|
||||
{:ok, token} = create_session_token_from_subject(subject)
|
||||
|
||||
assert {:ok, _subject} = sign_in(token, user_agent, remote_ip)
|
||||
assert {:ok, _subject} = sign_in(token, context)
|
||||
|
||||
assert updated_identity = Repo.one(Auth.Identity)
|
||||
assert updated_identity.last_seen_at != identity.last_seen_at
|
||||
@@ -2463,19 +2468,18 @@ defmodule Domain.AuthTest do
|
||||
# } do
|
||||
# user_agent = "iOS/12.6 (iPhone) connlib/0.7.412"
|
||||
# {:ok, token} = create_session_token_from_subject(subject)
|
||||
# assert sign_in(token, user_agent, remote_ip) == {:error, :unauthorized}
|
||||
# assert sign_in(token, context) == {:error, :unauthorized}
|
||||
# end
|
||||
|
||||
test "returns error when token is created for a deleted identity", %{
|
||||
identity: identity,
|
||||
subject: subject,
|
||||
user_agent: user_agent,
|
||||
remote_ip: remote_ip
|
||||
context: context
|
||||
} do
|
||||
{:ok, _identity} = delete_identity(identity, subject)
|
||||
|
||||
{:ok, token} = create_session_token_from_subject(subject)
|
||||
assert sign_in(token, user_agent, remote_ip) == {:error, :unauthorized}
|
||||
assert sign_in(token, context) == {:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -337,6 +337,14 @@ defmodule Domain.ClientsTest do
|
||||
refute is_nil(client.ipv6)
|
||||
|
||||
assert client.last_seen_remote_ip == %Postgrex.INET{address: subject.context.remote_ip}
|
||||
|
||||
assert client.last_seen_remote_ip_location_region ==
|
||||
subject.context.remote_ip_location_region
|
||||
|
||||
assert client.last_seen_remote_ip_location_city == subject.context.remote_ip_location_city
|
||||
assert client.last_seen_remote_ip_location_lat == subject.context.remote_ip_location_lat
|
||||
assert client.last_seen_remote_ip_location_lon == subject.context.remote_ip_location_lon
|
||||
|
||||
assert client.last_seen_user_agent == subject.context.user_agent
|
||||
assert client.last_seen_version == "0.7.412"
|
||||
assert client.last_seen_at
|
||||
@@ -353,6 +361,10 @@ defmodule Domain.ClientsTest do
|
||||
| context: %Domain.Auth.Context{
|
||||
subject.context
|
||||
| remote_ip: {100, 64, 100, 101},
|
||||
remote_ip_location_region: "Mexico",
|
||||
remote_ip_location_city: "Merida",
|
||||
remote_ip_location_lat: 7.7758,
|
||||
remote_ip_location_lon: -2.4128,
|
||||
user_agent: "iOS/12.5 (iPhone) connlib/0.7.411"
|
||||
}
|
||||
}
|
||||
@@ -376,6 +388,18 @@ defmodule Domain.ClientsTest do
|
||||
assert updated_client.ipv6 == client.ipv6
|
||||
assert updated_client.last_seen_at
|
||||
assert updated_client.last_seen_at != client.last_seen_at
|
||||
|
||||
assert updated_client.last_seen_remote_ip_location_region ==
|
||||
subject.context.remote_ip_location_region
|
||||
|
||||
assert updated_client.last_seen_remote_ip_location_city ==
|
||||
subject.context.remote_ip_location_city
|
||||
|
||||
assert updated_client.last_seen_remote_ip_location_lat ==
|
||||
subject.context.remote_ip_location_lat
|
||||
|
||||
assert updated_client.last_seen_remote_ip_location_lon ==
|
||||
subject.context.remote_ip_location_lon
|
||||
end
|
||||
|
||||
test "does not reserve additional addresses on update", %{
|
||||
|
||||
@@ -556,7 +556,11 @@ defmodule Domain.GatewaysTest do
|
||||
external_id: nil,
|
||||
public_key: "x",
|
||||
last_seen_user_agent: "foo",
|
||||
last_seen_remote_ip: {256, 0, 0, 0}
|
||||
last_seen_remote_ip: {256, 0, 0, 0},
|
||||
last_seen_remote_ip_location_region: -1,
|
||||
last_seen_remote_ip_location_city: -1,
|
||||
last_seen_remote_ip_location_lat: :x,
|
||||
last_seen_remote_ip_location_lon: :x
|
||||
}
|
||||
|
||||
assert {:error, changeset} = upsert_gateway(token, attrs)
|
||||
@@ -564,7 +568,11 @@ defmodule Domain.GatewaysTest do
|
||||
assert errors_on(changeset) == %{
|
||||
public_key: ["should be 44 character(s)", "must be a base64-encoded string"],
|
||||
external_id: ["can't be blank"],
|
||||
last_seen_user_agent: ["is invalid"]
|
||||
last_seen_user_agent: ["is invalid"],
|
||||
last_seen_remote_ip_location_region: ["is invalid"],
|
||||
last_seen_remote_ip_location_city: ["is invalid"],
|
||||
last_seen_remote_ip_location_lat: ["is invalid"],
|
||||
last_seen_remote_ip_location_lon: ["is invalid"]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -590,6 +598,13 @@ defmodule Domain.GatewaysTest do
|
||||
assert gateway.last_seen_user_agent == attrs.last_seen_user_agent
|
||||
assert gateway.last_seen_version == "0.7.412"
|
||||
assert gateway.last_seen_at
|
||||
|
||||
assert gateway.last_seen_remote_ip_location_region ==
|
||||
attrs.last_seen_remote_ip_location_region
|
||||
|
||||
assert gateway.last_seen_remote_ip_location_city == attrs.last_seen_remote_ip_location_city
|
||||
assert gateway.last_seen_remote_ip_location_lat == attrs.last_seen_remote_ip_location_lat
|
||||
assert gateway.last_seen_remote_ip_location_lon == attrs.last_seen_remote_ip_location_lon
|
||||
end
|
||||
|
||||
test "updates gateway when it already exists", %{
|
||||
@@ -601,7 +616,11 @@ defmodule Domain.GatewaysTest do
|
||||
Fixtures.Gateways.gateway_attrs(
|
||||
external_id: gateway.external_id,
|
||||
last_seen_remote_ip: {100, 64, 100, 101},
|
||||
last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411"
|
||||
last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411",
|
||||
last_seen_remote_ip_location_region: "Mexico",
|
||||
last_seen_remote_ip_location_city: "Merida",
|
||||
last_seen_remote_ip_location_lat: 7.7758,
|
||||
last_seen_remote_ip_location_lon: -2.4128
|
||||
)
|
||||
|
||||
assert {:ok, updated_gateway} = upsert_gateway(token, attrs)
|
||||
@@ -624,6 +643,18 @@ defmodule Domain.GatewaysTest do
|
||||
|
||||
assert updated_gateway.ipv4 == gateway.ipv4
|
||||
assert updated_gateway.ipv6 == gateway.ipv6
|
||||
|
||||
assert updated_gateway.last_seen_remote_ip_location_region ==
|
||||
attrs.last_seen_remote_ip_location_region
|
||||
|
||||
assert updated_gateway.last_seen_remote_ip_location_city ==
|
||||
attrs.last_seen_remote_ip_location_city
|
||||
|
||||
assert updated_gateway.last_seen_remote_ip_location_lat ==
|
||||
attrs.last_seen_remote_ip_location_lat
|
||||
|
||||
assert updated_gateway.last_seen_remote_ip_location_lon ==
|
||||
attrs.last_seen_remote_ip_location_lon
|
||||
end
|
||||
|
||||
test "does not reserve additional addresses on update", %{
|
||||
@@ -771,31 +802,111 @@ defmodule Domain.GatewaysTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "load_balance_gateways/1" do
|
||||
describe "load_balance_gateways/2" do
|
||||
test "returns nil when there are no gateways" do
|
||||
assert load_balance_gateways({0, 0}, []) == nil
|
||||
end
|
||||
|
||||
test "returns random gateway" do
|
||||
gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end)
|
||||
assert Enum.member?(gateways, load_balance_gateways(gateways))
|
||||
assert Enum.member?(gateways, load_balance_gateways({0, 0}, gateways))
|
||||
end
|
||||
|
||||
test "returns random gateways when there are no coordinates" do
|
||||
gateway_1 = Fixtures.Gateways.create_gateway()
|
||||
gateway_2 = Fixtures.Gateways.create_gateway()
|
||||
gateway_3 = Fixtures.Gateways.create_gateway()
|
||||
|
||||
assert gateway = load_balance_gateways({nil, nil}, [gateway_1, gateway_2, gateway_3])
|
||||
assert gateway.id in [gateway_1.id, gateway_2.id, gateway_3.id]
|
||||
end
|
||||
|
||||
test "returns gateways in two closest regions to a given location" do
|
||||
# Moncks Corner, South Carolina
|
||||
gateway_us_east_1 =
|
||||
Fixtures.Gateways.create_gateway(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
gateway_us_east_2 =
|
||||
Fixtures.Gateways.create_gateway(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
gateway_us_east_3 =
|
||||
Fixtures.Gateways.create_gateway(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
# The Dalles, Oregon
|
||||
gateway_us_west_1 =
|
||||
Fixtures.Gateways.create_gateway(
|
||||
last_seen_remote_ip_location_lat: 45.5946,
|
||||
last_seen_remote_ip_location_lon: -121.1787
|
||||
)
|
||||
|
||||
gateway_us_west_2 =
|
||||
Fixtures.Gateways.create_gateway(
|
||||
last_seen_remote_ip_location_lat: 45.5946,
|
||||
last_seen_remote_ip_location_lon: -121.1787
|
||||
)
|
||||
|
||||
# Council Bluffs, Iowa
|
||||
gateway_us_central_1 =
|
||||
Fixtures.Gateways.create_gateway(
|
||||
last_seen_remote_ip_location_lat: 41.2619,
|
||||
last_seen_remote_ip_location_lon: -95.8608
|
||||
)
|
||||
|
||||
gateways = [
|
||||
gateway_us_east_1,
|
||||
gateway_us_east_2,
|
||||
gateway_us_east_3,
|
||||
gateway_us_west_1,
|
||||
gateway_us_west_2,
|
||||
gateway_us_central_1
|
||||
]
|
||||
|
||||
# multiple attempts are used to increase chances that all gateways in a group are randomly selected
|
||||
for _ <- 0..3 do
|
||||
assert gateway = load_balance_gateways({32.2029, -80.0131}, gateways)
|
||||
assert gateway.id in [gateway_us_east_1.id, gateway_us_east_2.id, gateway_us_east_3.id]
|
||||
end
|
||||
|
||||
for _ <- 0..2 do
|
||||
assert gateway = load_balance_gateways({45.5946, -121.1787}, gateways)
|
||||
assert gateway.id in [gateway_us_west_1.id, gateway_us_west_2.id]
|
||||
end
|
||||
|
||||
assert gateway = load_balance_gateways({42.2619, -96.8608}, gateways)
|
||||
assert gateway.id == gateway_us_central_1.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "load_balance_gateways/2" do
|
||||
describe "load_balance_gateways/3" do
|
||||
test "returns random gateway if no gateways are already connected" do
|
||||
gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end)
|
||||
assert Enum.member?(gateways, load_balance_gateways(gateways, []))
|
||||
assert Enum.member?(gateways, load_balance_gateways({0, 0}, gateways, []))
|
||||
end
|
||||
|
||||
test "reuses gateway that is already connected to reduce the latency" do
|
||||
gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end)
|
||||
[connected_gateway | _] = gateways
|
||||
|
||||
assert load_balance_gateways(gateways, [connected_gateway.id]) == connected_gateway
|
||||
assert load_balance_gateways({0, 0}, gateways, [connected_gateway.id]) == connected_gateway
|
||||
end
|
||||
|
||||
test "returns random gateway from the connected ones" do
|
||||
gateways = Enum.map(1..10, fn _ -> Fixtures.Gateways.create_gateway() end)
|
||||
[connected_gateway1, connected_gateway2 | _] = gateways
|
||||
|
||||
assert load_balance_gateways(gateways, [connected_gateway1.id, connected_gateway2.id]) in [
|
||||
assert load_balance_gateways({0, 0}, gateways, [
|
||||
connected_gateway1.id,
|
||||
connected_gateway2.id
|
||||
]) in [
|
||||
connected_gateway1,
|
||||
connected_gateway2
|
||||
]
|
||||
|
||||
20
elixir/apps/domain/test/domain/geo_test.exs
Normal file
20
elixir/apps/domain/test/domain/geo_test.exs
Normal file
@@ -0,0 +1,20 @@
|
||||
defmodule Domain.GeoTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Geo
|
||||
|
||||
describe "distance/2" do
|
||||
test "calculates distance between two identical points" do
|
||||
assert distance({34.0522, -118.2437}, {34.0522, -118.2437}) == 0
|
||||
end
|
||||
|
||||
test "calculates distance between Los Angeles and San Francisco" do
|
||||
distance = distance({34.0522, -118.2437}, {37.7749, -122.4194})
|
||||
assert 558 < distance and distance < 560
|
||||
end
|
||||
|
||||
test "calculates distance between New York and London" do
|
||||
distance = distance({40.7128, -74.0060}, {51.5074, -0.1278})
|
||||
assert 5569 < distance and distance < 5571
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -74,9 +74,7 @@ defmodule Domain.GoogleCloudPlatformTest do
|
||||
}
|
||||
] = nodes
|
||||
|
||||
assert_receive {:bypass_request, conn}
|
||||
|
||||
filters = conn.params["filter"]
|
||||
assert_receive {:bypass_request, %{params: %{"filter" => filters}}}
|
||||
|
||||
assert Enum.sort(String.split(filters, " AND ")) == [
|
||||
"labels.cluster_name=firezone",
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule Domain.Jobs.Executors.GlobalTest do
|
||||
})
|
||||
|
||||
refute_receive {:executed, _pid, _time}, 50
|
||||
assert_receive {:executed, _pid, _time}, 400
|
||||
assert_receive {:executed, _pid, _time}, 1000
|
||||
end
|
||||
|
||||
test "registers itself as a leader if there is no global name registered" do
|
||||
|
||||
@@ -575,7 +575,11 @@ defmodule Domain.RelaysTest do
|
||||
ipv4: "1.1.1.256",
|
||||
ipv6: "fd01::10000",
|
||||
last_seen_user_agent: "foo",
|
||||
last_seen_remote_ip: {256, 0, 0, 0},
|
||||
last_seen_remote_ip: -1,
|
||||
last_seen_remote_ip_location_region: -1,
|
||||
last_seen_remote_ip_location_city: -1,
|
||||
last_seen_remote_ip_location_lat: :x,
|
||||
last_seen_remote_ip_location_lon: :x,
|
||||
port: -1
|
||||
}
|
||||
|
||||
@@ -585,6 +589,11 @@ defmodule Domain.RelaysTest do
|
||||
ipv4: ["one of these fields must be present: ipv4, ipv6", "is invalid"],
|
||||
ipv6: ["one of these fields must be present: ipv4, ipv6", "is invalid"],
|
||||
last_seen_user_agent: ["is invalid"],
|
||||
last_seen_remote_ip: ["is invalid"],
|
||||
last_seen_remote_ip_location_region: ["is invalid"],
|
||||
last_seen_remote_ip_location_city: ["is invalid"],
|
||||
last_seen_remote_ip_location_lat: ["is invalid"],
|
||||
last_seen_remote_ip_location_lon: ["is invalid"],
|
||||
port: ["must be greater than or equal to 1"]
|
||||
}
|
||||
|
||||
@@ -609,6 +618,14 @@ defmodule Domain.RelaysTest do
|
||||
assert relay.ipv6.address == attrs.ipv6
|
||||
|
||||
assert relay.last_seen_remote_ip.address == attrs.last_seen_remote_ip
|
||||
|
||||
assert relay.last_seen_remote_ip_location_region ==
|
||||
attrs.last_seen_remote_ip_location_region
|
||||
|
||||
assert relay.last_seen_remote_ip_location_city == attrs.last_seen_remote_ip_location_city
|
||||
assert relay.last_seen_remote_ip_location_lat == attrs.last_seen_remote_ip_location_lat
|
||||
assert relay.last_seen_remote_ip_location_lon == attrs.last_seen_remote_ip_location_lon
|
||||
|
||||
assert relay.last_seen_user_agent == attrs.last_seen_user_agent
|
||||
assert relay.last_seen_version == "0.7.412"
|
||||
assert relay.last_seen_at
|
||||
@@ -639,7 +656,11 @@ defmodule Domain.RelaysTest do
|
||||
Fixtures.Relays.relay_attrs(
|
||||
ipv4: relay.ipv4,
|
||||
last_seen_remote_ip: relay.ipv4,
|
||||
last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411"
|
||||
last_seen_user_agent: "iOS/12.5 (iPhone) connlib/0.7.411",
|
||||
last_seen_remote_ip_location_region: "Mexico",
|
||||
last_seen_remote_ip_location_city: "Merida",
|
||||
last_seen_remote_ip_location_lat: 7.7758,
|
||||
last_seen_remote_ip_location_lon: -2.4128
|
||||
)
|
||||
|
||||
assert {:ok, updated_relay} = upsert_relay(token, attrs)
|
||||
@@ -661,6 +682,18 @@ defmodule Domain.RelaysTest do
|
||||
assert updated_relay.ipv6 != relay.ipv6
|
||||
assert updated_relay.port == 3478
|
||||
|
||||
assert updated_relay.last_seen_remote_ip_location_region ==
|
||||
attrs.last_seen_remote_ip_location_region
|
||||
|
||||
assert updated_relay.last_seen_remote_ip_location_city ==
|
||||
attrs.last_seen_remote_ip_location_city
|
||||
|
||||
assert updated_relay.last_seen_remote_ip_location_lat ==
|
||||
attrs.last_seen_remote_ip_location_lat
|
||||
|
||||
assert updated_relay.last_seen_remote_ip_location_lon ==
|
||||
attrs.last_seen_remote_ip_location_lon
|
||||
|
||||
assert Repo.aggregate(Domain.Network.Address, :count) == 0
|
||||
end
|
||||
|
||||
@@ -763,6 +796,111 @@ defmodule Domain.RelaysTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "load_balance_relays/2" do
|
||||
test "returns empty list when there are no relays" do
|
||||
assert load_balance_relays({0, 0}, []) == []
|
||||
end
|
||||
|
||||
test "returns random relays when there are no coordinates" do
|
||||
relay_1 = Fixtures.Relays.create_relay()
|
||||
relay_2 = Fixtures.Relays.create_relay()
|
||||
relay_3 = Fixtures.Relays.create_relay()
|
||||
|
||||
assert relays = load_balance_relays({nil, nil}, [relay_1, relay_2, relay_3])
|
||||
assert length(relays) == 2
|
||||
assert Enum.all?(relays, &(&1.id in [relay_1.id, relay_2.id, relay_3.id]))
|
||||
end
|
||||
|
||||
test "returns at least two relays even if they are at the same location" do
|
||||
# Moncks Corner, South Carolina
|
||||
relay_us_east_1 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
relay_us_east_2 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
relays = [
|
||||
relay_us_east_1,
|
||||
relay_us_east_2
|
||||
]
|
||||
|
||||
assert [relay1] = load_balance_relays({32.2029, -80.0131}, relays)
|
||||
assert relay1.id in [relay_us_east_1.id, relay_us_east_2.id]
|
||||
end
|
||||
|
||||
test "selects relays in two closest regions to a given location" do
|
||||
# Moncks Corner, South Carolina
|
||||
relay_us_east_1 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
relay_us_east_2 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
relay_us_east_3 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 33.2029,
|
||||
last_seen_remote_ip_location_lon: -80.0131
|
||||
)
|
||||
|
||||
# The Dalles, Oregon
|
||||
relay_us_west_1 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 45.5946,
|
||||
last_seen_remote_ip_location_lon: -121.1787
|
||||
)
|
||||
|
||||
relay_us_west_2 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 45.5946,
|
||||
last_seen_remote_ip_location_lon: -121.1787
|
||||
)
|
||||
|
||||
# Council Bluffs, Iowa
|
||||
relay_us_central_1 =
|
||||
Fixtures.Relays.create_relay(
|
||||
last_seen_remote_ip_location_lat: 41.2619,
|
||||
last_seen_remote_ip_location_lon: -95.8608
|
||||
)
|
||||
|
||||
relays = [
|
||||
relay_us_east_1,
|
||||
relay_us_east_2,
|
||||
relay_us_east_3,
|
||||
relay_us_west_1,
|
||||
relay_us_west_2,
|
||||
relay_us_central_1
|
||||
]
|
||||
|
||||
# multiple attempts are used to increase chances that all relays in a group are randomly selected
|
||||
for _ <- 0..3 do
|
||||
assert [relay1, relay2] = load_balance_relays({32.2029, -80.0131}, relays)
|
||||
assert relay1.id in [relay_us_east_1.id, relay_us_east_2.id, relay_us_east_3.id]
|
||||
assert relay2.id == relay_us_central_1.id
|
||||
end
|
||||
|
||||
for _ <- 0..2 do
|
||||
assert [relay1, relay2] = load_balance_relays({45.5946, -121.1787}, relays)
|
||||
assert relay1.id in [relay_us_west_1.id, relay_us_west_2.id]
|
||||
assert relay2.id == relay_us_central_1.id
|
||||
end
|
||||
|
||||
assert [relay1, _relay2] = load_balance_relays({42.2619, -96.8608}, relays)
|
||||
assert relay1.id == relay_us_central_1.id
|
||||
end
|
||||
end
|
||||
|
||||
describe "encode_token!/1" do
|
||||
test "returns encoded token" do
|
||||
token = Fixtures.Relays.create_token()
|
||||
|
||||
@@ -333,12 +333,41 @@ defmodule Domain.Fixtures.Auth do
|
||||
user_agent()
|
||||
end)
|
||||
|
||||
{remote_ip, _attrs} =
|
||||
{remote_ip, attrs} =
|
||||
Map.pop_lazy(attrs, :remote_ip, fn ->
|
||||
remote_ip()
|
||||
end)
|
||||
|
||||
Auth.build_subject(identity, expires_at, user_agent, remote_ip)
|
||||
{remote_ip_location_region, attrs} =
|
||||
Map.pop_lazy(attrs, :remote_ip_location_region, fn ->
|
||||
Enum.random(["US", "UA"])
|
||||
end)
|
||||
|
||||
{remote_ip_location_city, attrs} =
|
||||
Map.pop_lazy(attrs, :remote_ip_location_city, fn ->
|
||||
Enum.random(["Odessa", "New York"])
|
||||
end)
|
||||
|
||||
{remote_ip_location_lat, attrs} =
|
||||
Map.pop_lazy(attrs, :remote_ip_location_city, fn ->
|
||||
Enum.random([37.7758, 40.7128])
|
||||
end)
|
||||
|
||||
{remote_ip_location_lon, _attrs} =
|
||||
Map.pop_lazy(attrs, :remote_ip_location_city, fn ->
|
||||
Enum.random([-122.4128, -74.0060])
|
||||
end)
|
||||
|
||||
context = %Auth.Context{
|
||||
remote_ip: remote_ip,
|
||||
remote_ip_location_region: remote_ip_location_region,
|
||||
remote_ip_location_city: remote_ip_location_city,
|
||||
remote_ip_location_lat: remote_ip_location_lat,
|
||||
remote_ip_location_lon: remote_ip_location_lon,
|
||||
user_agent: user_agent
|
||||
}
|
||||
|
||||
Auth.build_subject(identity, expires_at, context)
|
||||
end
|
||||
|
||||
def remove_permissions(%Auth.Subject{} = subject) do
|
||||
|
||||
@@ -6,7 +6,13 @@ defmodule Domain.Fixtures.Clients do
|
||||
Enum.into(attrs, %{
|
||||
external_id: Ecto.UUID.generate(),
|
||||
name: "client-#{unique_integer()}",
|
||||
public_key: unique_public_key()
|
||||
public_key: unique_public_key(),
|
||||
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
|
||||
last_seen_remote_ip: Enum.random([unique_ipv4(), unique_ipv6()]),
|
||||
last_seen_remote_ip_location_region: "US",
|
||||
last_seen_remote_ip_location_city: "San Francisco",
|
||||
last_seen_remote_ip_location_lat: 37.7758,
|
||||
last_seen_remote_ip_location_lon: -122.4128
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -74,7 +74,11 @@ defmodule Domain.Fixtures.Gateways do
|
||||
name_suffix: "gw-#{Domain.Crypto.random_token(5, encoder: :user_friendly)}",
|
||||
public_key: unique_public_key(),
|
||||
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
|
||||
last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}}
|
||||
last_seen_remote_ip: %Postgrex.INET{address: {189, 172, 73, 153}},
|
||||
last_seen_remote_ip_location_region: "US",
|
||||
last_seen_remote_ip_location_city: "San Francisco",
|
||||
last_seen_remote_ip_location_lat: 37.7758,
|
||||
last_seen_remote_ip_location_lon: -122.4128
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ defmodule Domain.Fixtures.Relays do
|
||||
ipv4: ipv4,
|
||||
ipv6: unique_ipv6(),
|
||||
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/0.7.412",
|
||||
last_seen_remote_ip: ipv4
|
||||
last_seen_remote_ip: ipv4,
|
||||
last_seen_remote_ip_location_region: "Mexico",
|
||||
last_seen_remote_ip_location_city: "Merida",
|
||||
last_seen_remote_ip_location_lat: 37.7749,
|
||||
last_seen_remote_ip_location_lon: -120.4194
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
@@ -135,9 +135,21 @@ defmodule Web.Auth do
|
||||
Fetches the session token from the session and assigns the subject to the connection.
|
||||
"""
|
||||
def fetch_subject_and_account(%Plug.Conn{} = conn, _opts) do
|
||||
{location_region, location_city, {location_lat, location_lon}} =
|
||||
get_load_balancer_ip_location(conn)
|
||||
|
||||
context = %Auth.Context{
|
||||
user_agent: conn.assigns.user_agent,
|
||||
remote_ip: conn.remote_ip,
|
||||
remote_ip_location_region: location_region,
|
||||
remote_ip_location_city: location_city,
|
||||
remote_ip_location_lat: location_lat,
|
||||
remote_ip_location_lon: location_lon
|
||||
}
|
||||
|
||||
with token when not is_nil(token) <- Plug.Conn.get_session(conn, :session_token),
|
||||
{:ok, subject} <-
|
||||
Domain.Auth.sign_in(token, conn.assigns.user_agent, conn.remote_ip),
|
||||
Domain.Auth.sign_in(token, context),
|
||||
{:ok, account} <-
|
||||
Domain.Accounts.fetch_account_by_id_or_slug(
|
||||
conn.path_params["account_id_or_slug"],
|
||||
@@ -151,6 +163,66 @@ defmodule Web.Auth do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_load_balancer_ip_location(%Plug.Conn{} = conn) do
|
||||
location_region =
|
||||
case Plug.Conn.get_req_header(conn, "x-geo-location-region") do
|
||||
[location_region | _] -> location_region
|
||||
[] -> nil
|
||||
end
|
||||
|
||||
location_city =
|
||||
case Plug.Conn.get_req_header(conn, "x-geo-location-city") do
|
||||
[location_city | _] -> location_city
|
||||
[] -> nil
|
||||
end
|
||||
|
||||
{location_lat, location_lon} =
|
||||
case Plug.Conn.get_req_header(conn, "x-geo-location-coordinates") do
|
||||
[coordinates | _] ->
|
||||
[lat, lon] = String.split(coordinates, ",", parts: 2)
|
||||
lat = String.to_float(lat)
|
||||
lon = String.to_float(lon)
|
||||
{lat, lon}
|
||||
|
||||
[] ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
{location_region, location_city, {location_lat, location_lon}}
|
||||
end
|
||||
|
||||
defp get_load_balancer_ip_location(x_headers) do
|
||||
location_region =
|
||||
case get_socket_header(x_headers, "x-geo-location-region") do
|
||||
{"x-geo-location-region", location_region} -> location_region
|
||||
_other -> nil
|
||||
end
|
||||
|
||||
location_city =
|
||||
case get_socket_header(x_headers, "x-geo-location-city") do
|
||||
{"x-geo-location-city", location_city} -> location_city
|
||||
_other -> nil
|
||||
end
|
||||
|
||||
{location_lat, location_lon} =
|
||||
case get_socket_header(x_headers, "x-geo-location-coordinates") do
|
||||
{"x-geo-location-coordinates", coordinates} ->
|
||||
[lat, lon] = String.split(coordinates, ",", parts: 2)
|
||||
lat = String.to_float(lat)
|
||||
lon = String.to_float(lon)
|
||||
{lat, lon}
|
||||
|
||||
_other ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
{location_region, location_city, {location_lat, location_lon}}
|
||||
end
|
||||
|
||||
defp get_socket_header(x_headers, key) do
|
||||
List.keyfind(x_headers, key, 0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Used for routes that require the user to not be authenticated.
|
||||
"""
|
||||
@@ -293,9 +365,22 @@ defmodule Web.Auth do
|
||||
Phoenix.Component.assign_new(socket, :subject, fn ->
|
||||
user_agent = Phoenix.LiveView.get_connect_info(socket, :user_agent)
|
||||
real_ip = real_ip(socket)
|
||||
x_headers = Phoenix.LiveView.get_connect_info(socket, :x_headers) || []
|
||||
|
||||
{location_region, location_city, {location_lat, location_lon}} =
|
||||
get_load_balancer_ip_location(x_headers)
|
||||
|
||||
context = %Domain.Auth.Context{
|
||||
user_agent: user_agent,
|
||||
remote_ip: real_ip,
|
||||
remote_ip_location_region: location_region,
|
||||
remote_ip_location_city: location_city,
|
||||
remote_ip_location_lat: location_lat,
|
||||
remote_ip_location_lon: location_lon
|
||||
}
|
||||
|
||||
with token when not is_nil(token) <- session["session_token"],
|
||||
{:ok, subject} <- Domain.Auth.sign_in(token, user_agent, real_ip) do
|
||||
{:ok, subject} <- Domain.Auth.sign_in(token, context) do
|
||||
subject
|
||||
else
|
||||
_ -> nil
|
||||
|
||||
@@ -35,7 +35,17 @@ defmodule Web.AcceptanceCase.Auth do
|
||||
def authenticate(session, %Domain.Auth.Identity{} = identity) do
|
||||
user_agent = fetch_session_user_agent!(session)
|
||||
remote_ip = {127, 0, 0, 1}
|
||||
subject = Domain.Auth.build_subject(identity, nil, user_agent, remote_ip)
|
||||
|
||||
context = %Domain.Auth.Context{
|
||||
user_agent: user_agent,
|
||||
remote_ip_location_region: "UA",
|
||||
remote_ip_location_city: "Kyiv",
|
||||
remote_ip_location_lat: 50.4501,
|
||||
remote_ip_location_lon: 30.5234,
|
||||
remote_ip: remote_ip
|
||||
}
|
||||
|
||||
subject = Domain.Auth.build_subject(identity, nil, context)
|
||||
authenticate(session, subject)
|
||||
end
|
||||
|
||||
@@ -77,8 +87,8 @@ defmodule Web.AcceptanceCase.Auth do
|
||||
if token = cookie["session_token"] do
|
||||
user_agent = fetch_session_user_agent!(session)
|
||||
remote_ip = {127, 0, 0, 1}
|
||||
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(token, user_agent, remote_ip)
|
||||
context = %Domain.Auth.Context{user_agent: user_agent, remote_ip: remote_ip}
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(token, context)
|
||||
flunk("User is authenticated, identity: #{inspect(subject.identity)}")
|
||||
:ok
|
||||
else
|
||||
@@ -91,9 +101,11 @@ defmodule Web.AcceptanceCase.Auth do
|
||||
|
||||
def assert_authenticated(session, identity) do
|
||||
with {:ok, cookie} <- fetch_session_cookie(session),
|
||||
user_agent = fetch_session_user_agent!(session),
|
||||
remote_ip = {127, 0, 0, 1},
|
||||
{:ok, subject} <- Domain.Auth.sign_in(cookie["session_token"], user_agent, remote_ip) do
|
||||
context = %Domain.Auth.Context{
|
||||
user_agent: fetch_session_user_agent!(session),
|
||||
remote_ip: {127, 0, 0, 1}
|
||||
},
|
||||
{:ok, subject} <- Domain.Auth.sign_in(cookie["session_token"], context) do
|
||||
assert subject.identity.id == identity.id,
|
||||
"Expected #{inspect(identity)}, got #{inspect(subject.identity)}"
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ defmodule Web.ConnCase do
|
||||
Phoenix.ConnTest.build_conn()
|
||||
|> Plug.Conn.put_req_header("user-agent", user_agent)
|
||||
|> Plug.Test.init_test_session(%{})
|
||||
|> Plug.Conn.put_req_header("x-geo-location-region", "UA")
|
||||
|> Plug.Conn.put_req_header("x-geo-location-city", "Kyiv")
|
||||
|> Plug.Conn.put_req_header("x-geo-location-coordinates", "50.4333,30.5167")
|
||||
|
||||
{:ok, conn: conn, user_agent: user_agent}
|
||||
end
|
||||
@@ -46,7 +49,17 @@ defmodule Web.ConnCase do
|
||||
def authorize_conn(conn, %Domain.Auth.Identity{} = identity) do
|
||||
expires_in = DateTime.utc_now() |> DateTime.add(300, :second)
|
||||
{"user-agent", user_agent} = List.keyfind(conn.req_headers, "user-agent", 0, "FooBar 1.1")
|
||||
subject = Domain.Auth.build_subject(identity, expires_in, user_agent, conn.remote_ip)
|
||||
|
||||
context = %Domain.Auth.Context{
|
||||
user_agent: user_agent,
|
||||
remote_ip_location_region: "UA",
|
||||
remote_ip_location_city: "Kyiv",
|
||||
remote_ip_location_lat: 50.4501,
|
||||
remote_ip_location_lon: 30.5234,
|
||||
remote_ip: conn.remote_ip
|
||||
}
|
||||
|
||||
subject = Domain.Auth.build_subject(identity, expires_in, context)
|
||||
|
||||
conn
|
||||
|> Web.Auth.put_subject_in_session(subject)
|
||||
|
||||
@@ -35,8 +35,7 @@ defmodule Web.AuthTest do
|
||||
conn = put_subject_in_session(conn, subject)
|
||||
assert token = get_session(conn, "session_token")
|
||||
|
||||
assert {:ok, _subject} =
|
||||
Domain.Auth.sign_in(token, subject.context.user_agent, subject.context.remote_ip)
|
||||
assert {:ok, _subject} = Domain.Auth.sign_in(token, subject.context)
|
||||
end
|
||||
|
||||
test "persists sign in time in session", %{conn: conn, admin_subject: subject} do
|
||||
@@ -125,6 +124,30 @@ defmodule Web.AuthTest do
|
||||
assert conn.assigns.account.id == subject.account.id
|
||||
end
|
||||
|
||||
test "puts load balancer GeoIP headers to subject context", %{
|
||||
conn: conn,
|
||||
admin_subject: subject
|
||||
} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
|
||||
conn =
|
||||
%{
|
||||
conn
|
||||
| path_params: %{"account_id_or_slug" => subject.account.id},
|
||||
remote_ip: {100, 64, 100, 58}
|
||||
}
|
||||
|> put_req_header("x-geo-location-region", "Ukraine")
|
||||
|> put_req_header("x-geo-location-city", "Kyiv")
|
||||
|> put_req_header("x-geo-location-coordinates", "50.4333,30.5167")
|
||||
|> put_session(:session_token, session_token)
|
||||
|> fetch_subject_and_account([])
|
||||
|
||||
assert conn.assigns.subject.context.remote_ip_location_region == "Ukraine"
|
||||
assert conn.assigns.subject.context.remote_ip_location_city == "Kyiv"
|
||||
assert conn.assigns.subject.context.remote_ip_location_lat == 50.4333
|
||||
assert conn.assigns.subject.context.remote_ip_location_lon == 30.5167
|
||||
end
|
||||
|
||||
test "does not authenticate to an incorrect account", %{conn: conn, admin_subject: subject} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
|
||||
@@ -228,7 +251,12 @@ defmodule Web.AuthTest do
|
||||
private: %{
|
||||
connect_info: %{
|
||||
user_agent: context.admin_subject.context.user_agent,
|
||||
peer_data: %{address: {100, 64, 100, 58}}
|
||||
peer_data: %{address: {100, 64, 100, 58}},
|
||||
x_headers: [
|
||||
{"x-geo-location-region", "UA"},
|
||||
{"x-geo-location-city", "Kyiv"},
|
||||
{"x-geo-location-coordinates", "50.4333,30.5167"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,6 +275,25 @@ defmodule Web.AuthTest do
|
||||
|
||||
assert {:cont, updated_socket} = on_mount(:mount_subject, params, session, socket)
|
||||
assert updated_socket.assigns.subject.identity.id == subject.identity.id
|
||||
assert updated_socket.assigns.subject.context.user_agent == subject.context.user_agent
|
||||
assert updated_socket.assigns.subject.context.remote_ip == subject.context.remote_ip
|
||||
end
|
||||
|
||||
test "puts load balancer GeoIP information to subject context", %{
|
||||
conn: conn,
|
||||
socket: socket,
|
||||
admin_subject: subject
|
||||
} do
|
||||
{:ok, session_token} = Domain.Auth.create_session_token_from_subject(subject)
|
||||
session = conn |> put_session(:session_token, session_token) |> get_session()
|
||||
params = %{"account_id_or_slug" => subject.account.id}
|
||||
|
||||
assert {:cont, socket} = on_mount(:mount_subject, params, session, socket)
|
||||
|
||||
assert socket.assigns.subject.context.remote_ip_location_region == "UA"
|
||||
assert socket.assigns.subject.context.remote_ip_location_city == "Kyiv"
|
||||
assert socket.assigns.subject.context.remote_ip_location_lat == 50.4333
|
||||
assert socket.assigns.subject.context.remote_ip_location_lon == 30.5167
|
||||
end
|
||||
|
||||
test "assigns nil to subject assign if there isn't a valid session_token", %{
|
||||
|
||||
@@ -206,11 +206,24 @@ defmodule Web.AuthControllerTest do
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
context = %Domain.Auth.Context{
|
||||
remote_ip: conn.remote_ip,
|
||||
user_agent: "testing",
|
||||
remote_ip_location_region: "Mexico",
|
||||
remote_ip_location_city: "Merida",
|
||||
remote_ip_location_lat: 37.7749,
|
||||
remote_ip_location_lon: -120.4194
|
||||
}
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, context)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
assert subject.identity.last_seen_remote_ip_location_region == "Mexico"
|
||||
assert subject.identity.last_seen_remote_ip_location_city == "Merida"
|
||||
assert subject.identity.last_seen_remote_ip_location_lat == 37.7749
|
||||
assert subject.identity.last_seen_remote_ip_location_lon == -120.4194
|
||||
assert subject.identity.last_seen_at
|
||||
end
|
||||
|
||||
@@ -835,8 +848,10 @@ defmodule Web.AuthControllerTest do
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
context = %Domain.Auth.Context{remote_ip: conn.remote_ip, user_agent: "testing"}
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, context)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
@@ -1056,8 +1071,10 @@ defmodule Web.AuthControllerTest do
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
context = %Domain.Auth.Context{remote_ip: conn.remote_ip, user_agent: "testing"}
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, context)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
|
||||
@@ -144,9 +144,18 @@ defmodule Web.Live.Actors.ServiceAccounts.NewIdentityTest do
|
||||
|> form("form", identity: attrs)
|
||||
|> render_submit()
|
||||
|
||||
context = %Domain.Auth.Context{
|
||||
remote_ip: Fixtures.Auth.remote_ip(),
|
||||
user_agent: Fixtures.Auth.user_agent(),
|
||||
remote_ip_location_region: "Mexico",
|
||||
remote_ip_location_city: "Merida",
|
||||
remote_ip_location_lat: 37.7749,
|
||||
remote_ip_location_lon: -120.4194
|
||||
}
|
||||
|
||||
# TODO: assert {:ok, _token} =
|
||||
Floki.find(html, "code")
|
||||
|> element_to_text()
|
||||
|> Domain.Auth.sign_in(Fixtures.Auth.user_agent(), Fixtures.Auth.remote_ip())
|
||||
|> Domain.Auth.sign_in(context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -200,8 +200,17 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.Connect do
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
context = %Domain.Auth.Context{
|
||||
remote_ip: conn.remote_ip,
|
||||
user_agent: "testing",
|
||||
remote_ip_location_region: "Mexico",
|
||||
remote_ip_location_city: "Merida",
|
||||
remote_ip_location_lat: 37.7749,
|
||||
remote_ip_location_lon: -120.4194
|
||||
}
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, context)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
|
||||
@@ -192,8 +192,17 @@ defmodule Web.Live.Settings.IdentityProviders.OpenIDConnect.Connect do
|
||||
"session_token" => session_token
|
||||
} = conn.private.plug_session
|
||||
|
||||
context = %Domain.Auth.Context{
|
||||
remote_ip: conn.remote_ip,
|
||||
user_agent: "testing",
|
||||
remote_ip_location_region: "Mexico",
|
||||
remote_ip_location_city: "Merida",
|
||||
remote_ip_location_lat: 37.7749,
|
||||
remote_ip_location_lon: -120.4194
|
||||
}
|
||||
|
||||
assert socket_id == identity.actor_id
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, "testing", conn.remote_ip)
|
||||
assert {:ok, subject} = Domain.Auth.sign_in(session_token, context)
|
||||
assert subject.identity.id == identity.id
|
||||
assert subject.identity.last_seen_user_agent == "testing"
|
||||
assert subject.identity.last_seen_remote_ip.address == {127, 0, 0, 1}
|
||||
|
||||
@@ -418,8 +418,16 @@ resource "google_compute_backend_service" "default" {
|
||||
enable_cdn = false
|
||||
compression_mode = "DISABLED"
|
||||
|
||||
custom_request_headers = []
|
||||
custom_response_headers = []
|
||||
custom_request_headers = [
|
||||
"X-Geo-Location-Region:{client_region}",
|
||||
"X-Geo-Location-City:{client_city}",
|
||||
"X-Geo-Location-Coordinates:{client_city_lat_long}",
|
||||
"X-Client-IP:{client_ip}",
|
||||
]
|
||||
|
||||
custom_response_headers = [
|
||||
"X-Cache-Hit: {cdn_cache_status}"
|
||||
]
|
||||
|
||||
session_affinity = "CLIENT_IP"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user