mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(gateway): extend flow logs with more client properties (#10717)
In order to make the flow logs emitted by the Gateway more useful and self-contained, we extend the `authorize_flow` message sent to the Gateway with some more context around the Client and Actor of that flow. In particular, we now also send the following to the Gateway: - `client_version` - `device_os_version` - `device_os_name` - `device_serial` - `device_uuid` - `device_identifier_for_vendor` - `device_firebase_installation_id` - `identity_id` - `identity_name` - `actor_id` - `actor_email` We only extend the `authorize_flow` message with these additional properties. The legacy messages for 1.3.x Clients remain as is. For those Clients, the above properties will be empty in the flow logs. Resolves: #10690 --------- Signed-off-by: Thomas Eizinger <thomas@eizinger.io> Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
@@ -377,7 +377,8 @@ defmodule API.Client.Channel do
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: socket.assigns.subject
|
||||
}}
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule API.Gateway.Channel do
|
||||
Changes.Change,
|
||||
Flows,
|
||||
Gateways,
|
||||
Auth,
|
||||
PubSub,
|
||||
Relays,
|
||||
Resources
|
||||
@@ -226,7 +227,8 @@ defmodule API.Gateway.Channel do
|
||||
flow_id: flow_id,
|
||||
authorization_expires_at: authorization_expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: %Auth.Subject{} = subject
|
||||
} = payload
|
||||
|
||||
ref =
|
||||
@@ -244,7 +246,8 @@ defmodule API.Gateway.Channel do
|
||||
gateway_ice_credentials: ice_credentials.gateway,
|
||||
client: Views.Client.render(client, preshared_key),
|
||||
client_ice_credentials: ice_credentials.client,
|
||||
expires_at: DateTime.to_unix(authorization_expires_at, :second)
|
||||
expires_at: DateTime.to_unix(authorization_expires_at, :second),
|
||||
subject: Views.Subject.render(subject)
|
||||
})
|
||||
|
||||
cache =
|
||||
|
||||
@@ -2,12 +2,32 @@ defmodule API.Gateway.Views.Client do
|
||||
alias Domain.Clients
|
||||
|
||||
def render(%Clients.Client{} = client, preshared_key) do
|
||||
# The OS name can have spaces, hence split the user-agent step by step.
|
||||
[os_name, rest] = String.split(client.last_seen_user_agent, "/", parts: 2)
|
||||
[os_version, rest] = String.split(rest, " ", parts: 2)
|
||||
[_, rest] = String.split(rest, "/", parts: 2)
|
||||
|
||||
# TODO: For easier testing, we re-parse the client version here.
|
||||
# Long term, we should not be parsing the user-agent at all in here.
|
||||
# Instead we should directly store the parsed information in the DB.
|
||||
[client_version | _] = String.split(rest, " ", parts: 2)
|
||||
|
||||
# Note: We purposely omit the client_type as that will say `connlib` for older clients
|
||||
# (we've only recently changed this to `apple-client` etc).
|
||||
|
||||
%{
|
||||
id: client.id,
|
||||
public_key: client.public_key,
|
||||
preshared_key: preshared_key,
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6
|
||||
ipv6: client.ipv6,
|
||||
version: client_version,
|
||||
device_serial: client.device_serial,
|
||||
device_os_name: os_name,
|
||||
device_os_version: os_version,
|
||||
device_uuid: client.device_uuid,
|
||||
firebase_installation_id: client.firebase_installation_id,
|
||||
identifier_for_vendor: client.identifier_for_vendor
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
12
elixir/apps/api/lib/api/gateway/views/subject.ex
Normal file
12
elixir/apps/api/lib/api/gateway/views/subject.ex
Normal file
@@ -0,0 +1,12 @@
|
||||
defmodule API.Gateway.Views.Subject do
|
||||
alias Domain.Auth
|
||||
|
||||
def render(%Auth.Subject{} = subject) do
|
||||
%{
|
||||
identity_id: get_in(subject, [Access.key(:identity), Access.key(:id)]),
|
||||
identity_name: subject.actor.name,
|
||||
actor_id: subject.actor.id,
|
||||
actor_email: get_in(subject, [Access.key(:identity), Access.key(:email)])
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -2823,7 +2823,7 @@ defmodule API.Client.ChannelTest do
|
||||
account: account,
|
||||
group: internet_gateway_group,
|
||||
context: %{
|
||||
user_agent: "iOS/12.5 (iPhone) connlib/1.2.0"
|
||||
user_agent: "iOS/12.5 connlib/1.2.0"
|
||||
}
|
||||
)
|
||||
|> Repo.preload(:group)
|
||||
@@ -2841,7 +2841,7 @@ defmodule API.Client.ChannelTest do
|
||||
account: account,
|
||||
group: internet_gateway_group,
|
||||
context: %{
|
||||
user_agent: "iOS/12.5 (iPhone) connlib/1.3.0"
|
||||
user_agent: "iOS/12.5 connlib/1.3.0"
|
||||
}
|
||||
)
|
||||
|> Repo.preload(:group)
|
||||
|
||||
@@ -10,7 +10,7 @@ defmodule API.Client.SocketTest do
|
||||
]
|
||||
|
||||
@connect_info %{
|
||||
user_agent: "iOS/12.7 (iPhone) connlib/1.3.0",
|
||||
user_agent: "iOS/12.7 connlib/1.3.0",
|
||||
peer_data: %{address: {189, 172, 73, 001}},
|
||||
x_headers:
|
||||
[
|
||||
|
||||
@@ -117,7 +117,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
client: client,
|
||||
resource: resource,
|
||||
socket: socket,
|
||||
gateway: gateway
|
||||
gateway: gateway,
|
||||
subject: subject
|
||||
} do
|
||||
expired_flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
@@ -146,7 +147,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
flow_id: expired_flow.id,
|
||||
authorization_expires_at: expired_expiration,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: subject
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -177,7 +179,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
client: client,
|
||||
resource: resource,
|
||||
socket: socket,
|
||||
gateway: gateway
|
||||
gateway: gateway,
|
||||
subject: subject
|
||||
} do
|
||||
expired_flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
@@ -216,7 +219,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
flow_id: expired_flow.id,
|
||||
authorization_expires_at: expired_expiration,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: subject
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -231,7 +235,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
flow_id: unexpired_flow.id,
|
||||
authorization_expires_at: unexpired_expiration,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: subject
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -1488,7 +1493,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
account: account,
|
||||
gateway: gateway,
|
||||
resource: resource,
|
||||
socket: socket
|
||||
socket: socket,
|
||||
subject: subject
|
||||
} do
|
||||
flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
@@ -1516,7 +1522,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: subject
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -1542,7 +1549,22 @@ defmodule API.Gateway.ChannelTest do
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6,
|
||||
preshared_key: preshared_key,
|
||||
public_key: client.public_key
|
||||
public_key: client.public_key,
|
||||
version: client.last_seen_version,
|
||||
device_serial: client.device_serial,
|
||||
device_uuid: client.device_uuid,
|
||||
identifier_for_vendor: client.identifier_for_vendor,
|
||||
firebase_installation_id: client.firebase_installation_id,
|
||||
# Hardcode these to avoid having to reparse the user agent.
|
||||
device_os_name: "iOS",
|
||||
device_os_version: "12.5"
|
||||
}
|
||||
|
||||
assert payload.subject == %{
|
||||
identity_id: subject.identity.id,
|
||||
identity_name: subject.actor.name,
|
||||
actor_id: subject.actor.id,
|
||||
actor_email: subject.identity.email
|
||||
}
|
||||
|
||||
assert payload.client_ice_credentials == ice_credentials.client
|
||||
@@ -1588,7 +1610,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: subject
|
||||
}}
|
||||
)
|
||||
|
||||
@@ -1629,7 +1652,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
account: account,
|
||||
resource: resource,
|
||||
gateway: gateway,
|
||||
socket: socket
|
||||
socket: socket,
|
||||
subject: subject
|
||||
} do
|
||||
flow =
|
||||
Fixtures.Flows.create_flow(
|
||||
@@ -1663,7 +1687,8 @@ defmodule API.Gateway.ChannelTest do
|
||||
flow_id: flow.id,
|
||||
authorization_expires_at: expires_at,
|
||||
ice_credentials: ice_credentials,
|
||||
preshared_key: preshared_key
|
||||
preshared_key: preshared_key,
|
||||
subject: subject
|
||||
}}
|
||||
)
|
||||
|
||||
|
||||
92
elixir/apps/api/test/api/gateway/views/client_test.exs
Normal file
92
elixir/apps/api/test/api/gateway/views/client_test.exs
Normal file
@@ -0,0 +1,92 @@
|
||||
# The user-agents in this file have been taken from the production DB.
|
||||
|
||||
defmodule API.Gateway.Views.ClientTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Domain.Clients
|
||||
|
||||
describe "render/2" do
|
||||
test "parses_linux_headless_user_agent" do
|
||||
client = %Clients.Client{
|
||||
last_seen_user_agent: "Ubuntu/22.4.0 headless-client/1.5.4 (x86_64; 6.8.0-1036-azure)"
|
||||
}
|
||||
|
||||
view = API.Gateway.Views.Client.render(client, "")
|
||||
|
||||
assert view.device_os_name == "Ubuntu"
|
||||
assert view.device_os_version == "22.4.0"
|
||||
assert view.version == "1.5.4"
|
||||
end
|
||||
|
||||
test "parses_macos_user_agent" do
|
||||
client = %Clients.Client{
|
||||
last_seen_user_agent: "Mac OS/15.4.1 apple-client/1.5.8 (arm64; 24.4.0)"
|
||||
}
|
||||
|
||||
view = API.Gateway.Views.Client.render(client, "")
|
||||
|
||||
assert view.device_os_name == "Mac OS"
|
||||
assert view.device_os_version == "15.4.1"
|
||||
assert view.version == "1.5.8"
|
||||
end
|
||||
|
||||
test "parses_android_user_agent" do
|
||||
client = %Clients.Client{
|
||||
last_seen_user_agent: "Android/12 android-client/1.5.2 (4.14.180-perf+)"
|
||||
}
|
||||
|
||||
view = API.Gateway.Views.Client.render(client, "")
|
||||
|
||||
assert view.device_os_name == "Android"
|
||||
assert view.device_os_version == "12"
|
||||
assert view.version == "1.5.2"
|
||||
end
|
||||
|
||||
test "parses_windows_gui_user_agent" do
|
||||
client = %Clients.Client{
|
||||
last_seen_user_agent: "Windows/10.0.26200 gui-client/1.5.8 (x86_64)"
|
||||
}
|
||||
|
||||
view = API.Gateway.Views.Client.render(client, "")
|
||||
|
||||
assert view.device_os_name == "Windows"
|
||||
assert view.device_os_version == "10.0.26200"
|
||||
assert view.version == "1.5.8"
|
||||
end
|
||||
|
||||
test "parses_ios_user_agent" do
|
||||
client = %Clients.Client{
|
||||
last_seen_user_agent: "iOS/26.0.1 apple-client/1.5.8 (25.0.0)"
|
||||
}
|
||||
|
||||
view = API.Gateway.Views.Client.render(client, "")
|
||||
|
||||
assert view.device_os_name == "iOS"
|
||||
assert view.device_os_version == "26.0.1"
|
||||
assert view.version == "1.5.8"
|
||||
end
|
||||
|
||||
test "parses_pop_os_user_agent" do
|
||||
client = %Clients.Client{
|
||||
last_seen_user_agent: "Pop!_OS/24.4.0 gui-client/1.5.8 (x86_64; 6.16.3-76061603-generic)"
|
||||
}
|
||||
|
||||
view = API.Gateway.Views.Client.render(client, "")
|
||||
|
||||
assert view.device_os_name == "Pop!_OS"
|
||||
assert view.device_os_version == "24.4.0"
|
||||
assert view.version == "1.5.8"
|
||||
end
|
||||
|
||||
test "parses_user_agent_without_additional_data" do
|
||||
client = %Clients.Client{
|
||||
last_seen_user_agent: "iOS/26.0.1 apple-client/1.5.8"
|
||||
}
|
||||
|
||||
view = API.Gateway.Views.Client.render(client, "")
|
||||
|
||||
assert view.device_os_name == "iOS"
|
||||
assert view.device_os_version == "26.0.1"
|
||||
assert view.version == "1.5.8"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -36,7 +36,7 @@ defmodule Domain.Fixtures.Auth do
|
||||
|
||||
def user_password, do: "Hello w0rld!"
|
||||
def remote_ip, do: {100, 64, 100, 58}
|
||||
def user_agent, do: "iOS/12.5 (iPhone) connlib/1.3.0"
|
||||
def user_agent, do: "iOS/12.5 connlib/1.3.0"
|
||||
def email(domain \\ "example.com"), do: "user-#{unique_integer()}@#{domain}"
|
||||
|
||||
def random_provider_identifier(%Domain.Auth.Provider{adapter: :email, name: name}) do
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Domain.Fixtures.Clients do
|
||||
external_id: Ecto.UUID.generate(),
|
||||
name: "client-#{unique_integer()}",
|
||||
public_key: unique_public_key(),
|
||||
last_seen_user_agent: "iOS/12.7 (iPhone) connlib/1.3.0",
|
||||
last_seen_user_agent: "iOS/18.6.2 apple-client/1.5.8 (24.6.0)",
|
||||
last_seen_remote_ip: Enum.random([unique_ipv4(), unique_ipv6()]),
|
||||
last_seen_remote_ip_location_region: "US",
|
||||
last_seen_remote_ip_location_city: "San Francisco",
|
||||
|
||||
Reference in New Issue
Block a user