feat(rpc): bind canonical body digest into internode mutating disk RPC signatures (#5234)

* feat(rpc): bind canonical body digest into internode mutating disk RPC signatures

Binds a domain-separated, length-prefixed canonical request-body digest into the
v2 HMAC signature scope for every mutating NodeService disk RPC, so an on-path
attacker on the default-plaintext internode channel can no longer tamper with a
mutation payload (or strip the msgpack `_bin` field to force the JSON fallback
decode) without invalidating the signature.

Covers 13 mutating disk RPCs: RenameData, DeleteVersion, DeleteVersions,
WriteMetadata, UpdateMetadata, WriteAll, Delete, DeletePaths, RenameFile,
RenamePart, DeleteVolume, MakeVolume, MakeVolumes. The digest covers both the
msgpack `_bin` payloads and their JSON compatibility copies. Gated fail-open by
default (RUSTFS_INTERNODE_RPC_BODY_DIGEST_STRICT) with a convergence counter, so
rolling upgrades are byte-for-byte unaffected; the replay-cache capacity is now
configurable and overflow fails closed with a metric.

Refs https://github.com/rustfs/backlog/issues/1327

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(rpc): satisfy architecture-migration compat-marker guard

Put the removal condition on the RUSTFS_COMPAT_TODO marker line itself, and
stop backticking env-var/metric names in the cleanup-register entry so the
guard's id extractor only sees the task-id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Zhengchao An
2026-07-25 22:56:27 +08:00
committed by GitHub
parent 6974963e20
commit 61d4e04d65
11 changed files with 1198 additions and 18 deletions
+58 -13
View File
@@ -16,6 +16,7 @@ use crate::cluster::rpc::client::{
TonicInterceptor, gen_tonic_signature_interceptor, is_network_like_disk_error, node_service_time_out_client,
node_service_time_out_client_for_class, node_service_time_out_client_no_auth,
};
use crate::cluster::rpc::http_auth::set_tonic_canonical_body_digest;
use crate::cluster::rpc::internode_data_transport::{
InternodeDataTransport, NsScannerCapabilityRequest, NsScannerStreamRequest, ReadStreamRequest, WalkDirStreamRequest,
WriteStreamRequest,
@@ -99,6 +100,18 @@ const LOG_SUBSYSTEM_REMOTE_DISK: &str = "remote_disk";
const EVENT_REMOTE_DISK_HEALTH: &str = "remote_disk_health";
const EVENT_REMOTE_DISK_RPC: &str = "remote_disk_rpc";
/// Bind a mutating disk RPC to its canonical body: the digest lands in the request metadata, and
/// the signing interceptor folds it (plus a replay-protected nonce) into the v2 signature scope
/// (backlog#1327).
fn attach_mutation_body_digest<T>(
request: &mut Request<T>,
canonical_body: std::result::Result<Vec<u8>, std::num::TryFromIntError>,
op: &'static str,
) -> Result<()> {
let canonical_body = canonical_body.map_err(|_| Error::other(format!("{op} request length cannot be represented")))?;
set_tonic_canonical_body_digest(request, &canonical_body).map_err(Error::other)
}
fn decode_volume_infos(volume_infos: Vec<String>) -> Result<Vec<VolumeInfo>> {
volume_infos
.into_iter()
@@ -1339,10 +1352,12 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(MakeVolumeRequest {
let mut request = Request::new(MakeVolumeRequest {
disk: self.endpoint.to_string(),
volume: volume.to_string(),
});
let canonical_body = rustfs_protos::canonical_make_volume_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "make_volume")?;
let response = client.make_volume(request).await?.into_inner();
@@ -1376,10 +1391,12 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(MakeVolumesRequest {
let mut request = Request::new(MakeVolumesRequest {
disk: self.endpoint.to_string(),
volumes: volumes.iter().map(|s| (*s).to_string()).collect(),
});
let canonical_body = rustfs_protos::canonical_make_volumes_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "make_volumes")?;
let response = client.make_volumes(request).await?.into_inner();
@@ -1489,11 +1506,13 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(DeleteVolumeRequest {
let mut request = Request::new(DeleteVolumeRequest {
disk: self.endpoint.to_string(),
volume: volume.to_string(),
force: force_delete,
});
let canonical_body = rustfs_protos::canonical_delete_volume_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "delete_volume")?;
let response = client.delete_volume(request).await?.into_inner();
@@ -1542,7 +1561,7 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(DeleteVersionRequest {
let mut request = Request::new(DeleteVersionRequest {
disk: self.endpoint.to_string(),
volume: volume.to_string(),
path: path.to_string(),
@@ -1552,6 +1571,8 @@ impl DiskAPI for RemoteDisk {
file_info_bin: file_info_bin.into(),
opts_bin: opts_bin.into(),
});
let canonical_body = rustfs_protos::canonical_delete_version_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "delete_version")?;
let response = client.delete_version(request).await?.into_inner();
@@ -1643,7 +1664,7 @@ impl DiskAPI for RemoteDisk {
}
};
let request = Request::new(DeleteVersionsRequest {
let mut request = Request::new(DeleteVersionsRequest {
disk: self.endpoint.to_string(),
volume: volume.to_string(),
versions: versions_str,
@@ -1651,6 +1672,14 @@ impl DiskAPI for RemoteDisk {
versions_bin,
opts_bin: opts_bin.into(),
});
let canonical_body = rustfs_protos::canonical_delete_versions_request_body(request.get_ref());
if let Err(err) = attach_mutation_body_digest(&mut request, canonical_body, "delete_versions") {
let mut errors = Vec::with_capacity(versions.len());
for _ in 0..versions.len() {
errors.push(Some(err.clone()));
}
return errors;
}
// TODO: use Error not string
@@ -1719,11 +1748,13 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(DeletePathsRequest {
let mut request = Request::new(DeletePathsRequest {
disk: self.endpoint.to_string(),
volume: volume.to_string(),
paths: paths.clone(),
});
let canonical_body = rustfs_protos::canonical_delete_paths_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "delete_paths")?;
let response = client.delete_paths(request).await?.into_inner();
@@ -1762,13 +1793,15 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(WriteMetadataRequest {
let mut request = Request::new(WriteMetadataRequest {
disk,
volume: volume.to_string(),
path: path.to_string(),
file_info,
file_info_bin: file_info_bin.into(),
});
let canonical_body = rustfs_protos::canonical_write_metadata_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "write_metadata")?;
let response = client.write_metadata(request).await?.into_inner();
@@ -1840,7 +1873,7 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(UpdateMetadataRequest {
let mut request = Request::new(UpdateMetadataRequest {
disk,
volume: volume.to_string(),
path: path.to_string(),
@@ -1849,6 +1882,8 @@ impl DiskAPI for RemoteDisk {
file_info_bin: file_info_bin.into(),
opts_bin: opts_bin.into(),
});
let canonical_body = rustfs_protos::canonical_update_metadata_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "update_metadata")?;
let response = client.update_metadata(request).await?.into_inner();
@@ -2096,7 +2131,7 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(RenameDataRequest {
let mut request = Request::new(RenameDataRequest {
disk: self.endpoint.to_string(),
src_volume: src_volume.to_string(),
src_path: src_path.to_string(),
@@ -2105,6 +2140,8 @@ impl DiskAPI for RemoteDisk {
dst_path: dst_path.to_string(),
file_info_bin: file_info_bin.into(),
});
let canonical_body = rustfs_protos::canonical_rename_data_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "rename_data")?;
let response = client.rename_data(request).await?.into_inner();
@@ -2361,13 +2398,15 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(RenameFileRequest {
let mut request = Request::new(RenameFileRequest {
disk: self.endpoint.to_string(),
src_volume: src_volume.to_string(),
src_path: src_path.to_string(),
dst_volume: dst_volume.to_string(),
dst_path: dst_path.to_string(),
});
let canonical_body = rustfs_protos::canonical_rename_file_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "rename_file")?;
let response = client.rename_file(request).await?.into_inner();
@@ -2404,7 +2443,7 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(RenamePartRequest {
let mut request = Request::new(RenamePartRequest {
disk: self.endpoint.to_string(),
src_volume: src_volume.to_string(),
src_path: src_path.to_string(),
@@ -2412,6 +2451,8 @@ impl DiskAPI for RemoteDisk {
dst_path: dst_path.to_string(),
meta,
});
let canonical_body = rustfs_protos::canonical_rename_part_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "rename_part")?;
let response = client.rename_part(request).await?.into_inner();
@@ -2449,12 +2490,14 @@ impl DiskAPI for RemoteDisk {
.get_client()
.await
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
let request = Request::new(DeleteRequest {
let mut request = Request::new(DeleteRequest {
disk: self.endpoint.to_string(),
volume: volume.to_string(),
path: path.to_string(),
options,
});
let canonical_body = rustfs_protos::canonical_delete_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "delete")?;
let response = client.delete(request).await?.into_inner();
@@ -2665,12 +2708,14 @@ impl DiskAPI for RemoteDisk {
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_write_all_error();
Error::other(format!("can not get client, err: {err}"))
})?;
let request = Request::new(WriteAllRequest {
let mut request = Request::new(WriteAllRequest {
disk,
volume: volume.to_string(),
path: path.to_string(),
data,
});
let canonical_body = rustfs_protos::canonical_write_all_request_body(request.get_ref());
attach_mutation_body_digest(&mut request, canonical_body, "write_all")?;
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_write_all_request();
let response = match client.write_all(request).await {