Fix agent IP detection to use real client IP from proxy headers

The agent's last_seen IP was showing the Docker container IP (10.244.x.x)
because conn.remote_ip returns the proxy/load balancer IP.

Changes:
- Added get_client_ip/1 helper that checks X-Forwarded-For first
- Falls back to X-Real-IP if X-Forwarded-For not present
- Falls back to conn.remote_ip as last resort
- Applied to both AgentAuth plug and AgentController heartbeat endpoint

Now correctly shows the actual external IP of the remote agent.
This commit is contained in:
Graham McIntire 2026-01-14 17:04:59 -06:00
parent 0ed82c47b9
commit 31b906b0fa
No known key found for this signature in database
2 changed files with 52 additions and 2 deletions

View file

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

View file

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