defmodule AprsmeWeb.MobileUserSocket do @moduledoc """ Socket for mobile applications (iOS/Android) to receive real-time APRS packets. Connections are anonymous but rate-limited per peer IP to prevent a single client from opening unbounded connections and exhausting server resources. """ use Phoenix.Socket require Logger # Channels channel "mobile:packets", AprsmeWeb.MobileChannel # 30 new connections per IP per minute — well above normal reconnect churn # but tight enough to stop a script from floodting the socket layer. @connect_limit 30 @connect_scale 60_000 @impl true def connect(_params, socket, connect_info) do ip = peer_ip(connect_info) if test_env?() or allow_connect?(ip) do {:ok, assign(socket, :peer_ip, ip)} else Logger.warning("Mobile socket connect rate-limited for ip=#{ip}") :error end end defp allow_connect?(ip) do case Aprsme.RateLimiter.hit("mobile_socket_connect:#{ip}", @connect_scale, @connect_limit) do {:allow, _count} -> true {:deny, _retry_after} -> false end end defp test_env?, do: Application.get_env(:aprsme, :env) == :test @impl true def id(_socket), do: nil defp peer_ip(%{x_headers: headers}) when is_list(headers) do headers |> Enum.find_value(fn {"cf-connecting-ip", v} -> v {"x-forwarded-for", v} -> v |> String.split(",") |> List.first() |> String.trim() {"x-real-ip", v} -> v _ -> nil end) |> case do nil -> "unknown" ip -> ip end end defp peer_ip(%{peer_data: %{address: address}}) when not is_nil(address) do address |> :inet.ntoa() |> to_string() end defp peer_ip(_), do: "unknown" end