Compare commits

...

1 Commits

Author SHA1 Message Date
rustdesk
b8f2654dcf webrtc: encrypt the signalling legs to the rendezvous server
Three TCP connections carry WebRTC signalling to hbbs in the clear: the
controller's punch connection, which carries the offer up and the answer and
both sides' ICE candidates through it, and on the controlled side the
short-lived connection that returns the answer and the one that trickles its
candidates. Candidates are every interface address of both machines, and the
controller is the side most often on a network it does not trust.

Each now runs `secure_tcp` first. The controller's connection was encrypted
only with a login token or a switch code, the condition that kept old servers
without the exchange working; an offer joins that condition, since only a
server that forwards offers is ever sent one and every such server does the
exchange. The two connections on the controlled side exist only for WebRTC,
so they encrypt unconditionally. Nothing changes for a punch without an
offer.

The controller's other path, the punch and its candidates once the server
has answered over the mediator channel, stays as it is: that channel is UDP
by default and outside what `secure_tcp` covers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns
2026-09-15 12:24:25 +08:00
2 changed files with 22 additions and 2 deletions

View File

@@ -825,7 +825,14 @@ impl Client {
};
let switch_code = interface.get_switch_code();
if !key.is_empty() && (!token.is_empty() || !switch_code.is_empty()) {
// With an offer this socket also carries both sides' ICE candidates, every interface
// address of both machines, so it is encrypted whenever a key is configured. Only a
// server that forwards offers is ever sent one, and such a server does the exchange.
let carries_offer = webrtc_offerer
.as_ref()
.and_then(|g| g.stream())
.is_some();
if !key.is_empty() && (!token.is_empty() || !switch_code.is_empty() || carries_offer) {
secure_tcp(&mut socket, &key)
.await
.map_err(|e| anyhow!("Failed to secure tcp: {}", e))?;

View File

@@ -806,6 +806,7 @@ impl RendezvousMediator {
// trickle, and TCP reliability replaces the old 400ms duplicate re-send
// (the controller keeps its own re-send for the server->peer UDP downlink).
let mut conn = None;
let key = crate::get_key(true).await;
while let Some(candidate) = local_ice_rx.recv().await {
let mut msg = Message::new();
msg.set_ice_candidate(IceCandidate {
@@ -819,7 +820,18 @@ impl RendezvousMediator {
for _ in 0..2 {
if conn.is_none() {
match connect_tcp(&*host, CONNECT_TIMEOUT).await {
Ok(s) => conn = Some(s),
Ok(mut s) => {
// Candidates are every interface address of this machine;
// the server that routes them does the key exchange.
if let Err(err) = crate::secure_tcp(&mut s, &key).await {
log::warn!(
"failed to secure the WebRTC ICE candidate connection: {}",
err
);
break;
}
conn = Some(s);
}
Err(err) => {
log::warn!(
"failed to connect for WebRTC ICE candidate: {}",
@@ -993,6 +1005,7 @@ impl RendezvousMediator {
let mut msg_out = Message::new();
msg_out.set_punch_hole_sent(msg_punch);
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
crate::secure_tcp(&mut socket, &crate::get_key(true).await).await?;
socket.send(&msg_out).await?;
return Ok(());
}