Skip to content

Commit c0470f8

Browse files
author
Threepwood-7
committed
RUST-BUG-078: avoid plaintext downgrade after obfuscated failure
1 parent a05c962 commit c0470f8

4 files changed

Lines changed: 85 additions & 48 deletions

File tree

crates/emulebb-core/src/ed2k_sources.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ use tokio_util::sync::CancellationToken;
2929

3030
use crate::{
3131
ED2K_DOWNLOAD_KAD_SOURCE_CAP, ED2K_DOWNLOAD_KAD_SOURCE_QUIET_DELAY_MS,
32-
ED2K_DOWNLOAD_KAD_SOURCE_RETRY_DELAY_MS, ED2K_HASH_ONLY_QUERY_PREFIX,
33-
ED2K_SOURCE_OBFUSCATION_REQUIRES_CRYPT, Transfer,
32+
ED2K_DOWNLOAD_KAD_SOURCE_RETRY_DELAY_MS, ED2K_HASH_ONLY_QUERY_PREFIX, Transfer,
3433
};
3534

3635
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -520,20 +519,6 @@ pub(crate) async fn collect_kad_ed2k_sources(
520519
sources
521520
}
522521

523-
pub(crate) fn plaintext_fallback_for_obfuscated_source(
524-
source: &Ed2kFoundSource,
525-
) -> Option<Ed2kFoundSource> {
526-
let options = source.obfuscation_options?;
527-
if options & ED2K_SOURCE_OBFUSCATION_REQUIRES_CRYPT != 0 {
528-
return None;
529-
}
530-
let mut fallback = source.clone();
531-
fallback.obfuscated = false;
532-
fallback.obfuscation_options = None;
533-
fallback.user_hash = None;
534-
Some(fallback)
535-
}
536-
537522
/// Identity used to recognize and drop our own client from a learned source set
538523
/// (eMule `CDownloadQueue::CheckAndAddSource`: a source whose user-hash equals
539524
/// ours, or whose IP/client-id + port equals ours, is never added as a source).

crates/emulebb-core/src/lib.rs

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ use ed2k_sources::{
131131
global_udp_source_batch_server_attempts, global_udp_source_search_excluded_endpoint,
132132
hash_only_ed2k_search_query, kad_source_result_to_ed2k_found_source, keyword_target,
133133
manifest_has_ed2k_transfer_progress, merge_download_sources, new_direct_ed2k_source_count,
134-
plaintext_fallback_for_obfuscated_source, select_ed2k_keyword_metadata,
135-
should_adopt_hash_only_metadata_name, should_query_kad_source_supplement,
136-
should_query_server_udp_source_supplement, should_refresh_ed2k_server_sources,
137-
should_skip_no_progress_source_requery, sort_download_sources, source_endpoint_key, source_key,
134+
select_ed2k_keyword_metadata, should_adopt_hash_only_metadata_name,
135+
should_query_kad_source_supplement, should_query_server_udp_source_supplement,
136+
should_refresh_ed2k_server_sources, should_skip_no_progress_source_requery,
137+
sort_download_sources, source_endpoint_key, source_key,
138138
};
139139
#[cfg(test)]
140140
use ed2k_sources::{
@@ -5389,16 +5389,6 @@ where
53895389
peer_addr
53905390
);
53915391
last_error = Some(error);
5392-
if let Some(fallback_source) = plaintext_fallback_for_obfuscated_source(&source)
5393-
{
5394-
tracing::info!(
5395-
"ED2K direct download scheduling plaintext fallback file_hash={} peer={}:{}",
5396-
file_hash_hex,
5397-
source.ip,
5398-
source.tcp_port
5399-
);
5400-
pending_sources.push_front(fallback_source);
5401-
}
54025392
}
54035393
}
54045394

@@ -5551,8 +5541,6 @@ const ED2K_DOWNLOAD_KAD_SOURCE_QUIET_DELAY_MS: u64 = 750;
55515541
const ED2K_DOWNLOAD_SOURCE_REQUERY_ROUNDS: usize = 2;
55525542
const ED2K_DOWNLOAD_SOURCE_REQUERY_DELAY_SECS: u64 = 5;
55535543
const ED2K_DOWNLOAD_BACKGROUND_RETRY_SECS: u64 = 5;
5554-
const ED2K_SOURCE_OBFUSCATION_REQUIRES_CRYPT: u8 = 0x04;
5555-
55565544
/// Parse a REST-surface IP string (the `Upload.address` / `TransferSource.ip`
55575545
/// fields) into an `Ipv4Addr` for the ban store. Returns `None` when the value
55585546
/// is empty or not a dialable IPv4 (e.g. a LowID client-id), so the ban falls
@@ -7724,9 +7712,9 @@ mod tests {
77247712
}
77257713

77267714
#[tokio::test]
7727-
async fn direct_download_scheduler_tries_plaintext_after_optional_obfuscated_failure() {
7715+
async fn direct_download_scheduler_does_not_downgrade_failed_obfuscated_peer() {
77287716
let (transfer_runtime, secure_ident, file_hash_hex, file_name, file_size) =
7729-
completed_ed2k_transfer_runtime("emulebb-core-direct-download-plaintext-fallback")
7717+
completed_ed2k_transfer_runtime("emulebb-core-direct-download-no-plaintext-downgrade")
77307718
.await;
77317719
let file_hash: Ed2kHash = file_hash_hex.parse().unwrap();
77327720
let attempts = Arc::new(Mutex::new(Vec::new()));
@@ -7771,22 +7759,15 @@ mod tests {
77717759
.await
77727760
.unwrap();
77737761

7774-
assert!(outcome.completed);
77757762
assert_eq!(
7776-
*attempts.lock().await,
7777-
vec![(41001, true, true), (41001, false, false)]
7763+
outcome
7764+
.last_error
7765+
.as_ref()
7766+
.map(ToString::to_string)
7767+
.as_deref(),
7768+
Some("simulated obfuscated peer close")
77787769
);
7779-
}
7780-
7781-
#[test]
7782-
fn plaintext_fallback_preserves_crypt_required_sources() {
7783-
let file_hash = Ed2kHash::from_bytes([0x33; 16]);
7784-
let mut source = direct_test_source(file_hash, Ipv4Addr::new(192, 0, 2, 10), 41001);
7785-
source.obfuscated = true;
7786-
source.obfuscation_options = Some(0x07);
7787-
source.user_hash = Some([0x22; 16]);
7788-
7789-
assert!(plaintext_fallback_for_obfuscated_source(&source).is_none());
7770+
assert_eq!(*attempts.lock().await, vec![(41001, true, true)]);
77907771
}
77917772

77927773
#[test]

docs/active/INDEX.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ items.
8989
| [RUST-BUG-075](items/RUST-BUG-075.md) | Major | DONE | Preserve out-of-order requested ED2K blocks |
9090
| [RUST-BUG-076](items/RUST-BUG-076.md) | Major | DONE | Keep diag_event JSONL records line-atomic across shims |
9191
| [RUST-BUG-077](items/RUST-BUG-077.md) | Major | DONE | Persist accepted ED2K request blocks before bitmap recovery |
92+
| [RUST-BUG-078](items/RUST-BUG-078.md) | Major | DONE | Do not immediately downgrade failed obfuscated ED2K peers |
9293

9394
## Refactors (`REF`)
9495

docs/active/items/RUST-BUG-078.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
id: RUST-BUG-078
3+
title: Do not immediately downgrade failed obfuscated ED2K peers
4+
status: done
5+
priority: Major
6+
category: bug
7+
workflow: local
8+
---
9+
10+
# RUST-BUG-078: Do not immediately downgrade failed obfuscated ED2K peers
11+
12+
## Problem
13+
14+
The hide.me live-wire runs `rust-hideme-20260618T235505Z` and
15+
`rust-hideme-20260619T001217Z` stayed VPN-bound and completed transfers, but
16+
the obfuscation-enabled pass showed Rust immediately re-dialing the same ED2K
17+
peer in plaintext after an obfuscated direct-download connection failed.
18+
19+
Comparing against eMuleBB MFC showed the parity rule:
20+
21+
- `CUpDownClient::Connect` enables TCP obfuscation when the peer supports it,
22+
local obfuscation is enabled, and either the peer requests it or local
23+
preferences prefer it.
24+
- `CClientReqSocket::OnConnect` and `CClientReqSocket::Disconnect` tear down
25+
failed sockets through `CUpDownClient::Disconnected`.
26+
- The client connection path does not immediately retry the same failed
27+
obfuscated socket as plaintext. Normal source scheduling may later retry
28+
sources through its ordinary timers and source records.
29+
30+
Rust therefore must not synthesize an immediate plaintext source after a failed
31+
obfuscated attempt. A plaintext connection is still valid when an independent
32+
source record is actually plaintext.
33+
34+
## Acceptance
35+
36+
- [x] Failed obfuscated direct-download attempts do not enqueue an immediate
37+
plaintext downgrade for the same endpoint.
38+
- [x] Independent plaintext source records remain eligible through the normal
39+
source selection path.
40+
- [x] Existing direct-download retry behavior for other peers still works.
41+
- [x] Required-crypto peers are not downgraded.
42+
43+
## Implementation Notes
44+
45+
- Removed the direct scheduler's `plaintext_fallback_for_obfuscated_source`
46+
downgrade path.
47+
- Removed the helper and the obsolete required-crypto guard constant that only
48+
existed for that downgrade path.
49+
- Replaced the old fallback test with a regression test proving a failed
50+
obfuscated source is attempted once and its error is retained.
51+
52+
## Evidence
53+
54+
- Live evidence source: `rust-hideme-20260618T235505Z`,
55+
`rust-hideme-20260619T001217Z`.
56+
- Live log counts before the fix: 9 immediate plaintext fallbacks in
57+
`rust-hideme-20260618T235505Z` obfuscation-on, 19 in
58+
`rust-hideme-20260619T001217Z` obfuscation-on, and 1 in that run's
59+
obfuscation-off pass due an obfuscated source record discovered while local
60+
obfuscation was off.
61+
- Post-fix live proof: `rust-hideme-20260619T005416Z` passed, VPN-bound
62+
(`10.55.68.38`), ED2K HighID, Kad connected, packet diagnostics captured, 20
63+
downloads started, 1 completed, and `scheduling plaintext fallback` had zero
64+
matches.
65+
- Post-fix transfer store: one `Verified` piece with 839,813 bytes, no completed
66+
bitmap leftovers, and the completed transfer row marked completed.
67+
- `cargo test -p emulebb-core direct_download_scheduler_does_not_downgrade_failed_obfuscated_peer --locked`
68+
- `cargo test -p emulebb-core direct_download_scheduler --locked`
69+
- `python -m emule_workspace build clients --client emulebb-rust --diagnostics`
70+
- `python scripts/rust-live-wire-hideme.py --inputs live-wire-inputs.local.json --max-terms 3 --max-concurrent 20 --download-timeout 600 --require-packet-diagnostics --reask`

0 commit comments

Comments
 (0)