diff --git a/elixir/apps/api/lib/api/client/channel.ex b/elixir/apps/api/lib/api/client/channel.ex index a607368a8..f2b5d9e10 100644 --- a/elixir/apps/api/lib/api/client/channel.ex +++ b/elixir/apps/api/lib/api/client/channel.ex @@ -107,7 +107,12 @@ defmodule API.Client.Channel do push(socket, "init", %{ resources: Views.Resource.render_many(resources), - relays: Views.Relay.render_many(relays, socket.assigns.subject.expires_at), + relays: + Views.Relay.render_many( + relays, + socket.assigns.turn_salt, + socket.assigns.subject.expires_at + ), interface: Views.Interface.render(%{ socket.assigns.client @@ -439,7 +444,12 @@ defmodule API.Client.Channel do payload = %{ disconnected_ids: [relay_id], - connected: Views.Relay.render_many(relays, socket.assigns.subject.expires_at) + connected: + Views.Relay.render_many( + relays, + socket.assigns.turn_salt, + socket.assigns.subject.expires_at + ) } {:noreply, Debouncer.queue_leave(self(), socket, relay_id, payload)} @@ -492,7 +502,12 @@ defmodule API.Client.Channel do push(socket, "relays_presence", %{ disconnected_ids: disconnected_ids, - connected: Views.Relay.render_many(relays, socket.assigns.subject.expires_at) + connected: + Views.Relay.render_many( + relays, + socket.assigns.turn_salt, + socket.assigns.subject.expires_at + ) }) {:noreply, socket} diff --git a/elixir/apps/api/lib/api/client/socket.ex b/elixir/apps/api/lib/api/client/socket.ex index 9dfdfe428..3929998e3 100644 --- a/elixir/apps/api/lib/api/client/socket.ex +++ b/elixir/apps/api/lib/api/client/socket.ex @@ -28,8 +28,14 @@ defmodule API.Client.Socket do account_id: subject.account.id }) + # For Relay credentials + turn_salt = + Domain.Crypto.hash(:sha256, token) + |> Base.url_encode64(padding: false) + socket = socket + |> assign(:turn_salt, turn_salt) |> assign(:subject, subject) |> assign(:client, client) |> assign(:opentelemetry_span_ctx, OpenTelemetry.Tracer.current_span_ctx()) diff --git a/elixir/apps/api/lib/api/client/views/relay.ex b/elixir/apps/api/lib/api/client/views/relay.ex index 90efb95d4..96660917a 100644 --- a/elixir/apps/api/lib/api/client/views/relay.ex +++ b/elixir/apps/api/lib/api/client/views/relay.ex @@ -1,52 +1,34 @@ defmodule API.Client.Views.Relay do alias Domain.Relays - def render_many(relays, expires_at, stun_or_turn \\ :turn) do - Enum.flat_map(relays, &render(&1, expires_at, stun_or_turn)) - end - - def render(%Relays.Relay{} = relay, expires_at, stun_or_turn) do - [ - maybe_render(relay, expires_at, relay.ipv4, stun_or_turn), - maybe_render(relay, expires_at, relay.ipv6, stun_or_turn) - ] + def render_many(relays, salt, expires_at) do + relays + |> Enum.map(fn relay -> + [ + render_addr(relay, salt, expires_at, relay.ipv4), + render_addr(relay, salt, expires_at, relay.ipv6) + ] + end) |> List.flatten() end - defp maybe_render(%Relays.Relay{}, _expires_at, nil, _stun_or_turn), do: [] + defp render_addr(_relay, _salt, _expires_at, nil), do: [] - # STUN returns the reflective candidates to the peer and is used for hole-punching; - # TURN is used to real actual traffic if hole-punching fails. It requires authentication. - # WebRTC will automatically fail back to STUN if TURN fails, - # so there is no need to send both of them along with each other. - - defp maybe_render(%Relays.Relay{} = relay, _expires_at, address, :stun) do - [ - %{ - id: relay.id, - type: :stun, - addr: "#{format_address(address)}:#{relay.port}" - } - ] - end - - defp maybe_render(%Relays.Relay{} = relay, expires_at, address, :turn) do + defp render_addr(%Relays.Relay{} = relay, salt, expires_at, address) do %{ username: username, password: password, expires_at: expires_at - } = Relays.generate_username_and_password(relay, expires_at) + } = Relays.generate_username_and_password(relay, salt, expires_at) - [ - %{ - id: relay.id, - type: :turn, - addr: "#{format_address(address)}:#{relay.port}", - username: username, - password: password, - expires_at: expires_at - } - ] + %{ + id: relay.id, + type: :turn, + addr: "#{format_address(address)}:#{relay.port}", + username: username, + password: password, + expires_at: expires_at + } end defp format_address(%Postgrex.INET{address: address} = inet) when tuple_size(address) == 4, diff --git a/elixir/apps/api/lib/api/gateway/channel.ex b/elixir/apps/api/lib/api/gateway/channel.ex index a11ef8c4a..a8ba5a8bb 100644 --- a/elixir/apps/api/lib/api/gateway/channel.ex +++ b/elixir/apps/api/lib/api/gateway/channel.ex @@ -39,7 +39,7 @@ defmodule API.Gateway.Channel do :ok = Gateways.Presence.connect(socket.assigns.gateway) # Return all connected relays for the account - relay_credentials_expire_at = DateTime.utc_now() |> DateTime.add(14, :day) + relay_credentials_expire_at = DateTime.utc_now() |> DateTime.add(90, :day) {:ok, relays} = select_relays(socket) :ok = Enum.each(relays, &Domain.Relays.subscribe_to_relay_presence/1) :ok = maybe_subscribe_for_relays_presence(relays, socket) @@ -49,7 +49,8 @@ defmodule API.Gateway.Channel do push(socket, "init", %{ account_slug: account.slug, interface: Views.Interface.render(socket.assigns.gateway), - relays: Views.Relay.render_many(relays, relay_credentials_expire_at), + relays: + Views.Relay.render_many(relays, socket.assigns.turn_salt, relay_credentials_expire_at), # These aren't used but needed for API compatibility config: %{ ipv4_masquerade_enabled: true, @@ -165,7 +166,7 @@ defmodule API.Gateway.Channel do } do :ok = Domain.Relays.unsubscribe_from_relay_presence(relay_id) - relay_credentials_expire_at = DateTime.utc_now() |> DateTime.add(14, :day) + relay_credentials_expire_at = DateTime.utc_now() |> DateTime.add(90, :day) {:ok, relays} = select_relays(socket, [relay_id]) :ok = maybe_subscribe_for_relays_presence(relays, socket) @@ -179,7 +180,12 @@ defmodule API.Gateway.Channel do payload = %{ disconnected_ids: [relay_id], - connected: Views.Relay.render_many(relays, relay_credentials_expire_at) + connected: + Views.Relay.render_many( + relays, + socket.assigns.turn_salt, + relay_credentials_expire_at + ) } socket = Debouncer.queue_leave(self(), socket, relay_id, payload) @@ -207,7 +213,7 @@ defmodule API.Gateway.Channel do {:ok, relays} = select_relays(socket) if length(relays) > 0 do - relay_credentials_expire_at = DateTime.utc_now() |> DateTime.add(14, :day) + relay_credentials_expire_at = DateTime.utc_now() |> DateTime.add(90, :day) :ok = Relays.unsubscribe_from_relays_presence_in_account(socket.assigns.gateway.account_id) @@ -235,7 +241,12 @@ defmodule API.Gateway.Channel do push(socket, "relays_presence", %{ disconnected_ids: disconnected_ids, - connected: Views.Relay.render_many(relays, relay_credentials_expire_at) + connected: + Views.Relay.render_many( + relays, + socket.assigns.turn_salt, + relay_credentials_expire_at + ) }) {:noreply, socket} diff --git a/elixir/apps/api/lib/api/gateway/socket.ex b/elixir/apps/api/lib/api/gateway/socket.ex index bd5363684..90ae967e0 100644 --- a/elixir/apps/api/lib/api/gateway/socket.ex +++ b/elixir/apps/api/lib/api/gateway/socket.ex @@ -27,8 +27,14 @@ defmodule API.Gateway.Socket do version: gateway.last_seen_version }) + # For Relay credentials + turn_salt = + Domain.Crypto.hash(:sha256, encoded_token) + |> Base.url_encode64(padding: false) + socket = socket + |> assign(:turn_salt, turn_salt) |> assign(:token_id, token.id) |> assign(:gateway_group, group) |> assign(:gateway, gateway) diff --git a/elixir/apps/api/lib/api/gateway/views/relay.ex b/elixir/apps/api/lib/api/gateway/views/relay.ex index 56a707e9e..679b092a1 100644 --- a/elixir/apps/api/lib/api/gateway/views/relay.ex +++ b/elixir/apps/api/lib/api/gateway/views/relay.ex @@ -1,5 +1,5 @@ defmodule API.Gateway.Views.Relay do - def render_many(relays, expires_at) do - API.Client.Views.Relay.render_many(relays, expires_at) + def render_many(relays, salt, expires_at) do + API.Client.Views.Relay.render_many(relays, salt, expires_at) end end diff --git a/elixir/apps/api/test/api/client/channel_test.exs b/elixir/apps/api/test/api/client/channel_test.exs index fb7c2c7dd..10d6b1a7f 100644 --- a/elixir/apps/api/test/api/client/channel_test.exs +++ b/elixir/apps/api/test/api/client/channel_test.exs @@ -137,7 +137,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -191,7 +192,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -213,7 +215,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -231,7 +234,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -245,7 +249,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -258,7 +263,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") == {:error, %{reason: :invalid_version}} @@ -397,7 +403,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -470,7 +477,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -557,7 +565,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -609,7 +618,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -673,7 +683,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -1435,7 +1446,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -1509,7 +1521,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -1862,7 +1875,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -1931,7 +1945,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -2142,7 +2157,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") @@ -2353,7 +2369,8 @@ defmodule API.Client.ChannelTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") diff --git a/elixir/apps/api/test/api/gateway/channel_test.exs b/elixir/apps/api/test/api/gateway/channel_test.exs index 8460535fa..de2af9aed 100644 --- a/elixir/apps/api/test/api/gateway/channel_test.exs +++ b/elixir/apps/api/test/api/gateway/channel_test.exs @@ -23,7 +23,8 @@ defmodule API.Gateway.ChannelTest do gateway: gateway, gateway_group: gateway_group, opentelemetry_ctx: OpenTelemetry.Ctx.new(), - opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test") + opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), + turn_salt: "test_salt" }) |> subscribe_and_join(API.Gateway.Channel, "gateway") @@ -341,7 +342,8 @@ defmodule API.Gateway.ChannelTest do gateway: gateway, gateway_group: gateway_group, opentelemetry_ctx: OpenTelemetry.Ctx.new(), - opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test") + opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), + turn_salt: "test_salt" }) |> subscribe_and_join(API.Gateway.Channel, "gateway") @@ -393,7 +395,8 @@ defmodule API.Gateway.ChannelTest do gateway: gateway, gateway_group: gateway_group, opentelemetry_ctx: OpenTelemetry.Ctx.new(), - opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test") + opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), + turn_salt: "test_salt" }) |> subscribe_and_join(API.Gateway.Channel, "gateway") diff --git a/elixir/apps/domain/lib/domain/relays.ex b/elixir/apps/domain/lib/domain/relays.ex index 27b1c3cda..3d83c1258 100644 --- a/elixir/apps/domain/lib/domain/relays.ex +++ b/elixir/apps/domain/lib/domain/relays.ex @@ -270,10 +270,11 @@ defmodule Domain.Relays do {:ok, relays} end - def generate_username_and_password(%Relay{stamp_secret: stamp_secret}, %DateTime{} = expires_at) + # TODO: Relays + # Revisit credential lifetime when https://github.com/firezone/firezone/issues/8222 is implemented + def generate_username_and_password(%Relay{stamp_secret: stamp_secret}, salt, expires_at) when is_binary(stamp_secret) do expires_at = DateTime.to_unix(expires_at, :second) - salt = Domain.Crypto.random_token() password = generate_hash(expires_at, stamp_secret, salt) %{username: "#{expires_at}:#{salt}", password: password, expires_at: expires_at} end diff --git a/elixir/apps/domain/test/domain/events/hooks/actor_group_memberships_test.exs b/elixir/apps/domain/test/domain/events/hooks/actor_group_memberships_test.exs index 1ed5b81dc..8c639112f 100644 --- a/elixir/apps/domain/test/domain/events/hooks/actor_group_memberships_test.exs +++ b/elixir/apps/domain/test/domain/events/hooks/actor_group_memberships_test.exs @@ -60,7 +60,8 @@ defmodule Domain.Events.Hooks.ActorGroupMembershipsTest do opentelemetry_ctx: OpenTelemetry.Ctx.new(), opentelemetry_span_ctx: OpenTelemetry.Tracer.start_span("test"), client: client, - subject: subject + subject: subject, + turn_salt: "test_salt" }) |> subscribe_and_join(API.Client.Channel, "client") diff --git a/elixir/apps/domain/test/domain/relays_test.exs b/elixir/apps/domain/test/domain/relays_test.exs index 4a4887f51..a37d223bb 100644 --- a/elixir/apps/domain/test/domain/relays_test.exs +++ b/elixir/apps/domain/test/domain/relays_test.exs @@ -786,25 +786,23 @@ defmodule Domain.RelaysTest do end end - describe "generate_username_and_password/1" do + describe "generate_username_and_password/3" do test "returns username and password", %{account: account} do relay = Fixtures.Relays.create_relay(account: account) - stamp_secret = Ecto.UUID.generate() + stamp_secret = "test_secret" + turn_salt = "test_salt" relay = %{relay | stamp_secret: stamp_secret} - expires_at = DateTime.utc_now() |> DateTime.add(3, :second) + {:ok, expires_at, _} = DateTime.from_iso8601("2023-10-01T00:00:00Z") assert %{username: username, password: password, expires_at: expires_at_unix} = - generate_username_and_password(relay, expires_at) + generate_username_and_password(relay, turn_salt, expires_at) assert [username_expires_at_unix, username_salt] = String.split(username, ":", parts: 2) assert username_expires_at_unix == to_string(expires_at_unix) + assert username_salt == turn_salt assert DateTime.from_unix!(expires_at_unix) == DateTime.truncate(expires_at, :second) - - expected_hash = - :crypto.hash(:sha256, "#{expires_at_unix}:#{stamp_secret}:#{username_salt}") - |> Base.encode64(padding: false, case: :lower) - - assert password == expected_hash + assert username == "1696118400:test_salt" + assert password == "P0+gMB7RdvcvPv3eYFh1VSJUJh/FoAmOjUOqU8dToD8" end end