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:
@@ -60,6 +60,8 @@ pub const DEFAULT_UPLOAD_SLOTS: usize = 10;
|
||||
const BROKER_CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
const DEFAULT_MAX_PEERS: usize = 512;
|
||||
/// How long a message waits for a control connection to its peer.
|
||||
const PENDING_PEER_TTL: Duration = Duration::from_mins(1);
|
||||
|
||||
const BROWSE_PROTECT_WINDOW: Duration = Duration::from_mins(5);
|
||||
|
||||
@@ -338,19 +340,23 @@ pub enum ClientOperation {
|
||||
PossibleParents(Vec<(String, String, u16)>),
|
||||
/// The server told us to drop our parent.
|
||||
ResetDistributed,
|
||||
/// A parent candidate told us how deep it sits.
|
||||
/// A parent candidate told us how deep it sits. `link` says which dial
|
||||
/// to that user is talking.
|
||||
ParentBranchLevel {
|
||||
parent: String,
|
||||
link: u64,
|
||||
level: i32,
|
||||
},
|
||||
/// A parent candidate told us whose branch it is on.
|
||||
ParentBranchRoot {
|
||||
parent: String,
|
||||
link: u64,
|
||||
root: String,
|
||||
},
|
||||
/// A search came down the tree from `parent`.
|
||||
ParentSearch {
|
||||
parent: String,
|
||||
link: u64,
|
||||
username: String,
|
||||
token: u32,
|
||||
query: String,
|
||||
@@ -358,6 +364,7 @@ pub enum ClientOperation {
|
||||
/// The link to a parent or candidate is gone.
|
||||
ParentClosed {
|
||||
parent: String,
|
||||
link: u64,
|
||||
},
|
||||
}
|
||||
pub struct ClientContext {
|
||||
@@ -380,8 +387,10 @@ pub struct ClientContext {
|
||||
pub shared_directories: Vec<String>,
|
||||
/// Peer listen addresses learned from GetPeerAddress responses.
|
||||
peer_addresses: HashMap<String, (String, u32)>,
|
||||
/// Peer messages waiting for a control connection to that peer.
|
||||
pending_peer_messages: HashMap<String, Vec<crate::message::Message>>,
|
||||
/// Peer messages waiting for a control connection to that peer, and
|
||||
/// since when: a peer that never answers must not keep them forever.
|
||||
pending_peer_messages:
|
||||
HashMap<String, (Instant, Vec<crate::message::Message>)>,
|
||||
/// Uploads we have offered, keyed by our transfer token.
|
||||
uploads: HashMap<u32, UploadJob>,
|
||||
active_uploads: HashMap<u32, ActiveUpload>,
|
||||
@@ -474,6 +483,15 @@ impl ClientContext {
|
||||
}
|
||||
|
||||
impl ClientContext {
|
||||
/// A context for the client that logs in as `username`.
|
||||
#[must_use]
|
||||
pub fn for_user(username: &str) -> Self {
|
||||
Self {
|
||||
leaf: distributed::Leaf::new(username),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
let actor_system = Arc::new(ActorSystem::new());
|
||||
@@ -725,7 +743,8 @@ impl ClientContext {
|
||||
) {
|
||||
self.pending_peer_messages
|
||||
.entry(username.to_string())
|
||||
.or_default()
|
||||
.or_insert_with(|| (Instant::now(), Vec::new()))
|
||||
.1
|
||||
.push(message);
|
||||
}
|
||||
|
||||
@@ -736,9 +755,18 @@ impl ClientContext {
|
||||
) -> Vec<crate::message::Message> {
|
||||
self.pending_peer_messages
|
||||
.remove(username)
|
||||
.map(|(_, messages)| messages)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Drop peer messages nobody could deliver within
|
||||
/// [`PENDING_PEER_TTL`]: the searcher went offline, or never existed.
|
||||
pub(crate) fn expire_pending_peer_messages(&mut self, now: Instant) {
|
||||
self.pending_peer_messages.retain(|_, (since, _)| {
|
||||
now.duration_since(*since) < PENDING_PEER_TTL
|
||||
});
|
||||
}
|
||||
|
||||
/// Store a shared-file listing received from browsing `username`.
|
||||
pub fn store_browse_result(
|
||||
&mut self,
|
||||
@@ -865,11 +893,13 @@ impl Client {
|
||||
listen_port: settings.listen_port,
|
||||
bound_port: None,
|
||||
address: settings.server_address,
|
||||
context: Arc::new(RwLock::new(ClientContext::for_user(
|
||||
&settings.username,
|
||||
))),
|
||||
username: settings.username,
|
||||
password: settings.password,
|
||||
version: settings.version,
|
||||
shared_directories: settings.shared_directories,
|
||||
context: Arc::new(RwLock::new(ClientContext::new())),
|
||||
server_handle: None,
|
||||
session: SessionWatch::default(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user