diff --git a/lib/towerops_web/controllers/api/agent_controller.ex b/lib/towerops_web/controllers/api/agent_controller.ex index f220acdd..75a76fa0 100644 --- a/lib/towerops_web/controllers/api/agent_controller.ex +++ b/lib/towerops_web/controllers/api/agent_controller.ex @@ -91,7 +91,7 @@ defmodule ToweropsWeb.Api.AgentController do """ def heartbeat(conn, params) do agent_token = conn.assigns.current_agent_token - ip = to_string(:inet_parse.ntoa(conn.remote_ip)) + ip = get_client_ip(conn) # Parse metadata based on content type metadata = @@ -320,4 +320,29 @@ defmodule ToweropsWeb.Api.AgentController do defp convert_metadata_to_proto(metadata) when is_map(metadata) do Map.new(metadata, fn {k, v} -> {to_string(k), to_string(v)} end) end + + # Get the real client IP, checking forwarded headers first + defp get_client_ip(conn) do + # Check X-Forwarded-For header first (set by load balancers/proxies) + case get_req_header(conn, "x-forwarded-for") do + [forwarded | _] -> + # X-Forwarded-For can have multiple IPs (client, proxy1, proxy2...) + # Take the first one (original client) + forwarded + |> String.split(",") + |> List.first() + |> String.trim() + + [] -> + # Fall back to X-Real-IP + case get_req_header(conn, "x-real-ip") do + [real_ip | _] -> + String.trim(real_ip) + + [] -> + # Fall back to conn.remote_ip + to_string(:inet_parse.ntoa(conn.remote_ip)) + end + end + end end diff --git a/lib/towerops_web/plugs/agent_auth.ex b/lib/towerops_web/plugs/agent_auth.ex index e0657144..3e952642 100644 --- a/lib/towerops_web/plugs/agent_auth.ex +++ b/lib/towerops_web/plugs/agent_auth.ex @@ -42,7 +42,7 @@ defmodule ToweropsWeb.Plugs.AgentAuth do end defp update_heartbeat(conn, agent_token) do - ip = to_string(:inet_parse.ntoa(conn.remote_ip)) + ip = get_client_ip(conn) # In test environment, update synchronously to avoid DB ownership issues # In production, update asynchronously for better performance @@ -55,6 +55,31 @@ defmodule ToweropsWeb.Plugs.AgentAuth do conn end + # Get the real client IP, checking forwarded headers first + defp get_client_ip(conn) do + # Check X-Forwarded-For header first (set by load balancers/proxies) + case get_req_header(conn, "x-forwarded-for") do + [forwarded | _] -> + # X-Forwarded-For can have multiple IPs (client, proxy1, proxy2...) + # Take the first one (original client) + forwarded + |> String.split(",") + |> List.first() + |> String.trim() + + [] -> + # Fall back to X-Real-IP + case get_req_header(conn, "x-real-ip") do + [real_ip | _] -> + String.trim(real_ip) + + [] -> + # Fall back to conn.remote_ip + to_string(:inet_parse.ntoa(conn.remote_ip)) + end + end + end + defp update_heartbeat_async(agent_token_id, ip) do parent = self()