hbbr: relay legs arriving together are never paired (race in make_pair_)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- rust
- Domain
- networking
Research direction
Start in src/relay_server.rs at make_pair_ and inspect how PEERS is accessed. Run the supplied Python 3 reproduction against hbbr to establish the baseline. Done means simultaneous relay legs with the same UUID consistently pair and relay data without the delayed drop.
Written by the indexing model from the issue text.
Description
Describe the bug
make_pair_ in src/relay_server.rs looks up the waiting peer and registers a new one under two separate acquisitions of the PEERS mutex:
let mut peer = PEERS.lock().await.remove(&rf.uuid); // lock #1
if let Some(peer) = peer.as_mut() { /* pair + relay */ }
else {
PEERS.lock().await.insert(rf.uuid.clone(), Box::new(stream)); // lock #2
sleep(30.).await;
PEERS.lock().await.remove(&rf.uuid);
}
If both legs of a relay (same uuid) arrive between those two acquisitions, both miss and both take the else branch. The second insert replaces (and drops) the first stream, so neither leg is ever paired: one side is closed immediately and the other waits the full 30 s before being dropped. The client sees a failed relay connection.
Describe the environment
- Install environment: docker (official
rustdesk/rustdesk-serverimage, hbbs + hbbr,-k _,ENCRYPTED_ONLY=1) - Version in production: 1.1.14. Reproduced on current
master(a7736be) built from source, unmodified. - Clients: RustDesk 1.4.7 (Windows).
How to Reproduce the bug
- Build
hbbrfrommasterand run it on a spare port:hbbr -p 31317 -k "". - From a non-loopback address of that host (loopback connections go to hbbr's admin console), open two TCP connections, send the same
RequestRelayframe (same uuid) on both back-to-back, then send a byte on one leg and expect it on the other. Script below. - Some pairs are never relayed. Each lost uuid shows two
New relay request <uuid>lines and nogot paired.
Results, 200 simultaneous pairs per run (a control run with the two legs 0.2 s apart pairs 20/20):
| build | run 1 | run 2 |
|---|---|---|
master a7736be, unmodified |
177/200 | 176/200 |
master + patch below |
200/200 | 200/200 |
All 47 unpaired uuids in the unmodified runs show the double New relay request signature. 1.1.14 behaves the same (125/150 unmodified, 150/150 patched).
Expected behavior
Both legs of a relay are always paired, regardless of how close together they arrive.
Additional context
In production (1.1.14) we found 11 such uuids in about four months, mostly between legs with very low latency to the relay (same LAN). On the target side the RustDesk log shows create_relay requested ... uuid: <uuid> with no following Connection opened, i.e. the connection attempt silently failed.
Open PR #642 restructures this block but still does the remove and the insert under separate PEERS acquisitions, so it does not close this race.
Suggested fix: do the check and the insert under one lock.
diff --git a/src/relay_server.rs b/src/relay_server.rs
index de1a7ea..609f0cb 100644
--- a/src/relay_server.rs
+++ b/src/relay_server.rs
@@ -468,7 +468,22 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit
return;
}
if !rf.uuid.is_empty() {
- let mut peer = PEERS.lock().await.remove(&rf.uuid);
+ // Check-and-insert under one lock: with separate lock acquisitions, two
+ // legs of the same uuid arriving together can both miss and never pair.
+ let mut peers = PEERS.lock().await;
+ let mut peer = peers.remove(&rf.uuid);
+ if peer.is_none() {
+ log::info!("New relay request {} from {}", rf.uuid, addr);
+ peers.insert(rf.uuid.clone(), Box::new(stream));
+ drop(peers);
+ sleep(30.).await;
+ PEERS.lock().await.remove(&rf.uuid);
+ return;
+ }
+ drop(peers);
if let Some(peer) = peer.as_mut() {
log::info!("Relayrequest {} from {} got paired", rf.uuid, addr);
let id = format!("{}:{}", addr.ip(), addr.port());
@@ -485,11 +500,6 @@ async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limit
log::info!("Relay of {} closed", addr);
}
USAGE.write().await.remove(&id);
- } else {
- log::info!("New relay request {} from {}", rf.uuid, addr);
- PEERS.lock().await.insert(rf.uuid.clone(), Box::new(stream));
- sleep(30.).await;
- PEERS.lock().await.remove(&rf.uuid);
}
}
}
Repro script (Python 3, stdlib only)
# A/B harness for the hbbr pair race. Usage: hbbr_pair_race_test.py <port> <pairs> <gap_s> <non-loopback-host-ip>
import socket, sys, uuid, time
PORT, N, GAP, HOST = int(sys.argv[1]), int(sys.argv[2]), float(sys.argv[3]), sys.argv[4]
def frame(u):
inner = b'\x12' + bytes([len(u)]) + u # RequestRelay.uuid (field 2)
msg = b'\x92\x01' + bytes([len(inner)]) + inner # RendezvousMessage.request_relay (field 18)
return bytes([len(msg) << 2]) + msg # hbb_common 1-byte length header
ok = 0
for _ in range(N):
u = str(uuid.uuid4()).encode()
a = socket.create_connection((HOST, PORT)); b = socket.create_connection((HOST, PORT))
for s in (a, b): s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
time.sleep(0.02); f = frame(u); a.send(f)
if GAP: time.sleep(GAP)
b.send(f); time.sleep(0.1)
try:
a.send(b'ping-from-a'); b.settimeout(1); ok += (b.recv(64) == b'ping-from-a')
except Exception:
pass
a.close(); b.close(); time.sleep(0.05)
print(f'port {PORT} gap={GAP}: paired+relayed {ok}/{N}', flush=True)
Usage: python3 hbbr_pair_race_test.py <port> <pairs> <gap_seconds> <non-loopback-host-ip>, e.g. python3 hbbr_pair_race_test.py 31317 200 0 10.0.0.5.
- Dominant language
- Rust
- Stars
- 10.4k
- Forks
- 2.6k
- PR merge metrics
- No merged PRs in 30d
Getting set up
We have not checked this project's setup files yet. Start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from rustdesk/rustdesk-server
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
rustdesk/rustdesk-server#709 ·
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 55/100
rustdesk/rustdesk-server#704 · 1 comment ·
-
bug
Difficulty 3/5 1-2 days Newbie friendliness 68/100
rustdesk/rustdesk-server#701 ·
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 43/100
rustdesk/rustdesk-server#678 · 14 comments · 4 reactions ·
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 45/100
rustdesk/rustdesk-server#677 · 2 comments ·
All issues in rustdesk/rustdesk-server
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
trailofbits/dylint#2107 ·
Maintainers usually reply within 1 day
-
area:cli bug good first issue priority:medium
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
Maintainers usually reply within 1 day
-
arrays_zip with two same-named inputs fails with "ArrowArray struct has 2 children (expected 1)"Openbug requires-triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
apache/datafusion-comet#6251 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
bug false-positive harper-core linting
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Automattic/harper#4471 ·
Maintainers usually reply within 1 day