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:
@@ -183,12 +183,10 @@ pub struct ServerActor {
|
||||
shared_folder_count: u32,
|
||||
shared_file_count: u32,
|
||||
session: SessionWatch,
|
||||
/// The name we logged in as, once a login has been asked for.
|
||||
username: String,
|
||||
}
|
||||
|
||||
/// The messages a client sends right after a successful login: its shared-file
|
||||
/// counts, its distributed-network stance, online status, and (when listening)
|
||||
/// counts, online status, and (when listening)
|
||||
/// the port peers should connect to. Kept as a free function so it can be
|
||||
/// tested without a live connection.
|
||||
fn post_login_messages(
|
||||
@@ -196,7 +194,6 @@ fn post_login_messages(
|
||||
listen_port: u16,
|
||||
shared_folders: u32,
|
||||
shared_files: u32,
|
||||
own_username: &str,
|
||||
) -> Vec<Message> {
|
||||
let mut messages = vec![
|
||||
MessageFactory::build_shared_folders_message(
|
||||
@@ -205,19 +202,12 @@ fn post_login_messages(
|
||||
),
|
||||
MessageFactory::build_set_status_message(2),
|
||||
];
|
||||
messages.splice(1..1, parentless_stance(own_username));
|
||||
if enable_listen {
|
||||
messages.push(MessageFactory::build_set_wait_port_message(listen_port));
|
||||
}
|
||||
messages
|
||||
}
|
||||
|
||||
/// What a leaf tells the server while it has no parent: it is its own branch
|
||||
/// root at level 0.
|
||||
fn parentless_stance(own_username: &str) -> Vec<Message> {
|
||||
crate::message::distributed::stance(own_username, 0, false)
|
||||
}
|
||||
|
||||
impl ServerActor {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
@@ -245,7 +235,6 @@ impl ServerActor {
|
||||
shared_folder_count,
|
||||
shared_file_count,
|
||||
session: SessionWatch::default(),
|
||||
username: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,11 +568,13 @@ impl ServerActor {
|
||||
self.listen_port,
|
||||
self.shared_folder_count,
|
||||
self.shared_file_count,
|
||||
&self.username,
|
||||
) {
|
||||
self.send_message(msg);
|
||||
}
|
||||
self.session.clear();
|
||||
// The distributed stance is the leaf's to announce, and a new
|
||||
// session starts without a parent.
|
||||
self.forward_to_client(ClientOperation::ResetDistributed);
|
||||
}
|
||||
match self.context.write_safe() {
|
||||
Ok(mut ctx) => ctx.logged_in = Some(message),
|
||||
@@ -651,7 +642,6 @@ impl ServerActor {
|
||||
version: ClientVersion,
|
||||
response: std::sync::mpsc::Sender<Result<bool, SoulseekRs>>,
|
||||
) {
|
||||
self.username.clone_from(&username);
|
||||
if self.stream.is_none() && !self.initiate_connection() {
|
||||
let _ = response.send(Err(SoulseekRs::NotConnected));
|
||||
return;
|
||||
|
||||
@@ -193,18 +193,10 @@ fn a_successful_login_marks_the_session_live_again() {
|
||||
|
||||
#[test]
|
||||
fn post_login_messages_carry_counts_and_conditional_wait_port() {
|
||||
let messages = post_login_messages(true, 4321, 3, 7, "me");
|
||||
let messages = post_login_messages(true, 4321, 3, 7);
|
||||
let codes: Vec<u32> = messages.iter().map(code_of).collect();
|
||||
// SharedFolders; the distributed stance HaveNoParent, BranchRoot,
|
||||
// BranchLevel, AcceptChildren; SetStatus; SetWaitPort.
|
||||
assert_eq!(codes, vec![35, 71, 127, 126, 100, 28, 2]);
|
||||
// A leaf with no parent is its own branch root at level 0, and it
|
||||
// declines children.
|
||||
let mut root = messages[2].clone();
|
||||
root.set_pointer(4);
|
||||
assert_eq!(root.read_string(), "me");
|
||||
assert_eq!(&messages[3].get_data()[4..8], &0u32.to_le_bytes());
|
||||
assert_eq!(messages[4].get_data()[4], 0);
|
||||
// SharedFolders; SetStatus; SetWaitPort.
|
||||
assert_eq!(codes, vec![35, 28, 2]);
|
||||
|
||||
// The SharedFolders message (code 35) carries the real counts.
|
||||
let shared = messages[0].get_data();
|
||||
@@ -212,7 +204,7 @@ fn post_login_messages_carry_counts_and_conditional_wait_port() {
|
||||
assert_eq!(u32::from_le_bytes(shared[8..12].try_into().unwrap()), 7);
|
||||
|
||||
// Not listening omits SetWaitPort (code 2).
|
||||
let no_listen = post_login_messages(false, 4321, 3, 7, "me");
|
||||
let no_listen = post_login_messages(false, 4321, 3, 7);
|
||||
let codes: Vec<u32> = no_listen.iter().map(code_of).collect();
|
||||
assert_eq!(codes, vec![35, 71, 127, 126, 100, 28]);
|
||||
assert_eq!(codes, vec![35, 28]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user