CNA-BUG-170: A joined SystemLink session answers LAN discovery for itself, so Find lists the session once per member
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
Every SystemLink session, including a joined one, registers with discovery and ReplyToQuery ignores IsHost, so Find lists a session once per member; joining through a client's entry is refused by that client instead of reaching the host.
- Identifier
CNA-BUG-170- Category
- Bug
- Subsystem
- Networking & gamer services
- Status
- Narrowed (partially fixed; describes only what survives)
- Verified against
- CNA
3b43bd86(3b43bd8687ed46523d873f9bec4b32bd8481dcb5) - Severity
- Medium (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- Microsoft::Xna::Framework::Net::NetworkSession::Find and Join for NetworkSessionType::SystemLink (ENetDiscoveryService::RegisterHost and ReplyToQuery; ENetBackend::StartHosting and HandleClientHello)
Expected behaviour
XNA's Find returns the available sessions, one entry per hosted session, and joining an entry connects to that session's host. CNA's discovery code itself describes the reply as coming from a hosting session.
Actual behaviour at TARGET
The NetworkSession constructor calls ENetBackend::StartHosting for every SystemLink session, including the one EndJoin constructs with isHost false, and StartHosting calls ENetDiscoveryService::RegisterHost(session, port) (ENetBackend.cpp). ReplyToQuery in ENetDiscoveryService.cpp answers any query with a matching type filter with the registered session's port, gamer counts and host gamertag, without checking that the session is the host. A client therefore advertises the session it joined: with host A and client B, a Find on machine C returns two entries with the same host gamertag (results are de-duplicated by connect port, and the ports differ). If C joins B's entry, B's HandleClientHello now sees that its HostPeer names another machine, logs a warning and disconnects C, so the join fails although the session C saw is open. That every peer keeps an accepting ENet host is intended (host migration reuses it); the discovery registration is what exposes it.
Source locations
modules/net/src/Xna/NetworkSession.cpp— the constructor starts hosting for every SystemLink session; EndJoin constructs with isHost falsemodules/net/src/Internal/ENetBackend.cpp— StartHosting registers with discovery; HandleClientHello refuses a hello while HostPeer names another host; the migration successor re-registersmodules/net/src/Internal/ENetDiscoveryService.cpp— RegisterHost; ReplyToQuery answers without an IsHost check; FindSessions de-duplicates by connect portmodules/net/tests/CNA/Internal/Net/ENetDiscoveryServiceTests.cpp— discovery tests register a single hostmodules/net/tests/CNA/Internal/Net/ENetBackendTests.cpp— ClientRejectsClientHelloFromRogueThirdPartyPeerOnItsOwnListeningSocketmodules/net/tests/CNA/Internal/Net/TwoProcessLoopbackTest.cpp— passes the host port through a pipe, one client
Evidence
Checked by reading at 3b43bd86; not executed. The two-entry Find result follows from the code paths named above and was not observed on a network. No test has a joined client and a third searcher: the discovery tests register one host, and the two-process test hands the host's port to one client through a pipe. Within one process only one session can exist, so the single registered-session pointer is not itself a problem.
Narrowed at c6dec91f: a83673fc5 (2026-10-14) added the host check this entry also described as missing: a client-role session refuses a ClientHello while state.HostPeer names another host, so a third machine can no longer split the session by joining a client (ENetBackendTest.ClientRejectsClientHelloFromRogueThirdPartyPeerOnItsOwnListeningSocket). The client's discovery registration and ReplyToQuery are unchanged.
Focused reproduction
// Illustrative, three machines on one LAN; not run.
// A: hosts with NetworkSession::Create(NetworkSessionType::SystemLink, 1, 8).
// B: finds A and joins it.
// C:
AvailableNetworkSessionCollection found =
NetworkSession::Find(NetworkSessionType::SystemLink, 1, NetworkSessionProperties{});
// found holds two entries with A's gamertag as host: A's port and B's port.
Current tests
ENetDiscoveryServiceTests.cpp (for example FindSessionsDiscoversRegisteredHost and ReplyToQueryOnlyAnswersWhenSessionTypeFilterMatchesTheHost) registers one host; ENetBackendTests.cpp covers the refused ClientHello; no test registers a client session and checks that it stays silent.
Regression test
A discovery test that registers a client session and expects no announcement for it.
Blast radius
LAN games with a host and at least one joined client while another machine searches: duplicate listings, and a player who picks a non-host entry is refused. Two-machine games and Local sessions are unaffected. Host migration's rediscovery also searches by host gamertag and may see these client announcements; that interaction was not analysed.
Workaround
No workaround is known.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Tutorial 97: networking
- Deep dives
- Network sessions: LAN discovery
- Known issues
- Bug index