Support controller user attribution in audit logs (#15407)

* Support controller user attribution in audit logs

This PR supports associating audit logs with the controller user.

  ## Implementation:
  - Add `ControlledContext { conn_audit_token }` to `PunchHole`, `RequestRelay`, and `FetchLocalAddr`.
  - The server sends a controller-user identity snapshot to the controlled client through rendezvous messages.
  - The controlled client sends the token back to the server when posting the `on_open` conn audit or IP whitelist alarm audit.
  - This lets the server attach the controller user to audit logs.

  ## How the controlled client helps identify the controller user:
  - Conn audit: sends the token to the server in `on_open`; the server creates the audit log and caches the user snapshot.
  - File audit: sends `id` and `conn_id`; the server uses them to find the cached user snapshot.
  - Alarm audit: IP whitelist sends the token directly; other alarm logs send `id` and `conn_id`, and the server uses them to find the cached user
  snapshot.

  ## Compatibility:
  - Supported only for logs created with a new server and a new controlled client.
  - Does not require upgrading the controller client.

  ## Test

  - [x] New/old clients connected to new/old servers, and conn/file/alarm audit logs worked normally.
  - [x] New client connected to new server generated searchable conn/file/alarm audit logs.
  - [x] Punch hole, local addr, and relay paths worked with audit logs and control role on new/old servers.
  - [x] Direct IP connections produced audit logs, but do not support user audit.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* rename conn_audit_token to conn_audit_ref

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2026-06-26 15:07:27 +08:00
committed by GitHub
parent 78b5f47668
commit 989bf80fe8
4 changed files with 104 additions and 91 deletions

View File

@@ -310,6 +310,7 @@ pub struct Connection {
video_ack_required: bool,
server_audit_conn: String,
server_audit_file: String,
controlled_context: Option<ControlledContext>,
lr: LoginRequest,
peer_argb: u32,
session_last_recv_time: Option<Arc<Mutex<Instant>>>,
@@ -407,8 +408,12 @@ impl Connection {
stream: super::Stream,
id: i32,
server: super::ServerPtrWeak,
control_permissions: Option<ControlPermissions>,
meta: super::ConnectionMeta,
) {
let super::ConnectionMeta {
control_permissions,
controlled_context,
} = meta;
// Android is not supported yet, so we always set control_permissions to None.
#[cfg(target_os = "android")]
let control_permissions = None;
@@ -495,6 +500,7 @@ impl Connection {
video_ack_required: false,
server_audit_conn: "".to_owned(),
server_audit_file: "".to_owned(),
controlled_context,
lr: Default::default(),
peer_argb: 0u32,
session_last_recv_time: None,
@@ -1308,7 +1314,7 @@ impl Connection {
{
self.send_login_error("Your ip is blocked by the peer")
.await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::IpWhitelist, //"ip whitelist",
json!({ "ip":addr.ip() }),
);
@@ -1334,10 +1340,14 @@ impl Connection {
msg_out.set_hash(self.hash.clone());
self.send(msg_out).await;
self.get_api_server();
self.post_conn_audit(json!({
let mut audit = json!({
"ip": addr.ip(),
"action": "new",
}));
});
if let Some(audit_ref) = self.conn_audit_ref() {
audit["conn_audit_ref"] = json!(audit_ref);
}
self.post_conn_audit(audit);
true
}
@@ -1354,6 +1364,18 @@ impl Connection {
);
}
fn conn_audit_ref(&self) -> Option<&str> {
let audit_ref = self
.controlled_context
.as_ref()
.map(|c| c.conn_audit_ref.as_str())?;
if audit_ref.is_empty() {
None
} else {
Some(audit_ref)
}
}
fn post_conn_audit(&self, v: Value) {
if self.server_audit_conn.is_empty() {
return;
@@ -1408,6 +1430,7 @@ impl Connection {
"id":json!(Config::get_id()),
"uuid":json!(crate::encode64(hbb_common::get_uuid())),
"peer_id":json!(self.lr.my_id),
"conn_id":json!(self.inner.id()),
"type": r#type as i8,
"path":path,
"is_file":is_file,
@@ -1418,7 +1441,7 @@ impl Connection {
});
}
pub fn post_alarm_audit(typ: AlarmAuditType, info: Value) {
fn post_alarm_audit(&self, typ: AlarmAuditType, info: Value) {
let url = crate::get_audit_server(
Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"),
@@ -1432,6 +1455,12 @@ impl Connection {
v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
v["typ"] = json!(typ as i8);
v["info"] = serde_json::Value::String(info.to_string());
v["conn_id"] = json!(self.inner.id());
if typ == AlarmAuditType::IpWhitelist {
if let Some(audit_ref) = self.conn_audit_ref() {
v["conn_audit_ref"] = json!(audit_ref);
}
}
tokio::spawn(async move {
allow_err!(Self::post_audit_async(url, v).await);
});
@@ -1565,9 +1594,10 @@ impl Connection {
.unwrap()
.get(&self.session_key())
.map(|s| s.last_recv_time.clone());
self.post_conn_audit(
json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}),
);
self.post_conn_audit(json!({
"peer": ((&self.lr.my_id, &self.lr.my_name)),
"type": conn_type,
}));
#[allow(unused_mut)]
let mut username = crate::platform::get_active_username();
let mut res = LoginResponse::new();
@@ -3668,7 +3698,7 @@ impl Connection {
);
self.send_login_error("Please try 1 minute later").await;
sleep(1.).await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::TerminalOsLoginConcurrency,
json!({
"ip": self.ip,
@@ -3856,7 +3886,7 @@ impl Connection {
prefix_num
))
.await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::ExceedIPv6PrefixAttempts,
json!({
"ip": self.ip,
@@ -3901,7 +3931,7 @@ impl Connection {
if let Some(audit) = decision.audit {
// For OS blocked/backoff events, we currently emit one alarm report per blocked attempt.
// TODO: Add unified cumulative/aggregation fields across alarm producers.
Self::post_alarm_audit(
self.post_alarm_audit(
audit,
json!({
"ip": self.ip,
@@ -3938,7 +3968,7 @@ impl Connection {
let res = if failure.2 > 30 {
self.send_login_error("Too many wrong attempts").await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::ExceedThirtyAttempts,
json!({
"ip": self.ip,
@@ -3949,7 +3979,7 @@ impl Connection {
false
} else if time == failure.0 && failure.1 > 6 {
self.send_login_error("Please try 1 minute later").await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::SixAttemptsWithinOneMinute,
json!({
"ip": self.ip,
@@ -5490,6 +5520,7 @@ fn try_activate_screen() {
});
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AlarmAuditType {
IpWhitelist = 0,
ExceedThirtyAttempts = 1,