node: Limit number of Refs in RefAnnouncements

Use the BoundedVec to limit the supported number of refs in
RefAnnouncement messages.  The size (235) is the maximum supported by
message wire size.

To reduce the complexity of the changes, BoundedVec is used again
instead of using a new bounded Refs or BTreeMap.  This requires
defining new Decode/Encode logic for Ref's element types, and
workarounds for arbitrary instances.

Signed-off-by: Slack Coder <slackcoder@server.ky>
This commit is contained in:
Slack Coder 2022-11-29 12:34:08 -05:00 committed by Alexis Sellier
parent c676d8b99a
commit e7f2637126
No known key found for this signature in database
8 changed files with 142 additions and 9 deletions

View File

@ -18,6 +18,25 @@ impl<T, const N: usize> BoundedVec<T, N> {
BoundedVec { v: Vec::new() } BoundedVec { v: Vec::new() }
} }
/// Build a `BoundedVec` by consuming from the given iterator up to its limit.
///
/// # Examples
///
/// ```
/// use radicle_node::bounded;
///
/// let mut iter = (0..4).into_iter();
/// let bounded: bounded::BoundedVec<i32,3> = bounded::BoundedVec::collect_from(&mut iter);
///
/// assert_eq!(bounded.len(), 3);
/// assert_eq!(iter.count(), 1);
/// ```
pub fn collect_from<I: Iterator<Item = T>>(iter: &mut I) -> Self {
BoundedVec {
v: iter.into_iter().take(N).collect(),
}
}
/// Create a new `BoundedVec<T,N>` which takes upto the first N values of its argument, taking /// Create a new `BoundedVec<T,N>` which takes upto the first N values of its argument, taking
/// ownership. /// ownership.
/// ///

View File

@ -73,6 +73,8 @@ pub const MAX_CONNECTION_ATTEMPTS: usize = 3;
pub use message::ADDRESS_LIMIT; pub use message::ADDRESS_LIMIT;
/// Maximum inventory limit imposed by message size limits. /// Maximum inventory limit imposed by message size limits.
pub use message::INVENTORY_LIMIT; pub use message::INVENTORY_LIMIT;
/// Maximum number of project git references imposed by message size limits.
pub use message::REF_LIMIT;
/// A service event. /// A service event.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -959,12 +961,22 @@ where
/// Announce local refs for given id. /// Announce local refs for given id.
fn announce_refs(&mut self, id: Id) -> Result<(), storage::Error> { fn announce_refs(&mut self, id: Id) -> Result<(), storage::Error> {
type Refs = BoundedVec<Id, REF_LIMIT>;
let node = self.node_id(); let node = self.node_id();
let repo = self.storage.repository(id)?; let repo = self.storage.repository(id)?;
let remote = repo.remote(&node)?; let remote = repo.remote(&node)?;
let peers = self.sessions.negotiated().map(|(_, _, p)| p); let peers = self.sessions.negotiated().map(|(_, _, p)| p);
let refs = remote.refs.into();
let timestamp = self.clock.as_secs(); let timestamp = self.clock.as_secs();
if remote.refs.len() > Refs::max() {
log::error!(
"refs announcement limit ({}) exceeded, other nodes will see only some of your project references",
Refs::max(),
);
}
let refs = BoundedVec::collect_from(&mut remote.refs.iter().map(|(a, b)| (a.clone(), *b)));
let msg = AnnouncementMessage::from(RefsAnnouncement { let msg = AnnouncementMessage::from(RefsAnnouncement {
id, id,
refs, refs,

View File

@ -4,16 +4,18 @@ use std::{fmt, io, mem, net};
use thiserror::Error; use thiserror::Error;
use crate::crypto; use crate::crypto;
use crate::git;
use crate::identity::Id; use crate::identity::Id;
use crate::node; use crate::node;
use crate::prelude::BoundedVec; use crate::prelude::BoundedVec;
use crate::service::filter::Filter; use crate::service::filter::Filter;
use crate::service::{NodeId, Timestamp, PROTOCOL_VERSION}; use crate::service::{NodeId, Timestamp, PROTOCOL_VERSION};
use crate::storage::refs::Refs;
use crate::wire; use crate::wire;
/// Maximum number of addresses which can be announced to other nodes. /// Maximum number of addresses which can be announced to other nodes.
pub const ADDRESS_LIMIT: usize = 16; pub const ADDRESS_LIMIT: usize = 16;
/// Maximum number of project git references.
pub const REF_LIMIT: usize = 235;
/// Maximum number of inventory which can be announced to other nodes. /// Maximum number of inventory which can be announced to other nodes.
pub const INVENTORY_LIMIT: usize = 2973; pub const INVENTORY_LIMIT: usize = 2973;
@ -225,7 +227,7 @@ pub struct RefsAnnouncement {
/// Repository identifier. /// Repository identifier.
pub id: Id, pub id: Id,
/// Updated refs. /// Updated refs.
pub refs: Refs, pub refs: BoundedVec<(git::RefString, git::Oid), REF_LIMIT>,
/// Time of announcement. /// Time of announcement.
pub timestamp: Timestamp, pub timestamp: Timestamp,
} }
@ -508,8 +510,43 @@ mod tests {
use crate::crypto::test::signer::MockSigner; use crate::crypto::test::signer::MockSigner;
use crate::test::arbitrary; use crate::test::arbitrary;
use fastrand;
use qcheck_macros::quickcheck; use qcheck_macros::quickcheck;
#[test]
fn test_ref_limit() {
let mut refs = Refs::default();
while refs.len() < REF_LIMIT {
refs.insert(arbitrary::refstring(u8::MAX as usize), arbitrary::oid());
}
let bounded_refs = BoundedVec::collect_from(&mut refs.iter().map(|(a, b)| (a.clone(), *b)));
let msg: Message = AnnouncementMessage::from(RefsAnnouncement {
id: arbitrary::gen(1),
refs: bounded_refs,
timestamp: LocalTime::now().as_secs(),
})
.signed(&MockSigner::default())
.into();
let mut buf: Vec<u8> = Vec::new();
assert!(
msg.encode(&mut buf).is_ok(),
"REF_LIMIT is too big to support message encoding",
);
let decoded = wire::deserialize(buf.as_slice());
assert!(
decoded.is_ok(),
"REF_LIMIT is too big to support message decoding"
);
assert_eq!(
msg,
decoded.unwrap(),
"encoding and decoding should be safe for message at REF_LIMIT",
);
}
#[test] #[test]
fn test_inventory_limit() { fn test_inventory_limit() {
let msg = Message::inventory( let msg = Message::inventory(
@ -543,9 +580,10 @@ mod tests {
fn prop_refs_announcement_signing(id: Id, refs: Refs) { fn prop_refs_announcement_signing(id: Id, refs: Refs) {
let signer = MockSigner::new(&mut fastrand::Rng::new()); let signer = MockSigner::new(&mut fastrand::Rng::new());
let timestamp = 0; let timestamp = 0;
let message = AnnouncementMessage::Refs(RefsAnnouncement { let message = AnnouncementMessage::Refs(RefsAnnouncement {
id, id,
refs, refs: BoundedVec::collect_from(&mut refs.iter().map(|(k, v)| (k.clone(), *v))),
timestamp, timestamp,
}); });
let ann = message.signed(&signer); let ann = message.signed(&signer);

View File

@ -56,7 +56,9 @@ impl Arbitrary for Message {
node: NodeId::arbitrary(g), node: NodeId::arbitrary(g),
message: RefsAnnouncement { message: RefsAnnouncement {
id: Id::arbitrary(g), id: Id::arbitrary(g),
refs: Refs::arbitrary(g), refs: BoundedVec::collect_from(
&mut Refs::arbitrary(g).iter().map(|(k, v)| (k.clone(), *v)),
),
timestamp: Timestamp::arbitrary(g), timestamp: Timestamp::arbitrary(g),
} }
.into(), .into(),

View File

@ -1,5 +1,4 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::collections::BTreeMap;
use std::iter; use std::iter;
use std::net; use std::net;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@ -13,7 +12,7 @@ use crate::crypto::test::signer::MockSigner;
use crate::crypto::Signer; use crate::crypto::Signer;
use crate::identity::Id; use crate::identity::Id;
use crate::node; use crate::node;
use crate::prelude::NodeId; use crate::prelude::*;
use crate::service; use crate::service;
use crate::service::message::*; use crate::service::message::*;
use crate::service::reactor::Io; use crate::service::reactor::Io;
@ -214,7 +213,7 @@ where
} }
pub fn refs_announcement(&self, id: Id) -> Message { pub fn refs_announcement(&self, id: Id) -> Message {
let refs = BTreeMap::new().into(); let refs = BoundedVec::new();
let ann = AnnouncementMessage::from(RefsAnnouncement { let ann = AnnouncementMessage::from(RefsAnnouncement {
id, id,
refs, refs,

View File

@ -226,6 +226,24 @@ impl Encode for Refs {
} }
} }
impl<A, B> Encode for (A, B)
where
A: Encode,
B: Encode,
{
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
let mut n = self.0.encode(writer)?;
n += self.1.encode(writer)?;
Ok(n)
}
}
impl Encode for git::RefString {
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
self.as_str().encode(writer)
}
}
impl Encode for Signature { impl Encode for Signature {
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> { fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
self.deref().encode(writer) self.deref().encode(writer)
@ -266,6 +284,25 @@ impl Decode for Refs {
} }
} }
impl Decode for git::RefString {
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
let ref_str = String::decode(reader)?;
git::RefString::try_from(ref_str).map_err(Error::from)
}
}
impl<A, B> Decode for (A, B)
where
A: Decode,
B: Decode,
{
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
let a = A::decode(reader)?;
let b = B::decode(reader)?;
Ok((a, b))
}
}
impl Decode for git::Oid { impl Decode for git::Oid {
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> { fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, Error> {
let len = Size::decode(reader)? as usize; let len = Size::decode(reader)? as usize;
@ -628,6 +665,7 @@ impl<R, S, W, G, H: Handshake> Iterator for Wire<R, S, W, G, H> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use qcheck;
use qcheck_macros::quickcheck; use qcheck_macros::quickcheck;
use crate::crypto::Unverified; use crate::crypto::Unverified;
@ -700,6 +738,14 @@ mod tests {
assert_eq!(deserialize::<Refs>(&serialize(&input)).unwrap(), input); assert_eq!(deserialize::<Refs>(&serialize(&input)).unwrap(), input);
} }
#[quickcheck]
fn prop_tuple(input: (String, String)) {
assert_eq!(
deserialize::<(String, String)>(&serialize(&input)).unwrap(),
input
);
}
#[quickcheck] #[quickcheck]
fn prop_signature(input: [u8; 64]) { fn prop_signature(input: [u8; 64]) {
let signature = Signature::from(input); let signature = Signature::from(input);

View File

@ -130,7 +130,7 @@ impl wire::Encode for RefsAnnouncement {
impl wire::Decode for RefsAnnouncement { impl wire::Decode for RefsAnnouncement {
fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> { fn decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
let id = Id::decode(reader)?; let id = Id::decode(reader)?;
let refs = Refs::decode(reader)?; let refs = BoundedVec::decode(reader)?;
let timestamp = Timestamp::decode(reader)?; let timestamp = Timestamp::decode(reader)?;
Ok(Self { Ok(Self {

View File

@ -15,6 +15,23 @@ use crate::storage;
use crate::storage::refs::{Refs, SignedRefs}; use crate::storage::refs::{Refs, SignedRefs};
use crate::test::storage::MockStorage; use crate::test::storage::MockStorage;
pub fn oid() -> storage::Oid {
let oid_bytes: [u8; 20] = gen(1);
storage::Oid::try_from(oid_bytes.as_slice()).unwrap()
}
pub fn refstring(len: usize) -> git::RefString {
let mut buf = Vec::<u8>::new();
for _ in 0..len {
buf.push(fastrand::u8(0x61..0x7a));
}
std::str::from_utf8(&buf)
.unwrap()
.to_string()
.try_into()
.unwrap()
}
pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> { pub fn set<T: Eq + Hash + Arbitrary>(range: impl RangeBounds<usize>) -> HashSet<T> {
let size = fastrand::usize(range); let size = fastrand::usize(range);
let mut set = HashSet::with_capacity(size); let mut set = HashSet::with_capacity(size);