CNA-BUG-170: A joined SystemLink session answers LAN discovery for itself and handles ClientHello as if it were the host
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 009d40f5; 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 and joining through a client's entry attaches to that client instead of the host.
- Identifier
CNA-BUG-170- Category
- Bug
- Subsystem
- Networking & gamer services
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - 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 backend receives C's ClientHello and processes it in HandleClientHello, which has no host check (its comment reads "We are the host handling an incoming ClientHello"): B assigns wire ids from its own table, sends C a ServerWelcome and adds C's gamers to B's session only. A never learns of C. That every peer keeps an accepting ENet host is intended (host migration reuses it); the discovery registration and the missing host check are what expose 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; HandleConnect; HandleClientHello has no host check; 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/TwoProcessLoopbackTest.cpp— passes the host port through a pipe, one client
Evidence
Checked by reading at 009d40f5; not executed. The two-entry Find result and the split roster follow from the code paths named above and were 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.
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 and TwoProcessLoopbackTest.cpp connect clients by port.
Regression test
A discovery test that registers a client session and expects no announcement, and a backend test in which a client's ENet host receives a ClientHello and disconnects the peer instead of welcoming it. Both run in one process.
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 ends up in a split session whose roster and packets reach only that client. 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