feat(heal): gate control capability by cluster topology (#4994)

* fix(rpc): bind internode auth to exact targets

* fix(heal): initialize the runtime atomically

* fix(heal): aggregate status across cluster nodes

* fix(heal): return canonical tokens for duplicate starts

* feat(heal): add authenticated control RPC contract

* feat(heal): gate control capability by cluster topology

* fix(heal): return canonical tokens for duplicate starts (#4992)

---------

Co-authored-by: Zhengchao An <anzhengchao@gmail.com>
This commit is contained in:
cxymds
2026-07-20 13:04:30 +08:00
committed by GitHub
parent c92e99ba95
commit 908ca548bb
14 changed files with 805 additions and 39 deletions
@@ -15,7 +15,7 @@
use crate::cluster::rpc::client::{
TonicInterceptor, gen_tonic_signature_interceptor, heal_control_time_out_client, node_service_time_out_client,
};
use crate::cluster::rpc::set_tonic_canonical_body_digest;
use crate::cluster::rpc::{set_tonic_canonical_body_digest, verify_tonic_rpc_response_proof};
use crate::error::{Error, Result};
use crate::{
disk::disk_store::{get_drive_active_check_interval, get_drive_active_check_timeout},
@@ -94,6 +94,11 @@ fn decode_scanner_activity(response: ScannerActivityResponse) -> Result<ScannerP
})
}
fn validate_heal_control_capability_proof(canonical_ack: &[u8], proof: &[u8]) -> Result<()> {
verify_tonic_rpc_response_proof(canonical_ack, proof)
.map_err(|_| Error::other("peer returned an invalid heal control capability proof"))
}
#[derive(Clone, Debug)]
pub struct PeerLiveEventsBatch {
pub events: Vec<u8>,
@@ -762,6 +767,24 @@ impl PeerRestClient {
.await
}
/// Confirms that a peer supports heal-control v1 and has the same storage
/// topology. Every non-success response is an error so old or divergent
/// peers cannot be mistaken for compatible ones.
pub async fn probe_heal_control(&self, topology_fingerprint: String) -> Result<()> {
let nonce = uuid::Uuid::new_v4();
let probe = rustfs_protos::heal_control_capability_probe(nonce.as_bytes());
let canonical_ack = rustfs_protos::canonical_heal_control_capability_ack(
rustfs_protos::HEAL_CONTROL_PROTOCOL_VERSION,
&topology_fingerprint,
&probe,
)
.map_err(|_| Error::other("heal control capability acknowledgement length cannot be represented"))?;
let proof = self
.heal_control(rustfs_protos::HEAL_CONTROL_PROTOCOL_VERSION, topology_fingerprint, probe)
.await?;
validate_heal_control_capability_proof(&canonical_ack, &proof)
}
pub async fn load_bucket_metadata(&self, bucket: &str, scanner_maintenance_change: bool) -> Result<()> {
self.finalize_result(
async {
@@ -1374,6 +1397,16 @@ mod tests {
assert!(!client.offline.load(Ordering::Acquire));
}
#[test]
fn heal_control_capability_proof_must_authenticate_exact_ack() {
runtime_sources::ensure_test_rpc_secret();
let proof = crate::cluster::rpc::sign_tonic_rpc_response_proof(b"expected").expect("test proof should sign");
assert!(validate_heal_control_capability_proof(b"expected", &proof).is_ok());
let err = validate_heal_control_capability_proof(b"different", &proof)
.expect_err("a proof for a different acknowledgement must fail closed");
assert!(err.to_string().contains("invalid heal control capability proof"));
}
#[tokio::test]
async fn peer_rest_client_prepare_retry_clears_offline_gate() {
// finalize_result sets the offline gate on a network error; without