106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
"""Minimal RustDesk rendezvous protocol encoder/probe (stdlib only, zero deps).
|
|
|
|
hbbs speaks `RendezvousMessage` protobuf over UDP :21116 (one message per datagram, no
|
|
framing). The wire format is hand-encoded here from the upstream proto
|
|
(rustdesk/hbb_common protos/rendezvous.proto — field numbers are IDENTICAL between the
|
|
1.1.12-vendored and the current shared-proto era, verified against both tags):
|
|
|
|
message RendezvousMessage {
|
|
oneof union {
|
|
RegisterPeer register_peer = 6;
|
|
RegisterPeerResponse register_peer_response = 7;
|
|
PunchHoleRequest punch_hole_request = 8;
|
|
PunchHoleResponse punch_hole_response = 11;
|
|
...
|
|
}
|
|
}
|
|
message RegisterPeer { string id = 1; int32 serial = 2; }
|
|
message PunchHoleRequest { string id = 1; ... }
|
|
|
|
This is the surface a real RustDesk client uses to register and request connections; it is
|
|
the app's own interface (STYLE.md §1), exercised from the cc-ci host against the
|
|
host-published :21116 (the stock compose publishes 21115-21119 mode:host).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import socket
|
|
import time
|
|
|
|
RENDEZVOUS_PORT = 21116
|
|
|
|
|
|
def varint(n: int) -> bytes:
|
|
out = bytearray()
|
|
while n > 0x7F:
|
|
out.append((n & 0x7F) | 0x80)
|
|
n >>= 7
|
|
out.append(n)
|
|
return bytes(out)
|
|
|
|
|
|
def field_str(field: int, s: str) -> bytes:
|
|
b = s.encode()
|
|
return varint((field << 3) | 2) + varint(len(b)) + b
|
|
|
|
|
|
def field_varint(field: int, n: int) -> bytes:
|
|
return varint((field << 3) | 0) + varint(n)
|
|
|
|
|
|
def wrap(union_field: int, inner: bytes) -> bytes:
|
|
"""Wrap an inner message as the given oneof field of RendezvousMessage."""
|
|
return varint((union_field << 3) | 2) + varint(len(inner)) + inner
|
|
|
|
|
|
def register_peer(peer_id: str) -> bytes:
|
|
"""RendezvousMessage{ register_peer { id, serial=0 } } — a plain peer registration."""
|
|
return wrap(6, field_str(1, peer_id) + field_varint(2, 0))
|
|
|
|
|
|
def punch_hole_request(target_id: str) -> bytes:
|
|
"""RendezvousMessage{ punch_hole_request { id } } — an UNAUTHENTICATED connection request
|
|
for `target_id` (no licence_key/token — exactly the spoofable request shape from the
|
|
CVE-2026-30784 / GH #670 amplification report)."""
|
|
return wrap(8, field_str(1, target_id))
|
|
|
|
|
|
def parse_union_field(data: bytes) -> int | None:
|
|
"""Which RendezvousMessage oneof field the server answered with (None if unparseable)."""
|
|
if not data:
|
|
return None
|
|
key = data[0]
|
|
field, wire = key >> 3, key & 0x7
|
|
return field if wire == 2 else field # field number regardless of wire type
|
|
|
|
|
|
def udp_probe(
|
|
msg: bytes, host: str = "127.0.0.1", port: int = RENDEZVOUS_PORT, wait: float = 2.0
|
|
) -> bytes | None:
|
|
"""Send one datagram, return the server's reply (or None after `wait` seconds of silence)."""
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
s.settimeout(wait)
|
|
try:
|
|
s.sendto(msg, (host, port))
|
|
try:
|
|
data, _ = s.recvfrom(65535)
|
|
return data
|
|
except TimeoutError:
|
|
return None
|
|
finally:
|
|
s.close()
|
|
|
|
|
|
def retry_register_peer(peer_id: str, attempts: int = 6, interval: float = 5.0) -> bytes | None:
|
|
"""Poll register_peer until the server answers (post-deploy settle), or give up.
|
|
|
|
Returns the raw register_peer_response datagram."""
|
|
deadline = time.time() + attempts * interval
|
|
last: bytes | None = None
|
|
while time.time() < deadline:
|
|
last = udp_probe(register_peer(peer_id))
|
|
if last is not None:
|
|
return last
|
|
time.sleep(interval)
|
|
return last
|