GeoIP routing and load-balancing for traffic (#2517)

This commit is contained in:
Andrew Dryga
2023-10-31 15:01:37 -06:00
committed by GitHub
parent 2bca378f17
commit ad26e508ff
46 changed files with 921 additions and 146 deletions

View File

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

View File

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

View File

@@ -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}",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@@ -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!()

View File

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

View File

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

View File

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

View File

@@ -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: ")

View File

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

View File

@@ -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", %{

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)}"

View File

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

View File

@@ -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", %{

View File

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

View File

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

View File

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

View File

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

View File

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