Files
firezone/elixir/apps/api/lib/api/gateway/socket.ex
Jamil 1e577d31b9 fix(portal): use reproducible relay creds (#9857)
When giving TURN credentials to clients and gateways, it's important
that they remain consistent across hiccups in the portal connection so
that relayed connections are not interrupted during a deploy, or if the
user's internet is flaky, or the GCP load balancer decides to disconnect
the client/gateway.

Prior to this PR, that was not the case because we essentially tied TURN
credentials, required for data plane packet flows, to the WebSocket
connection, a control plane element. This happened because we generated
random `expires_at` and `salt` elements on _each_ connection to the
portal.

Instead, what we do now is make these reproducible and tied to the auth
token by hashing then base64-encoding it. The expiry is tied to the
auth-token's expiry.


Fixes #9856
2025-07-14 17:42:11 +00:00

65 lines
2.0 KiB
Elixir

defmodule API.Gateway.Socket do
use Phoenix.Socket
alias Domain.{Tokens, Gateways}
require Logger
require OpenTelemetry.Tracer
## Channels
channel "gateway", API.Gateway.Channel
## Authentication
@impl true
def connect(%{"token" => encoded_token} = attrs, socket, connect_info) do
:otel_propagator_text_map.extract(connect_info.trace_context_headers)
OpenTelemetry.Tracer.with_span "gateway.connect" do
context = API.Sockets.auth_context(connect_info, :gateway_group)
attrs = Map.take(attrs, ~w[external_id name public_key])
with {:ok, group, token} <- Gateways.authenticate(encoded_token, context),
{:ok, gateway} <- Gateways.upsert_gateway(group, token, attrs, context) do
OpenTelemetry.Tracer.set_attributes(%{
token_id: token.id,
gateway_id: gateway.id,
account_id: gateway.account_id,
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)
|> assign(:opentelemetry_span_ctx, OpenTelemetry.Tracer.current_span_ctx())
|> assign(:opentelemetry_ctx, OpenTelemetry.Ctx.get_current())
{:ok, socket}
else
{:error, :unauthorized} ->
OpenTelemetry.Tracer.set_status(:error, "invalid_token")
{:error, :invalid_token}
{:error, reason} ->
OpenTelemetry.Tracer.set_status(:error, inspect(reason))
Logger.debug("Error connecting gateway websocket: #{inspect(reason)}")
{:error, reason}
end
end
end
def connect(_params, _socket, _connect_info) do
{:error, :missing_token}
end
@impl true
def id(socket), do: Tokens.socket_id(socket.assigns.token_id)
end