From 94e0a5128d7f34fb5e8e3ce74f21cd9bf05a39f7 Mon Sep 17 00:00:00 2001 From: Adrian Duke Date: Fri, 13 Feb 2026 17:51:33 +0000 Subject: [PATCH] node: Add block command to control socket Introduces the ability to explicitly block a peer via the node control socket. Previously, the node only exposed follow and unfollow commands. While the underlying policy database schema supported a Block variant, there was no mechanism to trigger this state via the client handle. The new block command: 1. Updates the node's follow policy to Block. 2. Immediately disconnects the peer if a session is active. --- crates/radicle-node/src/control.rs | 8 +++++++ crates/radicle-node/src/runtime/handle.rs | 6 ++++++ crates/radicle-node/src/test/handle.rs | 11 +++++++++- crates/radicle-protocol/src/service.rs | 14 +++++++++++++ .../radicle-protocol/src/service/command.rs | 3 +++ crates/radicle/CHANGELOG.md | 2 ++ crates/radicle/src/node.rs | 9 ++++++++ crates/radicle/src/node/command.rs | 4 ++++ crates/radicle/src/node/policy/store.rs | 21 +++++++++++++++---- 9 files changed, 73 insertions(+), 5 deletions(-) diff --git a/crates/radicle-node/src/control.rs b/crates/radicle-node/src/control.rs index 9ed568dd..1836f9f7 100644 --- a/crates/radicle-node/src/control.rs +++ b/crates/radicle-node/src/control.rs @@ -170,6 +170,14 @@ where return Err(CommandError::Runtime(e)); } }, + Command::Block { nid } => match handle.block(nid) { + Ok(result) => { + CommandResult::updated(result).to_writer(writer)?; + } + Err(e) => { + return Err(CommandError::Runtime(e)); + } + }, Command::Unfollow { nid } => match handle.unfollow(nid) { Ok(result) => { CommandResult::updated(result).to_writer(writer)?; diff --git a/crates/radicle-node/src/runtime/handle.rs b/crates/radicle-node/src/runtime/handle.rs index e33a374a..d3acc256 100644 --- a/crates/radicle-node/src/runtime/handle.rs +++ b/crates/radicle-node/src/runtime/handle.rs @@ -247,6 +247,12 @@ impl radicle::node::Handle for Handle { Ok(receiver.recv()??) } + fn block(&mut self, id: NodeId) -> Result { + let (sender, receiver) = chan::bounded(1); + self.command(service::Command::Block(id, sender))?; + receiver.recv().map_err(Error::from) + } + fn seed(&mut self, id: RepoId, scope: policy::Scope) -> Result { let (responder, receiver) = service::command::Responder::oneshot(); self.command(service::Command::Seed(id, scope, responder))?; diff --git a/crates/radicle-node/src/test/handle.rs b/crates/radicle-node/src/test/handle.rs index 0c5b6e23..b005f3ae 100644 --- a/crates/radicle-node/src/test/handle.rs +++ b/crates/radicle-node/src/test/handle.rs @@ -18,6 +18,7 @@ pub struct Handle { pub updates: Arc>>, pub seeding: Arc>>, pub following: Arc>>, + pub blocked: Arc>>, } impl radicle::node::Handle for Handle { @@ -85,6 +86,7 @@ impl radicle::node::Handle for Handle { } fn follow(&mut self, id: NodeId, _alias: Option) -> Result { + self.blocked.lock().unwrap().remove(&id); Ok(self.following.lock().unwrap().insert(id)) } @@ -93,7 +95,14 @@ impl radicle::node::Handle for Handle { } fn unfollow(&mut self, id: NodeId) -> Result { - Ok(self.following.lock().unwrap().remove(&id)) + let f = self.following.lock().unwrap().remove(&id); + let b = self.blocked.lock().unwrap().remove(&id); + Ok(f || b) + } + + fn block(&mut self, id: NodeId) -> Result { + self.following.lock().unwrap().remove(&id); + Ok(self.blocked.lock().unwrap().insert(id)) } fn announce_refs_for( diff --git a/crates/radicle-protocol/src/service.rs b/crates/radicle-protocol/src/service.rs index e5b9cdb3..2e2a86ff 100644 --- a/crates/radicle-protocol/src/service.rs +++ b/crates/radicle-protocol/src/service.rs @@ -856,6 +856,16 @@ where .expect("Service::command: error unfollowing node"); resp.ok(updated).ok(); } + Command::Block(id, resp) => { + let updated = self + .policies + .set_follow_policy(&id, policy::Policy::Block) + .expect("Service::command: error blocking node"); + if updated { + self.outbox.disconnect(id, DisconnectReason::Policy); + } + resp.send(updated).ok(); + } Command::AnnounceRefs(id, namespaces, resp) => { let doc = match self.storage.get(id) { Ok(Some(doc)) => doc, @@ -1344,6 +1354,7 @@ where DisconnectReason::Session(e) => e.severity(), DisconnectReason::Command | DisconnectReason::Conflict + | DisconnectReason::Policy | DisconnectReason::SelfConnection => Severity::Low, }; @@ -2684,6 +2695,8 @@ pub enum DisconnectReason { Conflict, /// Connection to self. SelfConnection, + /// Peer is blocked by policy + Policy, /// User requested disconnect Command, } @@ -2712,6 +2725,7 @@ impl fmt::Display for DisconnectReason { Self::Command => write!(f, "command"), Self::SelfConnection => write!(f, "self-connection"), Self::Conflict => write!(f, "conflict"), + Self::Policy => write!(f, "policy"), Self::Session(err) => write!(f, "{err}"), Self::Fetch(err) => write!(f, "fetch: {err}"), } diff --git a/crates/radicle-protocol/src/service/command.rs b/crates/radicle-protocol/src/service/command.rs index 9baf6e30..05bb17a9 100644 --- a/crates/radicle-protocol/src/service/command.rs +++ b/crates/radicle-protocol/src/service/command.rs @@ -99,6 +99,8 @@ pub enum Command { Follow(NodeId, Option, Responder), /// Unfollow the given node. Unfollow(NodeId, Responder), + /// Block the given node. + Block(NodeId, Sender), /// Query the internal service state. QueryState(Arc, Sender>), } @@ -194,6 +196,7 @@ impl fmt::Debug for Command { Self::Unseed(id, _) => write!(f, "Unseed({id})"), Self::Follow(id, _, _) => write!(f, "Follow({id})"), Self::Unfollow(id, _) => write!(f, "Unfollow({id})"), + Self::Block(id, _) => write!(f, "Block({id})"), Self::QueryState { .. } => write!(f, "QueryState(..)"), } } diff --git a/crates/radicle/CHANGELOG.md b/crates/radicle/CHANGELOG.md index 856b1c9e..3d5470ce 100644 --- a/crates/radicle/CHANGELOG.md +++ b/crates/radicle/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `radicle::node::Handle` added a `block` method to allow setting the follow + policy to `Policy::Block`. - `radicle::node::Handle::announce_refs_for` now allows specifying for which namespaces changes should be announced. A corresponding enum variant `radicle::node::Command::AnnounceRefsFor` is added. diff --git a/crates/radicle/src/node.rs b/crates/radicle/src/node.rs index 2dafb663..2dd62c9c 100644 --- a/crates/radicle/src/node.rs +++ b/crates/radicle/src/node.rs @@ -907,6 +907,8 @@ pub trait Handle: Clone + Sync + Send { fn seed(&mut self, id: RepoId, scope: policy::Scope) -> Result; /// Start following the given peer. fn follow(&mut self, id: NodeId, alias: Option) -> Result; + /// Set the following policy to block for the given peer. + fn block(&mut self, id: NodeId) -> Result; /// Un-seed the given repo and delete it from storage. fn unseed(&mut self, id: RepoId) -> Result; /// Unfollow the given peer. @@ -1191,6 +1193,13 @@ impl Handle for Node { Ok(response.updated) } + fn block(&mut self, nid: NodeId) -> Result { + let mut lines = self.call::(Command::Block { nid }, DEFAULT_TIMEOUT)?; + let response = lines.next().ok_or(Error::EmptyResponse)??; + + Ok(response.updated) + } + fn seed(&mut self, rid: RepoId, scope: policy::Scope) -> Result { let mut lines = self.call::(Command::Seed { rid, scope }, DEFAULT_TIMEOUT)?; let response = lines.next().ok_or(Error::EmptyResponse)??; diff --git a/crates/radicle/src/node/command.rs b/crates/radicle/src/node/command.rs index 55ace68b..dfe9f85a 100644 --- a/crates/radicle/src/node/command.rs +++ b/crates/radicle/src/node/command.rs @@ -120,6 +120,10 @@ pub enum Command { #[serde(rename_all = "camelCase")] Unfollow { nid: NodeId }, + /// Block the given node. + #[serde(rename_all = "camelCase")] + Block { nid: NodeId }, + /// Get the node's status. Status, diff --git a/crates/radicle/src/node/policy/store.rs b/crates/radicle/src/node/policy/store.rs index bfe0946a..23767985 100644 --- a/crates/radicle/src/node/policy/store.rs +++ b/crates/radicle/src/node/policy/store.rs @@ -119,14 +119,15 @@ impl Store { /// Follow a node. pub fn follow(&mut self, id: &NodeId, alias: Option<&Alias>) -> Result { let mut stmt = self.db.prepare( - "INSERT INTO `following` (id, alias) - VALUES (?1, ?2) - ON CONFLICT DO UPDATE - SET alias = ?2 WHERE alias != ?2", + "INSERT INTO `following` (id, alias, policy) + VALUES (?1, ?2, ?3) + ON CONFLICT (id) DO UPDATE + SET alias = ?2, policy = ?3 WHERE alias != ?2 OR policy != ?3", )?; stmt.bind((1, id))?; stmt.bind((2, alias.map_or("", |alias| alias.as_str())))?; + stmt.bind((3, Policy::Allow))?; stmt.next()?; Ok(self.db.change_count() > 0) @@ -248,6 +249,18 @@ impl Store { )) } + /// Returns `true` if there is a follow policy for the given node, and that + /// policy is [`Policy::Block`]. + pub fn is_blocked(&self, id: &NodeId) -> Result { + Ok(matches!( + self.follow_policy(id)?, + Some(FollowPolicy { + policy: Policy::Block, + .. + }) + )) + } + /// Get a node's follow policy. pub fn follow_policy(&self, id: &NodeId) -> Result, Error> { let mut stmt = self