Files
ade5919be0 feat: add BEP 55 uTP hole punching (#8884)
* feat: add BEP 55 message encode/decode helpers and introducer store

Adds bep55-holepunch.cc/h with encode/decode for MsgRendezvous,
MsgConnect, and MsgError (IPv4: 12 bytes, IPv6: 24 bytes), and
bep55-introducer-store.h for tracking PEX-provenance relay candidates
per swarm.

Design notes:
- Always emits the 12/24-byte form (err_code=0 appended) even for
  non-error messages. The spec allows an 8/20-byte form without
  err_code, but libtorrent always emits 12/24 and anacrolix's decoder
  expects it. Strict spec compliance breaks interop with the majority
  of the swarm.
- Decoder accepts a zero trailing err_code on non-error messages but
  rejects a non-zero one. Accommodates libtorrent's always-12/24
  encoding without being fully permissive.
- Introducer store is owned per-swarm, not session-global. The store
  is accessed under the swarm lock; a global store would require
  additional locking we chose to avoid. Per-swarm scoping is also
  semantically correct — introducers are swarm-specific relationships.

* feat: advertise ut_holepunch in LTEP handshake and parse incoming messages

Adds the ut_holepunch advertisement to the outgoing LTEP handshake and
dispatches incoming MsgRendezvous, MsgConnect, and MsgError to peer-mgr.
Widens tr_peerMsgs with remote_ut_holepunch_id() and send_ltep_message()
so peer-mgr can orchestrate cross-peer sends for the relay role.

Design notes:
- Advertisement gated on is_public() && allowsUTP(), compiled out
  entirely without WITH_UTP. Holepunch is a uTP mechanism (simultaneous
  open requires uTP's connection-less framing); advertising without uTP
  would be misleading. Private torrents have no PEX introducer path.
- MsgError is intentionally dropped without penalising the relay. A
  misbehaving relay could forge errors to poison peer reputation;
  relying on standard connection timeouts removes that vector. Matches
  libtorrent's behaviour.
- tr_peerMsgs widened with remote_ut_holepunch_id() and
  send_ltep_message() so peer-mgr can drive cross-peer LTEP sends for
  the relay role. The relay must send MsgConnect to a different peer
  than the rendezvous sender; peer-msgs cannot reach siblings, so the
  send is orchestrated from peer-mgr. Minimal interface change that
  keeps BEP 55 state out of tr_peerMsgs.

* feat: implement relay, holepunch connect, and PEX provenance

Implements all three BEP 55 roles in peer-mgr:
- Relay: on MsgRendezvous, locate both peers and send MsgConnect to each.
- Initiator/responder: on MsgConnect, fire a uTP holepunch via
  tr_peerMgrConnectHolepunch. The role is symmetric — both sides execute
  the same code path.
- Provenance recording: track which relay introduced a target via PEX
  for future rendezvous requests.

Design notes:
- Holepunch connect hard-codes utp=true, no TCP fallback. TCP cannot
  punch through NAT (requires a listening socket on one side) and a
  fallback would wrongly mark the peer non-uTP-capable on failure.
- Holepunch failure calls on_fruitless_connection() for retry backoff
  but skips set_connectable(false). Mirrors libtorrent: a NAT traversal
  failure says nothing about reachability via direct or future holepunch.
- Only PEX-introduced relays are eligible. anacrolix allows any
  connected holepunch-capable peer; we follow libtorrent's conservative
  policy. A PEX introducer has vouched for the target, reducing relay
  amplification surface.
- Failed holepunch does not re-trigger rendezvous (was_holepunch_attempt
  guard). Without this, failure loops: rendezvous → connect → fail →
  rendezvous. Mirrors libtorrent's !m_holepunch_mode guard.
- Dual-stack relay target lookup falls back to port-only matching when
  address families mismatch, but only when exactly one peer matches.
  The single-match constraint is the safety bound: ambiguous ports are
  dropped rather than guessed.
- Relay fires MsgConnect unconditionally once it holds live connections
  to both peers — no rate limiting. Per-target rate limiting and a
  per-swarm inflight cap were both implemented and removed: holding live
  connections to both peers is itself a strong admission bound.
- is_in_use() (is_connected() || has_handshake()) guards
  initiate_connection so the normal candidate scan skips peers already
  engaged by a racing holepunch. Without this, start_handshake could be
  called with outgoing_handshake_ already set, violating
  TR_ASSERT(!outgoing_handshake_) at peer-mgr.h:285.
  tr_peerMgrConnectHolepunch has a symmetric has_handshake() check for
  the other direction.

* feat: expose holepunch capability in RPC and frontends

Adds the supports_holepunch RPC field and 'h' peer flag across GTK, Qt,
and web frontends. Documents both in rpc-spec.md and Peer-Status-Text.md.

Design notes:
- 'h' is lowercase to avoid collision with 'H' (DHT-discovered peers).
  The flag table already overloads same-letter pairs for related-but-
  distinct meanings (D/d, U/u); h/H reuses that convention for
  capability vs source.

* build: add BEP 55 source files to Xcode project

Registers bep55-holepunch.cc/h, bep55-introducer-store.h, and their
test files in Transmission.xcodeproj so the Mac app build includes the
new extension.

* fix: address BEP 55 relay review findings

- Use pointer identity (target == sender) as primary ErrNoSelf guard in
  the rendezvous relay handler; the address comparison missed inbound
  peers resolved via listen_socket_address() whose ephemeral source port
  differs from the PEX-advertised listen port
- Replace reinterpret_cast/const_cast in make_ipv4 test helper with
  htonl(); remove mislabeled NOLINT comments that were suppressing an
  unrelated warning while leaving the live const_cast warning unsuppressed
- Factor the duplicated make_ipv4 helper from both bep55 test files into
  a shared bep55-test-utils.h header

* fix: remove redundant BEP 55 self-target check

* docs: clarify BEP 55 edge cases and remove what-comments

* style: fix HeaderSize comment, remove forward decl and dead comments

* fix: fast-retry holepunch connections on failure

* fix: use fast_reconnect() to properly rewind holepunch attempt timer

Instead of setting connection_attempt_time to 0, set it to
now minus reconnect interval so the scheduler retries immediately.

* Fix: crash when socket_ is nullptr

* fix: add checks for `socket_`

* code review: default to false

* Update libtransmission/bep55-introducer-store.h

Apply tearfur suggestion so intention is clearer.

Co-authored-by: Yat Ho <lagoho7@gmail.com>

* refactor: remove unnecessary _APICOMPAT quarks for new holepunch keys

New quarks never had legacy camelCase names in any prior release,
so no backward-compat shims are needed.

* docs: clarify BEP 55 introducer store comment

* refactor: use value_if<uint8_t> for ut_holepunch LTEP parsing

Co-authored-by: Yat Ho <lagoho7@gmail.com>

* refactor: merge two loops in find_connected_peer

* refactor: pass sender by reference to tr_peerMgrHandleHolepunchRendezvous

* refactor: add holepunch allow helper

* refactor: extract ut holepunch support check

* docs: clarify advertised peer port check

* fix: prefer peer ID over address heuristic for self-rendezvous guard

When result.peer_id is available, compare it against tor->peer_id()
instead of using is_local_peer_endpoint(). Peer ID comparison is
unambiguous; the address check can miss self during the startup window
before global_address is populated asynchronously.

Falls back to is_local_peer_endpoint() when the handshake did not
reach peer ID exchange (e.g. connection dropped at TCP level).

* docs: explain ErrNoSelf follows libtorrent's target==sender semantics

BEP 55's literal wording ('target belongs to the relay') would map to an
is_local_peer_endpoint(target) check, but that is unreachable: we never
connect to ourselves (handshake.cc), so a self-endpoint target resolves to
ErrNotConnected before reaching this guard. Document that we deliberately
follow libtorrent, whose check has fired against real peers in live swarms.

* fix: bound BEP 55 holepunch fast-retry and keep it uTP-only

A failed holepunch reset is_holepunch_attempt_ before the fast retry, so
the retry went through the normal candidate path: it could degrade to TCP
(which opens no uTP NAT mapping) and, no longer flagged as a punch, sent a
fresh rendezvous on failure. That closed a rendezvous->connect->fail loop.

Keep the punch sticky across a bounded number of fast retries instead.
While under the cap the retry stays uTP-forced (routed through
tr_peerMgrConnectHolepunch) and keeps skipping the rendezvous; past the
cap we give up and fall back to normal backoff. The counter resets on a
successful connect. MaxHolepunchFastRetries mirrors libtorrent's effective
fast_reconnect budget.

* fix: remove incomplete BEP 55 dual-stack relay fallback

* refactor: subsume is_holepunch_attempt_ into holepunch_retries_

Encode holepunch mode in the existing counter: 0 = inactive, >=1 = active.
Removes is_holepunch_attempt_ and the fast_reconnect() timestamp hack.

* Simplify find_connected_peer

* refactor: remove unused overload

* refactor: self-documenting bep55 constants

* refactor: decode takes BufferReader

* refactor: encode takes BufferWriter

* refactor: use std::span

* chore: housekeeping

* refactor: tidy up find_connected_peer

* refactor: tidy up peer_info holepunch bookkeeping

- Removed `set_holepunch_attempt()`
- Updated logic in `on_handshake_done()` and `tr_peerMgrConnectHolepunch()` to reflect updated bookkeeping

* refactor: set utp supported if holepunch connect succeeded

* refactor: remove `tr_peerMgrFindHolepunchIntroducer()` from external linkage

* fix: incorrect fast retry check

* fix: check holepunch id instead of pex flags

Even if the peer itself did not advertise its holepunch ID in the LTEP handshake, other peers might falsely advertise this peer supports holepunch in PEX flags.

* fix: always increment holepunch attempt when trying holepunch connect

* fix: don't mark peer as not connectible if failed to create io

Failing to create a uTP peer io object does not rule out TCP connectivity to the peer.

* fix: clarify relay endpoint comment in tr_peerMgrHandleHolepunchRendezvous

* fix: mark fresh BEP 55 peers as holepunch attempts

* fix: clarify BEP 55 connected peer lookup comment

* Remove unrelated formatting changes

* Sort BEP 55 source entries

* Reject trailing bytes in BEP 55 errors

* docs: add BEP 55 RPC changelog entries

* Use named error code when decoding holepunch messages

Co-authored-by: Yat Ho <lagoho7@gmail.com>

---------

Co-authored-by: Cœur <coeur@gmx.fr>
Co-authored-by: Yat Ho <lagoho7@gmail.com>
2026-08-23 17:46:41 -05:00
..
2022-10-11 18:39:41 +03:00
2022-04-26 09:11:44 -05:00
2025-12-18 07:39:25 -06:00

VOLUNTEERS WANTED

   - Qt developers and translators are needed
   - If you find a bug, please report it at https://github.com/transmission/transmission

ABOUT TRANSMISSION-QT

   Transmission-qt is a GUI for Transmission loosely based on the GTK+ client.

   This is the only Transmission client that can act as its own self-contained
   session (as the GTK+ and Mac clients do), and can also connect to a remote
   session (as the web client and transmission-remote terminal client do).

   Use Case 1: If you like to run BitTorrent for awhile from your desktop,
   then the Mac, GTK+, and Qt clients are a good match.

   Use Case 2: If you like to leave BitTorrent running nonstop on your
   computer or router, and want to control it from your desktop or
   from a remote site, then transmission-remote and the web and Qt clients
   are a good match.

   To use the Qt client as a remote, in the menu go to Edit > Change Session

   The Qt client is also the most likely to wind up running on Windows,
   though that's not a high priority at the moment...

BUILDING ON WINDOWS

   rb07 has a writeup of this on the Transmission wiki:
   https://trac.transmissionbt.com/wiki/BuildingTransmissionQtWindows

BUILDING ON MACOS

   nnc has a writeup of this on the Transmission wiki:
   https://trac.transmissionbt.com/wiki/BuildingTransmissionQtMac

BUILDING ON UNIX

   1. Prerequisites: Qt >= 4.8 and its development packages
   2. Build Transmission as normal
   3. In the qt/ directory, type "qmake qtr.pro" (or "qmake-qt4 qtr.pro", or "qmake-qt5 qtr.pro")
   4. In the qt/ directory, type "make"
   5. In the qt/ directory, as root, type "INSTALL_ROOT=/usr make install"
      (Feel free to replace /usr with /usr/local or /opt or whatever)