Skip to content

fix: Windows same-host mDNS discovery + surface received mDNS queries in diagnostics - #372

Merged
jonasbark merged 1 commit into
6.3from
fix/mdns-windows-shared-port-qu
Jul 30, 2026
Merged

fix: Windows same-host mDNS discovery + surface received mDNS queries in diagnostics#372
jonasbark merged 1 commit into
6.3from
fix/mdns-windows-shared-port-qu

Conversation

@jonasbark

Copy link
Copy Markdown
Collaborator

Problem

Trainer apps on the same Windows machine as BikeControl frequently never see the advertisement, while the identical advertisement is found without trouble from another machine. Roughly two dozen reports.

Root cause (prop side)

Our mDNS responder answered a QU (unicast-response) question by unicast only. On Windows both processes hold 0.0.0.0:5353 through SO_REUSEADDR, and Microsoft documents that delivery to such a shared port is non-deterministic — "the application will be unable to determine which of the two sockets received specific packets sent to the 'shared' port" — with one documented exception: "If two sockets are bound to the same interface and port and are members of the same multicast group, data will be delivered to both sockets."

So the answer was a coin flip. macOS is unaffected because one daemon owns 5353 and fans answers out to every Bonjour client. It also explains the field workaround of running tnc <host>.local first: that elicits a plain QM query, whose multicast answer every socket in the group receives.

RFC 6762 §5.4 already prefers multicast here, so we were violating a SHOULD in the one way that is fatal on Windows.

Fix lives in OpenBikeControl/prop#1; this PR bumps the submodule.

This repo

Adds the "mDNS queries received" section to the diagnostics report, listing each query the responder saw with its source, the QU bit, and whether the reply went out unicast, multicast, both or not at all.

This is the missing half of diagnosing "the trainer app on this machine can't find BikeControl". A same-host querier shows up with the advertised address as its source, so the report now distinguishes three very different situations:

What the log shows Diagnosis
no entries at all the app never queried — failure is upstream of our responder
PTR/SRV queries it's browsing; check whether we answered
A bikecontrol-xxxx.local queries browse works, the resolve step is failing

Empty on iOS, where the OS responder handles queries and never reports them to us.

Still unconfirmed

Whether the trainer app's Windows browse actually sets QU, and whether its failing step is the browse or the resolve. A separate report implicates Tailscale's DNS override, which breaks .local through GetAddrInfo; INSTRUCTIONS_WINDOWS_IPV6.md fits that side too. The new query log is what settles it — left that file untouched until a reporter's log comes back.

Tests

  • prop: 483/483 pass, flutter analyze lib/mdns clean.
  • app: 2 new diagnostics tests pass. Remaining full-suite failures are baseline, verified by stashing both repos (baseline −26): screenshot_test.dart fails 26/32 in isolation, and the snapshot tests fail on the known MissingPluginException ... com.llfbandit.app_links/events co-run flake. Neither references any changed code.

Not included

No CHANGELOG.md entry — 6.3.0 is already cut and I didn't want to presume a 6.4 heading or a patch note.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PanTG9kRBi4QTQ2psvpTxP

Adds the "mDNS queries received" section to the diagnostics report, listing
each query the responder saw with its source, the QU bit and whether the reply
went out unicast, multicast, both or not at all.

This is the missing half of diagnosing "the trainer app on this machine can't
find BikeControl". A same-host querier appears with the advertised address as
its source, so the report now distinguishes three very different situations:
the app never queried us at all, it queried for the service (PTR/SRV — the
browse step), or it queried for our hostname (A — the resolve step). Empty on
iOS, where the OS responder handles queries and never reports them to us.

Picks up the prop-side fix for the Windows shared-port QU delivery problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PanTG9kRBi4QTQ2psvpTxP
Copilot AI review requested due to automatic review settings July 30, 2026 11:23
@jonasbark
jonasbark merged commit 51f03db into 6.3 Jul 30, 2026
1 check passed
@jonasbark
jonasbark deleted the fix/mdns-windows-shared-port-qu branch July 30, 2026 11:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds responder-side visibility into incoming mDNS queries to help diagnose same-host (Windows) discovery failures, and validates the new diagnostics output via tests.

Changes:

  • Extend DebugDiagnostics to capture and render recent mDNS queries (source, QU/QM, questions, and reply mode).
  • Populate recentQueries from the responder backend during diagnostics gathering.
  • Add tests covering query rendering and the empty-log case.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/services/debug_diagnostics.dart Adds recentQueries to diagnostics, populates it when running the responder backend, and renders a new “mDNS queries received” section.
test/services/debug_diagnostics_test.dart Adds unit tests ensuring the new diagnostics section renders query entries and handles empty logs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +180 to +191
b.writeln(' mDNS queries received:');
if (recentQueries.isEmpty) {
b.writeln(' (none)');
} else {
for (final q in recentQueries) {
final at = q.at.toIso8601String().split('T').last.split('.').first;
b.writeln(
' $at ${q.source}:${q.sourcePort} ${q.wantsUnicast ? 'QU' : 'QM'} '
'${q.questions.join(', ')} → ${q.reply}',
);
}
}
Comment on lines +117 to +122
test('says so when nothing has queried us', () {
// The decisive line for "the trainer app on this machine cannot see
// BikeControl": if no query ever arrived, the problem is upstream of our
// responder, not in how we answer.
expect(withQueries(const []).toText(), contains('(none)'));
});
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.

2 participants