fix(lib): harden the distributed leaf against hostile and slow parents
Review of the leaf found the seams a stranger on the other end of a D connection could pull at, and a few that a slow one would. - A code-93 envelope is unwrapped once, as Nicotine+ does; a frame made of nothing but envelopes used to recurse once per byte. - A branch level is validated before it is used: negative levels leave the candidate unadoptable and the largest one no longer overflows. - The link reads through the crate's MessageReader, so a frame split across a read timeout is no longer torn or mistaken for a closed link. - Every event a link raises carries its link number, so a stale thread cannot adopt, close or move a newer link to the same user. - Later branch facts from the parent move our own place and are told to the server; a fresh login resets the leaf, whose stance is now the one place the four announcement messages come from. - Candidates from a later batch join the ones in flight instead of cancelling them, capped at ten; searches from the parent are budgeted at fifty a second; a reply queued for a searcher the server cannot place expires after a minute. - Truncated frames and half-read PossibleParents entries are dropped instead of yielding an empty root, a level of zero or a phantom candidate. The e2e now has a searcher of its own, so the answer must go to whoever asked and not to the parent that relayed it.
This commit is contained in:
@@ -34,15 +34,27 @@ pub fn parse(message: &mut Message) -> Option<Distributed> {
|
||||
parse_at(message, message.get_init_code())
|
||||
}
|
||||
|
||||
/// Decode the body that follows a one-byte `code`, the pointer already past it.
|
||||
/// Decode the body that follows a one-byte `code`, the pointer already past
|
||||
/// it. A code-93 envelope is unwrapped exactly once, as Nicotine+ does: an
|
||||
/// envelope inside an envelope is nobody's search.
|
||||
#[must_use]
|
||||
pub fn parse_at(message: &mut Message, code: u8) -> Option<Distributed> {
|
||||
let code = if code == 93 {
|
||||
message.read_int8()
|
||||
} else {
|
||||
code
|
||||
};
|
||||
match code {
|
||||
3 => {
|
||||
if message.read_int32() != SEARCH_IDENTIFIER {
|
||||
if remaining(message) < 4
|
||||
|| message.read_int32() != SEARCH_IDENTIFIER
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let username = message.read_string();
|
||||
if remaining(message) < 4 {
|
||||
return None;
|
||||
}
|
||||
let token = message.read_int32();
|
||||
let query = message.read_string();
|
||||
(!username.is_empty() && !query.is_empty()).then_some(
|
||||
@@ -53,17 +65,20 @@ pub fn parse_at(message: &mut Message, code: u8) -> Option<Distributed> {
|
||||
},
|
||||
)
|
||||
}
|
||||
4 => Some(Distributed::BranchLevel(message.read_int32() as i32)),
|
||||
5 => Some(Distributed::BranchRoot(message.read_string())),
|
||||
// Older branch roots wrap a search in a code-93 envelope.
|
||||
93 => {
|
||||
let inner = message.read_int8();
|
||||
parse_at(message, inner)
|
||||
4 => (remaining(message) >= 4)
|
||||
.then(|| Distributed::BranchLevel(message.read_int32() as i32)),
|
||||
5 => {
|
||||
let root = message.read_string();
|
||||
(!root.is_empty()).then_some(Distributed::BranchRoot(root))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn remaining(message: &mut Message) -> usize {
|
||||
message.get_size().saturating_sub(message.get_pointer())
|
||||
}
|
||||
|
||||
/// A distributed frame with the one-byte `code`: what a parent (or a test
|
||||
/// standing in for one) sends down the tree.
|
||||
fn framed(code: u8, body: impl FnOnce(&mut Message)) -> Message {
|
||||
@@ -171,5 +186,59 @@ mod tests {
|
||||
assert_eq!(parse(&mut ping), None);
|
||||
let mut short = Message::new_with_data(vec![1, 0, 0, 0, 3]);
|
||||
assert_eq!(parse(&mut short), None);
|
||||
let mut level = Message::new_with_data(vec![3, 0, 0, 0, 4, 1, 0]);
|
||||
assert_eq!(parse(&mut level), None, "a level needs four bytes");
|
||||
let mut root =
|
||||
Message::new_with_data(build_branch_root("").get_buffer());
|
||||
assert_eq!(parse(&mut root), None, "an empty root is no root");
|
||||
let mut blank =
|
||||
Message::new_with_data(build_search("seeker", 1, "").get_buffer());
|
||||
assert_eq!(parse(&mut blank), None, "an empty query is no search");
|
||||
}
|
||||
|
||||
// An envelope inside an envelope is unwrapped once and found empty; a
|
||||
// frame that is nothing but envelopes must not recurse its way off the
|
||||
// stack.
|
||||
#[test]
|
||||
fn a_nested_envelope_is_not_unwrapped_twice() {
|
||||
let mut nested = Message::new();
|
||||
nested.write_int8(93).write_raw_bytes(
|
||||
Message::new()
|
||||
.write_int8(93)
|
||||
.write_raw_bytes(build_search("seeker", 5, "q").get_data())
|
||||
.get_data(),
|
||||
);
|
||||
let mut message = Message::new_with_data(nested.get_buffer());
|
||||
assert_eq!(parse(&mut message), None);
|
||||
|
||||
let mut onions = Message::new_with_data(
|
||||
Message::new()
|
||||
.write_raw_bytes(vec![93u8; 64 * 1024])
|
||||
.get_buffer(),
|
||||
);
|
||||
assert_eq!(parse(&mut onions), None);
|
||||
}
|
||||
|
||||
// The frame as the protocol documents it, byte for byte: code 3, the
|
||||
// constant 49, then user, token and query.
|
||||
#[test]
|
||||
fn a_search_frame_matches_the_documented_layout() {
|
||||
let mut bytes = vec![3u8, 49, 0, 0, 0];
|
||||
bytes.extend(2u32.to_le_bytes());
|
||||
bytes.extend(b"me");
|
||||
bytes.extend(9u32.to_le_bytes());
|
||||
bytes.extend(1u32.to_le_bytes());
|
||||
bytes.extend(b"q");
|
||||
let mut framed = (bytes.len() as u32).to_le_bytes().to_vec();
|
||||
framed.extend(&bytes);
|
||||
assert_eq!(build_search("me", 9, "q").get_buffer(), framed);
|
||||
assert_eq!(
|
||||
parse(&mut Message::new_with_data(framed)),
|
||||
Some(Distributed::Search {
|
||||
username: "me".to_string(),
|
||||
token: 9,
|
||||
query: "q".to_string()
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user