Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
80e1654
activity mock UI
filipslezaklab Apr 25, 2025
d25fe5b
add basic skeleton of the event model
wojcik91 Apr 29, 2025
1214e8c
Merge branch 'dev' into add_audit_log_api
wojcik91 Apr 30, 2025
74b350b
add basic handlder skeleton with pagination
wojcik91 Apr 30, 2025
f45ebbe
a bit of cleanup
wojcik91 May 2, 2025
2cd2c05
update query data
wojcik91 May 2, 2025
68e4faa
Merge branch 'dev' into add_audit_log_api
wojcik91 May 2, 2025
5df3b3c
sketch out general event service setup
wojcik91 May 2, 2025
410546e
Merge branch 'dev' into activity-ui
filipslezaklab May 5, 2025
bf8105f
Merge pull request #1124 from DefGuard/add_audit_log_api
filipslezaklab May 5, 2025
2e2c02d
inifnite scroll audit log with next page detection
filipslezaklab May 5, 2025
11a4f9e
Merge branch 'dev' into event_router_poc
wojcik91 May 5, 2025
c902a31
implement a basic loop for the event logger
wojcik91 May 5, 2025
7b4d754
implement base event logging flow
wojcik91 May 5, 2025
c5e147a
add helper method for sending events
wojcik91 May 6, 2025
d66bf9b
remove unnecessary DB pool from mail handler
wojcik91 May 6, 2025
beb22c7
refactor logger message and add helper to log events
wojcik91 May 6, 2025
eeb7d73
add router module documentation
wojcik91 May 6, 2025
e04ac77
split router logic into smaller functions
wojcik91 May 6, 2025
9a20949
milify list count display
filipslezaklab May 6, 2025
acd924b
Merge branch 'dev' into event_router_poc
wojcik91 May 9, 2025
d57e6f6
Merge branch 'activity-ui' into event_router_poc
wojcik91 May 12, 2025
b101080
reorder migrations
wojcik91 May 12, 2025
101eb8a
add filtering by modules
wojcik91 May 14, 2025
0a638fe
implement sorting
wojcik91 May 14, 2025
93dadf7
add username to audit log event
wojcik91 May 14, 2025
a0850d5
add sorting by IP
wojcik91 May 14, 2025
80c4725
linter fixes
wojcik91 May 14, 2025
09c3794
remove details column
wojcik91 May 14, 2025
d6f604e
Merge branch 'dev' into event_router_poc
wojcik91 May 14, 2025
afa9918
add EventType enum
wojcik91 May 14, 2025
280343c
update query data
wojcik91 May 14, 2025
a98cb17
change default sort order
wojcik91 May 15, 2025
20cb54f
add search support
wojcik91 May 15, 2025
85f5dc7
add search support
wojcik91 May 15, 2025
b52b097
store username in audit log table
wojcik91 May 15, 2025
ff35435
update dependencies
wojcik91 May 15, 2025
d31292e
filter events for non-admin users
wojcik91 May 15, 2025
ab4bdf9
Merge branch 'dev' into activity-ui
filipslezaklab May 15, 2025
d1ca2f8
Update defguard-ui
filipslezaklab May 15, 2025
dc1abd3
upgrade web packages
filipslezaklab May 15, 2025
5b23cf3
Merge branch 'activity-ui' into event_router_poc
wojcik91 May 15, 2025
e92f066
fix missing package
filipslezaklab May 15, 2025
877ebb9
fix username query filter
wojcik91 May 15, 2025
671d0da
Merge branch 'activity-ui' into event_router_poc
wojcik91 May 15, 2025
e5427e4
remove FK constraint to avoid deletions
wojcik91 May 16, 2025
e206327
add user id to audit log events
wojcik91 May 16, 2025
1af132e
add device-related logging
wojcik91 May 19, 2025
1ca7375
expect UTC timestamp in API request query param
wojcik91 May 19, 2025
daf8e27
refactor group filters modal to accept string vlaues
filipslezaklab May 19, 2025
15c2ca6
Merge branch 'activity-ui' of github.com:DefGuard/defguard into activ…
filipslezaklab May 19, 2025
28d0198
fix params parsing for axum compatible arrays
filipslezaklab May 19, 2025
a379d16
add time filtering to activity view
filipslezaklab May 20, 2025
c4bbf5f
display timestamp in activity list as local time
filipslezaklab May 20, 2025
8efef27
fix timestamp formatting in activity list
filipslezaklab May 20, 2025
9e65874
add search to activity list
filipslezaklab May 20, 2025
9bf00d6
add more logger event types
wojcik91 May 20, 2025
2b764c6
add placeholder logic
wojcik91 May 21, 2025
b49715a
Translate event type and module in activtivy list
filipslezaklab May 21, 2025
ea5b2eb
Merge branch 'activity-ui' into event_router_poc
wojcik91 May 21, 2025
efa0a15
Merge branch 'dev' into event_router_poc
wojcik91 May 21, 2025
301a2a6
extract router and logger into separate crates
wojcik91 May 23, 2025
3cf8378
Merge branch 'dev' into activity-ui
wojcik91 May 27, 2025
ece2549
setup event channels for grpc servers
wojcik91 May 27, 2025
460a5e3
pass event tx to bidi services
wojcik91 May 28, 2025
8f73883
store map of connected clients in memory
wojcik91 Jun 2, 2025
66e54c2
emit client connected event
wojcik91 Jun 3, 2025
562d391
Merge branch 'dev' into vpn_client_events
wojcik91 Jun 3, 2025
66bee23
log audit event on VPN client connection
wojcik91 Jun 3, 2025
7ca1b5c
revert unintended merge changes
wojcik91 Jun 3, 2025
cf68b5f
handle client disconnect
wojcik91 Jun 3, 2025
0937680
handle emitting client disconnect events
wojcik91 Jun 4, 2025
b973af4
Merge branch 'dev' into vpn_client_events
wojcik91 Jun 4, 2025
2d26004
include ID in device name
wojcik91 Jun 4, 2025
85a5fe7
Merge branch 'dev' into vpn_client_events
wojcik91 Jun 4, 2025
e151558
remove commented out code
wojcik91 Jun 4, 2025
0e69a6a
update dependencies
wojcik91 Jun 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/defguard_core/src/db/models/wireguard_peer_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ pub struct WireguardPeerStats<I = NoId> {
pub device_id: Id,
pub collected_at: NaiveDateTime,
pub network: i64,
// optional because it's not available until a peer actually connects
pub endpoint: Option<String>,
// bytes sent to peer
pub upload: i64,
// bytes received from peer
pub download: i64,
pub latest_handshake: NaiveDateTime,
// FIXME: can contain multiple IP addresses
Expand Down
45 changes: 44 additions & 1 deletion crates/defguard_core/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::net::IpAddr;

use crate::db::Id;
use crate::db::{Device, Id, WireguardNetwork};
use chrono::{NaiveDateTime, Utc};
use ipnetwork::IpNetwork;

Expand Down Expand Up @@ -31,6 +31,39 @@ impl ApiRequestContext {
}
}

/// Shared context for every event generated by a user in the gRPC server.
///
/// Similarly to `ApiRequestContexts` at the moment it's mostly meant to populate the audit log.
#[derive(Debug)]
pub struct GrpcRequestContext {
pub timestamp: NaiveDateTime,
pub user_id: Id,
pub username: String,
pub ip: IpAddr,
pub device_id: Id,
pub device_name: String,
}

impl GrpcRequestContext {
pub fn new(
user_id: Id,
username: String,
ip: IpAddr,
device_id: Id,
device_name: String,
) -> Self {
let timestamp = Utc::now().naive_utc();
Self {
timestamp,
user_id,
username,
ip,
device_id,
device_name,
}
}
}

#[derive(Debug)]
pub enum ApiEventType {
UserLogin,
Expand Down Expand Up @@ -116,6 +149,16 @@ pub struct ApiEvent {
pub enum GrpcEvent {
GatewayConnected,
GatewayDisconnected,
ClientConnected {
context: GrpcRequestContext,
location: WireguardNetwork<Id>,
device: Device<Id>,
},
ClientDisconnected {
context: GrpcRequestContext,
location: WireguardNetwork<Id>,
device: Device<Id>,
},
}

/// Shared context for every event generated from a user request in the bi-directional gRPC stream.
Expand Down
198 changes: 198 additions & 0 deletions crates/defguard_core/src/grpc/gateway/client_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use std::{collections::HashMap, net::IpAddr};

use chrono::{NaiveDateTime, TimeDelta, Utc};
use thiserror::Error;
use tonic::{Code, Status};

use crate::{
db::{models::wireguard_peer_stats::WireguardPeerStats, Device, Id, User},
events::GrpcRequestContext,
};

#[derive(Debug, Error)]
pub enum ClientMapError {
#[error("VPN client {public_key} is already connected to location {location_id}")]
ClientAlreadyConnected { public_key: String, location_id: Id },
#[error("VPN client {public_key} is not connected to location {location_id}")]
ClientNotFound { public_key: String, location_id: Id },
#[error("Client state for location {location_id} not found")]
LocationNotFound { location_id: Id },
}

impl From<ClientMapError> for Status {
fn from(value: ClientMapError) -> Self {
Self::new(Code::Internal, value.to_string())
}
}

/// Represents current information about a connected VPN client
#[derive(Debug, Serialize, Clone)]
pub struct ClientState {
pub device: Device<Id>,
pub user_id: Id,
pub username: String,
// current IP from which the client is connecting
pub endpoint: IpAddr,
pub latest_handshake: NaiveDateTime,
// when last stats update was received
pub latest_update: NaiveDateTime,
// total bytes sent to peer
pub total_upload: i64,
// total bytes received from peer
pub total_download: i64,
}

impl ClientState {
pub fn new(
device: Device<Id>,
user: &User<Id>,
endpoint: IpAddr,
latest_handshake: NaiveDateTime,
total_upload: i64,
total_download: i64,
) -> Self {
let latest_update = Utc::now().naive_utc();
Self {
device,
user_id: user.id,
username: user.username.clone(),
endpoint,
latest_handshake,
latest_update,
total_upload,
total_download,
}
}

pub fn update_client_state(
&mut self,
current_device: Device<Id>,
current_endpoint: IpAddr,
latest_handshake: NaiveDateTime,
upload: i64,
download: i64,
) {
self.latest_update = Utc::now().naive_utc();
self.device = current_device;
self.endpoint = current_endpoint;
self.latest_handshake = latest_handshake;
self.total_upload = upload;
self.total_download = download;
}
}

/// Helper struct used to handle connected VPN clients state
/// Clients are grouped by location ID
type ClientPubKey = String;
#[derive(Debug, Serialize, Clone)]
pub struct ClientMap(HashMap<Id, HashMap<ClientPubKey, ClientState>>);

impl ClientMap {
pub fn new() -> Self {
Self(HashMap::new())
}

pub fn get_vpn_client(
&mut self,
location_id: Id,
client_pubkey: &str,
) -> Option<&mut ClientState> {
self.0
.get_mut(&location_id)
.and_then(|location_map| location_map.get_mut(client_pubkey))
}

/// Adds newly connected VPN client to client state map
pub fn connect_vpn_client(
&mut self,
location_id: Id,
gateway_hostname: &str,
public_key: &str,
device: &Device<Id>,
user: &User<Id>,
endpoint: IpAddr,
stats: &WireguardPeerStats,
) -> Result<(), ClientMapError> {
info!(
"VPN client {} with public key {public_key} connected to location {location_id} through gateway {gateway_hostname}",
device.name
);

// initialize location map if it doesn't exist yet
let location_map = match self.0.get_mut(&location_id) {
Some(location_map) => location_map,
None => {
// initialize new map for location and immediately return a mutable reference
self.0.insert(location_id, HashMap::new());
self.0.get_mut(&location_id).unwrap()
}
};

// check if client is already connected
if location_map.contains_key(public_key) {
return Err(ClientMapError::ClientAlreadyConnected {
public_key: public_key.to_string(),
location_id,
});
};

// add client state to location map
let client_state = ClientState::new(
device.clone(),
user,
endpoint,
stats.latest_handshake,
stats.upload,
stats.download,
);
location_map.insert(public_key.to_string(), client_state);

Ok(())
}

/// Removes all disconnected clients for a given location.
///
/// A client is considered disconnected if there have not been any stats received for it in more than `peer_disconnect_threshold_secs`.
///
/// Returns a list of devices.
pub fn disconnect_inactive_vpn_clients_for_location(
&mut self,
location_id: Id,
peer_disconnect_threshold_secs: i32,
) -> Result<Vec<(Device<Id>, GrpcRequestContext)>, ClientMapError> {
debug!("Disconnecting inactive VPN clients for location {location_id}");

// initialize result
let mut disconnected_clients = Vec::new();

// get client state map for given location
let location_map = match self.0.get_mut(&location_id) {
Some(location_map) => location_map,
None => {
return Err(ClientMapError::LocationNotFound { location_id });
}
};

let disconnect_threshold = TimeDelta::seconds(peer_disconnect_threshold_secs.into());

// remove clients which have been inactive longer than given location's `peer_disconnect_threshold`
location_map.retain(|_public_key, client_state| {
let now = Utc::now().naive_utc();
if (now - client_state.latest_update) > disconnect_threshold {
let disconnect_event_context = GrpcRequestContext::new(
client_state.user_id,
client_state.username.clone(),
client_state.endpoint,
client_state.device.id,
client_state.device.name.clone(),
);
disconnected_clients.push((client_state.device.clone(), disconnect_event_context));

return false;
};
true
});

Ok(disconnected_clients)
}
}
Loading