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:
Thomas Eizinger
2025-10-30 13:13:22 +11:00
committed by GitHub
parent f872754540
commit 3e9ef4772b
18 changed files with 541 additions and 62 deletions

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

@@ -224,6 +224,7 @@ redundant_clone = "warn"
unwrap_in_result = "warn"
unwrap_used = "warn"
too_many_arguments = "allow" # Don't care.
large_enum_variant = "allow" # Don't care.
[workspace.lints.rustdoc]
private-intra-doc-links = "allow" # We don't publish any of our docs but want to catch dead links.

View File

@@ -7,8 +7,8 @@ pub(crate) use crate::gateway::client_on_gateway::ClientOnGateway;
use crate::gateway::client_on_gateway::TranslateOutboundResult;
use crate::gateway::flow_tracker::FlowTracker;
use crate::messages::gateway::ResourceDescription;
use crate::messages::{Answer, IceCredentials, ResolveRequest, SecretKey};
use crate::messages::gateway::{Client, ResourceDescription, Subject};
use crate::messages::{Answer, IceCredentials, ResolveRequest};
use crate::peer_store::PeerStore;
use crate::{GatewayEvent, IpConfig, p2p_control};
use anyhow::{Context, Result};
@@ -169,7 +169,6 @@ impl GatewayState {
return Ok(None);
};
flow_tracker::inbound_wg::record_client(cid);
flow_tracker::inbound_wg::record_decrypted_packet(&packet);
let peer = self
@@ -177,6 +176,8 @@ impl GatewayState {
.get_mut(&cid)
.with_context(|| format!("No peer for connection {cid}"))?;
flow_tracker::inbound_wg::record_client(cid, peer.client_flow_properties());
if let Some(fz_p2p_control) = packet.as_fz_p2p_control() {
let immediate_response = match fz_p2p_control.event_type() {
p2p_control::ASSIGNED_IPS_EVENT => {
@@ -301,23 +302,21 @@ impl GatewayState {
})
}
#[tracing::instrument(level = "debug", skip_all, fields(%cid))]
#[tracing::instrument(level = "debug", skip_all, fields(cid = %client.id))]
pub fn authorize_flow(
&mut self,
cid: ClientId,
client_key: PublicKey,
preshared_key: SecretKey,
client: Client,
subject: Subject,
client_ice: IceCredentials,
gateway_ice: IceCredentials,
client_tun: IpConfig,
expires_at: Option<DateTime<Utc>>,
resource: ResourceDescription,
now: Instant,
) -> Result<(), NoTurnServers> {
self.node.upsert_connection(
cid,
client_key,
x25519::StaticSecret::from(preshared_key.expose_secret().0),
client.id,
client.public_key.into(),
x25519::StaticSecret::from(client.preshared_key.expose_secret().0),
Credentials {
username: gateway_ice.username,
password: gateway_ice.password,
@@ -329,7 +328,29 @@ impl GatewayState {
now,
)?;
let result = self.allow_access(cid, client_tun, expires_at, resource, None);
let result = self.allow_access(
client.id,
IpConfig {
v4: client.ipv4,
v6: client.ipv6,
},
flow_tracker::ClientProperties {
version: client.version,
device_os_name: client.device_os_name,
device_os_version: client.device_os_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,
identity_id: subject.identity_id,
identity_name: subject.identity_name,
actor_id: subject.actor_id,
actor_email: subject.actor_email,
},
expires_at,
resource,
None,
);
debug_assert!(
result.is_ok(),
"`allow_access` should never fail without a `DnsResourceEntry`"
@@ -342,6 +363,7 @@ impl GatewayState {
&mut self,
client: ClientId,
client_tun: IpConfig,
client_props: flow_tracker::ClientProperties,
expires_at: Option<DateTime<Utc>>,
resource: ResourceDescription,
dns_resource_nat: Option<DnsResourceNatEntry>,
@@ -351,7 +373,7 @@ impl GatewayState {
let peer = self
.peers
.entry(client)
.or_insert_with(|| ClientOnGateway::new(client, client_tun, gateway_tun));
.or_insert_with(|| ClientOnGateway::new(client, client_tun, gateway_tun, client_props));
peer.add_resource(resource.clone(), expires_at);
@@ -465,7 +487,21 @@ impl GatewayState {
tracing::trace!(
target: "flow_logs::tcp",
client_id = %flow.client,
client_id = %flow.client_id,
client_version = flow.client_version.map(tracing::field::display),
device_os_name = flow.device_os_name.map(tracing::field::display),
device_os_version = flow.device_os_version.map(tracing::field::display),
device_serial = flow.device_serial.map(tracing::field::display),
device_uuid = flow.device_uuid.map(tracing::field::display),
device_identifier_for_vendor = flow.device_identifier_for_vendor.map(tracing::field::display),
device_firebase_installation_id = flow.device_firebase_installation_id.map(tracing::field::display),
identity_id = flow.identity_id.map(tracing::field::display),
identity_name = flow.identity_name.map(tracing::field::display),
actor_id = flow.actor_id.map(tracing::field::display),
actor_email = flow.actor_email.map(tracing::field::display),
resource_id = %flow.resource_id,
resource_name = %flow.resource_name,
resource_address = %flow.resource_address,
@@ -495,7 +531,21 @@ impl GatewayState {
tracing::trace!(
target: "flow_logs::udp",
client_id = %flow.client,
client_id = %flow.client_id,
client_version = flow.client_version.map(tracing::field::display),
device_os_name = flow.device_os_name.map(tracing::field::display),
device_os_version = flow.device_os_version.map(tracing::field::display),
device_serial = flow.device_serial.map(tracing::field::display),
device_uuid = flow.device_uuid.map(tracing::field::display),
device_identifier_for_vendor = flow.device_identifier_for_vendor.map(tracing::field::display),
device_firebase_installation_id = flow.device_firebase_installation_id.map(tracing::field::display),
identity_id = flow.identity_id.map(tracing::field::display),
identity_name = flow.identity_name.map(tracing::field::display),
actor_id = flow.actor_id.map(tracing::field::display),
actor_email = flow.actor_email.map(tracing::field::display),
resource_id = %flow.resource_id,
resource_name = %flow.resource_name,
resource_address = %flow.resource_address,

View File

@@ -28,6 +28,8 @@ pub struct ClientOnGateway {
client_tun: IpConfig,
gateway_tun: IpConfig,
flow_properties: flow_tracker::ClientProperties,
resources: BTreeMap<ResourceId, ResourceOnGateway>,
/// Caches the existence of internet resource
internet_resource_enabled: Option<ResourceId>,
@@ -51,11 +53,13 @@ impl ClientOnGateway {
id: ClientId,
client_tun: IpConfig,
gateway_tun: IpConfig,
flow_properties: flow_tracker::ClientProperties,
) -> ClientOnGateway {
ClientOnGateway {
id,
client_tun,
gateway_tun,
flow_properties,
resources: BTreeMap::new(),
filters: IpNetworkTable::new(),
permanent_translations: Default::default(),
@@ -568,6 +572,10 @@ impl ClientOnGateway {
pub fn id(&self) -> ClientId {
self.id
}
pub fn client_flow_properties(&self) -> flow_tracker::ClientProperties {
self.flow_properties.clone()
}
}
#[derive(Debug)]
@@ -757,7 +765,12 @@ mod tests {
#[test]
fn gateway_filters_expire_individually() {
let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
let mut peer = ClientOnGateway::new(
client_id(),
client_tun(),
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
let now = Utc::now();
let then = now + Duration::from_secs(10);
let after_then = then + Duration::from_secs(10);
@@ -841,7 +854,12 @@ mod tests {
#[test]
fn allows_packets_for_and_from_gateway_tun_ip() {
let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
let mut peer = ClientOnGateway::new(
client_id(),
client_tun(),
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
let request = ip_packet::make::tcp_packet(
client_tun_ipv4(),
@@ -876,7 +894,12 @@ mod tests {
#[test]
fn dns_and_cidr_filters_dot_mix() {
let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
let mut peer = ClientOnGateway::new(
client_id(),
client_tun(),
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
peer.add_resource(foo_dns_resource(), None);
peer.add_resource(bar_cidr_resource(), None);
peer.setup_nat(
@@ -942,7 +965,12 @@ mod tests {
#[test]
fn internet_resource_doesnt_allow_all_traffic_for_dns_resources() {
let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
let mut peer = ClientOnGateway::new(
client_id(),
client_tun(),
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
peer.add_resource(foo_dns_resource(), None);
peer.add_resource(internet_resource(), None);
peer.setup_nat(
@@ -994,7 +1022,12 @@ mod tests {
fn dns_resource_packet_is_dropped_after_nat_session_expires() {
let _guard = firezone_logging::test("trace");
let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
let mut peer = ClientOnGateway::new(
client_id(),
client_tun(),
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
peer.add_resource(foo_dns_resource(), None);
peer.setup_nat(
foo_name().parse().unwrap(),
@@ -1061,7 +1094,12 @@ mod tests {
let now = Instant::now();
let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
let mut peer = ClientOnGateway::new(
client_id(),
client_tun(),
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
peer.add_resource(foo_dns_resource(), None);
peer.setup_nat(
foo_name().parse().unwrap(),
@@ -1116,7 +1154,12 @@ mod tests {
let now = Instant::now();
let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
let mut peer = ClientOnGateway::new(
client_id(),
client_tun(),
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
peer.add_resource(foo_dns_resource(), None);
peer.add_resource(baz_dns_resource(), None);
peer.setup_nat(
@@ -1345,6 +1388,7 @@ mod proptests {
v6: client_v6,
},
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
for (resource, _, _) in &resources {
peer.add_resource(resource.clone(), None);
@@ -1403,6 +1447,7 @@ mod proptests {
v6: client_v6,
},
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
for ((filters, _), resource_id) in std::iter::zip(&protocol_config, resources_ids) {
@@ -1466,6 +1511,7 @@ mod proptests {
v6: client_v6,
},
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
let packet = match protocol {
Protocol::Tcp { dport } => {
@@ -1523,6 +1569,7 @@ mod proptests {
v6: client_v6,
},
gateway_tun(),
flow_tracker::ClientProperties::default(),
);
let packet_allowed = match protocol_allowed {

View File

@@ -33,6 +33,22 @@ pub struct FlowTracker {
created_at_utc: DateTime<Utc>,
}
/// Additional properties we track for a client.
#[derive(Debug, Clone, Default)]
pub struct ClientProperties {
pub version: Option<String>,
pub device_serial: Option<String>,
pub device_uuid: Option<String>,
pub device_os_name: Option<String>,
pub device_os_version: Option<String>,
pub identifier_for_vendor: Option<String>,
pub firebase_installation_id: Option<String>,
pub identity_id: Option<String>,
pub identity_name: Option<String>,
pub actor_id: Option<String>,
pub actor_email: Option<String>,
}
impl FlowTracker {
pub fn new(now: Instant) -> Self {
Self {
@@ -182,7 +198,7 @@ impl FlowTracker {
match (src_proto, dst_proto) {
(Protocol::Tcp(src_port), Protocol::Tcp(dst_port)) => {
let key = TcpFlowKey {
client,
client: client.id,
resource: resource.id,
src_ip,
dst_ip,
@@ -209,6 +225,17 @@ impl FlowTracker {
domain,
resource_name: resource.name,
resource_address: resource.address,
client_version: client.version,
device_os_name: client.device_os_name,
device_os_version: client.device_os_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,
actor_id: client.actor_id,
actor_email: client.actor_email,
identity_id: client.identity_id,
identity_name: client.identity_name,
});
}
hash_map::Entry::Occupied(occupied) if occupied.get().context != context => {
@@ -237,6 +264,17 @@ impl FlowTracker {
domain,
resource_name: resource.name,
resource_address: resource.address,
client_version: client.version,
device_os_name: client.device_os_name,
device_os_version: client.device_os_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,
actor_id: client.actor_id,
actor_email: client.actor_email,
identity_id: client.identity_id,
identity_name: client.identity_name,
},
);
}
@@ -261,6 +299,17 @@ impl FlowTracker {
domain,
resource_name: resource.name,
resource_address: resource.address,
client_version: client.version,
device_os_name: client.device_os_name,
device_os_version: client.device_os_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,
actor_id: client.actor_id,
actor_email: client.actor_email,
identity_id: client.identity_id,
identity_name: client.identity_name,
},
);
}
@@ -286,7 +335,7 @@ impl FlowTracker {
}
(Protocol::Udp(src_port), Protocol::Udp(dst_port)) => {
let key = UdpFlowKey {
client,
client: client.id,
resource: resource.id,
src_ip,
dst_ip,
@@ -306,6 +355,17 @@ impl FlowTracker {
domain,
resource_name: resource.name,
resource_address: resource.address,
client_version: client.version,
device_os_name: client.device_os_name,
device_os_version: client.device_os_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,
actor_id: client.actor_id,
actor_email: client.actor_email,
identity_id: client.identity_id,
identity_name: client.identity_name,
});
}
hash_map::Entry::Occupied(occupied) if occupied.get().context != context => {
@@ -332,6 +392,17 @@ impl FlowTracker {
domain,
resource_name: value.resource_name,
resource_address: value.resource_address,
client_version: client.version,
device_os_name: client.device_os_name,
device_os_version: client.device_os_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,
actor_id: client.actor_id,
actor_email: client.actor_email,
identity_id: client.identity_id,
identity_name: client.identity_name,
},
);
}
@@ -457,7 +528,20 @@ pub enum CompletedFlow {
#[derive(Debug)]
pub struct CompletedTcpFlow {
pub client: ClientId,
pub client_id: ClientId,
pub client_version: Option<String>,
pub device_os_name: Option<String>,
pub device_os_version: Option<String>,
pub device_serial: Option<String>,
pub device_uuid: Option<String>,
pub device_identifier_for_vendor: Option<String>,
pub device_firebase_installation_id: Option<String>,
pub identity_id: Option<String>,
pub identity_name: Option<String>,
pub actor_id: Option<String>,
pub actor_email: Option<String>,
pub resource_id: ResourceId,
pub resource_name: String,
@@ -486,7 +570,20 @@ pub struct CompletedTcpFlow {
#[derive(Debug)]
pub struct CompletedUdpFlow {
pub client: ClientId,
pub client_id: ClientId,
pub client_version: Option<String>,
pub device_os_name: Option<String>,
pub device_os_version: Option<String>,
pub device_serial: Option<String>,
pub device_uuid: Option<String>,
pub device_identifier_for_vendor: Option<String>,
pub device_firebase_installation_id: Option<String>,
pub identity_id: Option<String>,
pub identity_name: Option<String>,
pub actor_id: Option<String>,
pub actor_email: Option<String>,
pub resource_id: ResourceId,
pub resource_name: String,
@@ -516,7 +613,18 @@ pub struct CompletedUdpFlow {
impl CompletedTcpFlow {
fn new(key: TcpFlowKey, value: TcpFlowValue, end: DateTime<Utc>) -> Self {
Self {
client: key.client,
client_id: key.client,
client_version: value.client_version,
device_os_name: value.device_os_name,
device_os_version: value.device_os_version,
device_serial: value.device_serial,
device_uuid: value.device_uuid,
device_identifier_for_vendor: value.identifier_for_vendor,
device_firebase_installation_id: value.firebase_installation_id,
actor_id: value.actor_id,
actor_email: value.actor_email,
identity_id: value.identity_id,
identity_name: value.identity_name,
resource_id: key.resource,
resource_name: value.resource_name,
resource_address: value.resource_address,
@@ -543,7 +651,18 @@ impl CompletedTcpFlow {
impl CompletedUdpFlow {
fn new(key: UdpFlowKey, value: UdpFlowValue, end: DateTime<Utc>) -> Self {
Self {
client: key.client,
client_id: key.client,
client_version: value.client_version,
device_os_name: value.device_os_name,
device_os_version: value.device_os_version,
device_serial: value.device_serial,
device_uuid: value.device_uuid,
device_identifier_for_vendor: value.identifier_for_vendor,
device_firebase_installation_id: value.firebase_installation_id,
actor_id: value.actor_id,
actor_email: value.actor_email,
identity_id: value.identity_id,
identity_name: value.identity_name,
resource_id: key.resource,
resource_name: value.resource_name,
resource_address: value.resource_address,
@@ -599,6 +718,19 @@ struct TcpFlowValue {
resource_name: String,
resource_address: String,
client_version: Option<String>,
device_serial: Option<String>,
device_uuid: Option<String>,
device_os_name: Option<String>,
device_os_version: Option<String>,
identifier_for_vendor: Option<String>,
firebase_installation_id: Option<String>,
identity_id: Option<String>,
identity_name: Option<String>,
actor_id: Option<String>,
actor_email: Option<String>,
fin_tx: bool,
fin_rx: bool,
}
@@ -614,6 +746,19 @@ struct UdpFlowValue {
resource_name: String,
resource_address: String,
client_version: Option<String>,
device_serial: Option<String>,
device_uuid: Option<String>,
device_os_name: Option<String>,
device_os_version: Option<String>,
identifier_for_vendor: Option<String>,
firebase_installation_id: Option<String>,
identity_id: Option<String>,
identity_name: Option<String>,
actor_id: Option<String>,
actor_email: Option<String>,
}
#[derive(Debug, Default, Clone, Copy)]
@@ -708,8 +853,23 @@ pub mod inbound_wg {
use super::*;
pub fn record_client(cid: ClientId) {
update_current_flow_inbound_wireguard(|wg| wg.client.replace(cid));
pub fn record_client(cid: ClientId, props: ClientProperties) {
update_current_flow_inbound_wireguard(|wg| {
wg.client.replace(Client {
id: cid,
version: props.version,
device_os_name: props.device_os_name,
device_os_version: props.device_os_version,
device_serial: props.device_serial,
actor_id: props.actor_id,
actor_email: props.actor_email,
identity_id: props.identity_id,
identity_name: props.identity_name,
device_uuid: props.device_uuid,
identifier_for_vendor: props.identifier_for_vendor,
firebase_installation_id: props.firebase_installation_id,
})
});
}
pub fn record_resource(id: ResourceId, name: String, address: String) {
@@ -812,7 +972,7 @@ enum FlowData {
struct InboundWireGuard {
outer: OuterFlow<SocketAddr>,
inner: Option<InnerFlow>,
client: Option<ClientId>,
client: Option<Client>,
resource: Option<Resource>,
/// The domain name in case this packet is for a DNS resource.
domain: Option<DomainName>,
@@ -847,6 +1007,26 @@ struct InnerFlow {
payload_len: usize,
}
#[derive(Debug)]
struct Client {
id: ClientId,
version: Option<String>,
device_serial: Option<String>,
device_uuid: Option<String>,
device_os_name: Option<String>,
device_os_version: Option<String>,
identifier_for_vendor: Option<String>,
firebase_installation_id: Option<String>,
identity_id: Option<String>,
identity_name: Option<String>,
actor_id: Option<String>,
actor_email: Option<String>,
}
#[derive(Debug)]
struct Resource {
id: ResourceId,

View File

@@ -210,6 +210,32 @@ pub struct Client {
pub preshared_key: SecretKey,
pub ipv4: Ipv4Addr,
pub ipv6: Ipv6Addr,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub device_os_name: Option<String>,
#[serde(default)]
pub device_os_version: Option<String>,
#[serde(default)]
pub device_serial: Option<String>,
#[serde(default)]
pub device_uuid: Option<String>,
#[serde(default)]
pub identifier_for_vendor: Option<String>,
#[serde(default)]
pub firebase_installation_id: Option<String>,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct Subject {
#[serde(default)]
pub identity_id: Option<String>,
#[serde(default)]
pub identity_name: Option<String>,
#[serde(default)]
pub actor_id: Option<String>,
#[serde(default)]
pub actor_email: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
@@ -220,6 +246,8 @@ pub struct AuthorizeFlow {
pub resource: ResourceDescription,
pub gateway_ice_credentials: IceCredentials,
pub client: Client,
#[serde(default)]
pub subject: Subject,
pub client_ice_credentials: IceCredentials,
#[serde(with = "ts_seconds_option")]

View File

@@ -53,6 +53,12 @@ impl From<PublicKey> for Key {
}
}
impl From<Key> for PublicKey {
fn from(value: Key) -> Self {
value.0.into()
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Base64Display::new(&self.0, &STANDARD))

View File

@@ -10,6 +10,7 @@ use super::stub_portal::StubPortal;
use super::transition::{Destination, DnsQuery};
use crate::client;
use crate::dns::is_subdomain;
use crate::messages::gateway::{Client, Subject};
use crate::messages::{IceCredentials, Key, SecretKey};
use crate::tests::assertions::*;
use crate::tests::flux_capacitor::FluxCapacitor;
@@ -823,12 +824,28 @@ impl TunnelTest {
gateway
.exec_mut(|g| {
g.sut.authorize_flow(
src,
client_key,
preshared_key.clone(),
Client {
id: src,
public_key: client_key.into(),
preshared_key: preshared_key.clone(),
ipv4: self.client.inner().sut.tunnel_ip_config().unwrap().v4,
ipv6: self.client.inner().sut.tunnel_ip_config().unwrap().v6,
device_os_name: None,
device_serial: None,
device_uuid: None,
identifier_for_vendor: None,
firebase_installation_id: None,
version: None,
device_os_version: None,
},
Subject {
identity_name: None,
actor_email: None,
identity_id: None,
actor_id: None,
},
client_ice.clone(),
gateway_ice.clone(),
self.client.inner().sut.tunnel_ip_config().unwrap(),
None,
resource,
now,

View File

@@ -352,15 +352,10 @@ impl Eventloop {
match msg {
IngressMessages::AuthorizeFlow(msg) => {
if let Err(snownet::NoTurnServers {}) = tunnel.state_mut().authorize_flow(
msg.client.id,
PublicKey::from(msg.client.public_key.0),
msg.client.preshared_key,
msg.client,
msg.subject,
msg.client_ice_credentials,
msg.gateway_ice_credentials,
IpConfig {
v4: msg.client.ipv4,
v6: msg.client.ipv6,
},
msg.expires_at,
msg.resource,
Instant::now(),
@@ -593,6 +588,7 @@ impl Eventloop {
v4: req.client.peer.ipv4,
v6: req.client.peer.ipv6,
},
Default::default(), // Additional client properties are not supported for 1.3.x Clients and will just be empty.
req.expires_at,
req.resource,
req.client
@@ -648,6 +644,7 @@ impl Eventloop {
v4: req.client_ipv4,
v6: req.client_ipv6,
},
Default::default(), // Additional client properties are not supported for 1.3.x Clients and will just be empty.
req.expires_at,
req.resource,
req.payload.map(|r| DnsResourceNatEntry::new(r, addresses)),