Skip to content

feat: add built-in CGNAT STUN port discovery#24687

Open
qzgeek wants to merge 1 commit into
qbittorrent:masterfrom
qzgeek:feat/cgnat-stun-port-discovery
Open

feat: add built-in CGNAT STUN port discovery#24687
qzgeek wants to merge 1 commit into
qbittorrent:masterfrom
qzgeek:feat/cgnat-stun-port-discovery

Conversation

@qzgeek

@qzgeek qzgeek commented Jul 13, 2026

Copy link
Copy Markdown

Summary

Add built-in CGNAT STUN port discovery to qBittorrent, eliminating the need for UPnP/NAT-PMP on carrier-grade NAT (CGNAT) connections.

Problem

When qBittorrent runs behind CGNAT (common with Chinese ISPs like China Mobile, and increasingly common worldwide with IPv4 exhaustion), the listen port is translated to a random external port by the carrier's NAT. qBittorrent announces its listen port to trackers, but peers receive the wrong port and cannot connect — resulting in zero upload.

Solution

CGNATManager — a lightweight, built-in STUN client that:

  1. Binds a UDP socket to the same port as the TCP listener (UDP/TCP can coexist)
  2. Sends periodic RFC 5389 STUN Binding Requests to discover the CGNAT-mapped external port
  3. Emits a signal when the mapped port is discovered or changes
  4. SessionImpl connects this signal to setAnnouncePort(), which updates libtorrent's announce port

The feature is toggled via the existing "Use UPnP / NAT-PMP port forwarding" setting — no new UI checkbox needed.

Architecture

qBittorrent startup
  └─ CGNATManager initialized
       ├─ UDP bind 0.0.0.0:<listen_port>
       ├─ STUN Binding Request → stun.miwifi.com:3478
       ├─ Parse XOR-MAPPED-ADDRESS → external port
       ├─ emit mappedPortDiscovered(port)
       └─ SessionImpl::setAnnouncePort(port)
            └─ libtorrent announces correct CGNAT port to trackers

Configuration

[BitTorrent]
Session\CGNATStunServer=stun.miwifi.com   # STUN server address
Session\CGNATInterval=45000               # Probe interval (ms)

Files Changed

  • New: src/base/bittorrent/cgnatmanager.h (74 lines) — STUN client header
  • New: src/base/bittorrent/cgnatmanager.cpp (219 lines) — RFC 5389 implementation
  • Modified: src/base/CMakeLists.txt — +2 lines for new files
  • Modified: src/base/bittorrent/sessionimpl.h — +5 lines (include, members)
  • Modified: src/base/bittorrent/sessionimpl.cpp — +22 lines (init, signal, enable/disable)

Testing

Tested on Debian 13 with qBittorrent-nox behind China Mobile CGNAT:

  • STUN successfully discovers CGNAT-mapped port (e.g., 4897)
  • announce_port auto-updated to correct external port
  • Port changes detected on next STUN probe cycle
  • No interference with existing UPnP/NAT-PMP logic
  • Configurable via standard qBittorrent.conf settings

@ValeZAA

ValeZAA commented Jul 14, 2026

Copy link
Copy Markdown

Does this work for any 100.64.0.0/10 prefix? I have CGNAT here.

@LewpyUK

LewpyUK commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Personally (as I am not a Project Maintainer so I don't speak for the Project), I have huge privacy concerns around this.
It effectively "beacons" out to a server that is hosted in one fixed Country (by default, China) to perform the STUN probe.
At the very least, it has to have an option to turn it completely off for those that don't want to use it when all they want is UPnP/NAT-PMP functionality. Opt-In only, not on by default.

@ValeZAA

ValeZAA commented Jul 15, 2026

Copy link
Copy Markdown

UPnP/NAT-PMP cannot work with CGNAT. So this is the same option.

- Independent CGNATEnabled toggle (default off, decoupled from UPnP)
- UDP STUN discovers external port for uTP
- Standalone TCP proxy maintains NAT mapping + forwards inbound connections
- Fix STUN XOR-MAPPED-ADDRESS byte-order bug
- WebUI settings: enable toggle, STUN server, probe interval
- Default STUN server empty (user-configured)

Verified: 364MB upload through CGNAT proxy port on pure IPv4 Full Cone NAT.
@qzgeek
qzgeek force-pushed the feat/cgnat-stun-port-discovery branch from 4c5ae0e to 51b9a7e Compare July 15, 2026 10:07
@qzgeek

qzgeek commented Jul 15, 2026

Copy link
Copy Markdown
Author

Thank you for reviewing — you were absolutely right. Hardcoding a
public STUN server is a privacy concern, and tying it to UPnP
doesn't make sense.

I've made these changes:

  1. CGNAT now has its own CGNATEnabled toggle, default off — zero
    impact on existing users
  2. STUN server field defaults to empty — users must fill in a server
    they trust
  3. The proxy process only starts when CGNAT is explicitly enabled

Please take another look when you have time. Happy to address any
remaining concerns. Thanks!

@qzgeek

qzgeek commented Jul 15, 2026

Copy link
Copy Markdown
Author

In theory it should work. STUN is designed for NAT traversal, and
RFC 6598 CGNAT falls within STUN's scope.

On my end (China Mobile Full Cone CGNAT) it works — external peers
downloaded 364MB through the discovered port.

That said, different ISPs may handle CGNAT differently. I also found
and fixed some issues since you last looked — byte-order bug in the
STUN parser, and the TCP port mapping wasn't persistent so I added
a proxy keepalive process. Please take another look when you can.
Would be great if you could test it on your setup as well!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants