fix(lib): serve uploads to downloaders we cannot dial

An accepted upload only ever dialled the downloader at the address the
server reported. Most downloaders sit behind a NAT, so the dial timed out
after 20s ("serve …: connection timed out") or, with port 0, the upload
sat in pending_serves forever. There was no fallback: an inbound
PierceFirewall was always treated as a P control connection.

Do what the reference clients do. When an upload starts, ask the server to
have the downloader connect back (ConnectToPeer type F) and dial them at
the same time; the first socket to arrive claims the transfer. Over the
pierced socket only the bare 4-byte transfer token precedes the offset
exchange. CantConnectToPeer and the brokered-connect timeout fail the
upload and free its slot; port 0 skips the dial instead of parking the job.

Covered by an e2e test where the leecher runs without a listener.

Closes #102
This commit is contained in:
Michel de Graaf
2026-10-26 17:03:23 +01:00
parent 68bc3067c3
commit 3205611609
7 changed files with 428 additions and 69 deletions
+41 -1
View File
@@ -5,7 +5,7 @@
//! of them having to be widened for the move.
use super::{
ActorSystem, Arc, AtomicUsize, BROKER_CONNECT_TIMEOUT,
ActorSystem, Arc, AtomicBool, AtomicUsize, BROKER_CONNECT_TIMEOUT,
BROWSE_PROTECT_WINDOW, ClientContext, DEFAULT_MAX_PEERS,
DEFAULT_UPLOAD_SLOTS, DownloadStatus, DownloadStore, HashMap, HashSet,
Instant, PENDING_PEER_TTL, Recommendation, RoomEvent, RoomInfo, RoomTicker,
@@ -50,6 +50,7 @@ impl ClientContext {
uploads: HashMap::new(),
active_uploads: HashMap::new(),
pending_serves: HashMap::new(),
brokered_uploads: HashMap::new(),
browse_results: HashMap::new(),
folder_contents: HashMap::new(),
peer_infos: HashMap::new(),
@@ -707,6 +708,45 @@ impl ClientContext {
.map(|(username, _)| username)
}
/// Returns the claim flag the dialling route races against.
pub(crate) fn add_brokered_upload(
&mut self,
connect_token: u32,
token: u32,
real_path: std::path::PathBuf,
) -> Arc<AtomicBool> {
let claimed = Arc::new(AtomicBool::new(false));
self.brokered_uploads.insert(
connect_token,
super::BrokeredUpload {
token,
real_path,
deadline: Instant::now() + BROKER_CONNECT_TIMEOUT,
claimed: claimed.clone(),
},
);
claimed
}
pub(crate) fn take_brokered_upload(
&mut self,
connect_token: u32,
) -> Option<super::BrokeredUpload> {
self.brokered_uploads.remove(&connect_token)
}
/// Uploads whose downloader never connected back in time.
pub(crate) fn take_expired_brokered_uploads(
&mut self,
now: Instant,
) -> Vec<super::BrokeredUpload> {
let (expired, live) = std::mem::take(&mut self.brokered_uploads)
.into_iter()
.partition(|(_, upload)| now >= upload.deadline);
self.brokered_uploads = live;
expired.into_values().collect()
}
pub fn take_expired_connects(&mut self, now: Instant) -> Vec<String> {
let mut expired = Vec::new();
self.pending_connect_tokens