Skip to content

feat(mdns): advertise _jetkvm._tcp via Bonjour/DNS-SD#1454

Draft
mcuelenaere wants to merge 1 commit into
jetkvm:devfrom
mcuelenaere:claude/keen-hodgkin-0657ba
Draft

feat(mdns): advertise _jetkvm._tcp via Bonjour/DNS-SD#1454
mcuelenaere wants to merge 1 commit into
jetkvm:devfrom
mcuelenaere:claude/keen-hodgkin-0657ba

Conversation

@mcuelenaere
Copy link
Copy Markdown
Contributor

@mcuelenaere mcuelenaere commented May 8, 2026

Summary

JetKVM devices now advertise themselves on the local network as the DNS-SD service type _jetkvm._tcp, so Bonjour browsers can discover every device on a LAN. Today the responder only answers A/AAAA for jetkvm.local, so PTR queries for the service type return nothing.

Important

Draft — blocked on pion/mdns#277.
pion/mdns gained DNS-SD service advertising, but ServiceInstance.Text used an unexported element type, so callers can't set TXT records. #277 exports TXTEntry + NewTXTEntry/NewTXTFlag. Until it merges and ships, go.mod pins pion/mdns/v2 to a fork branch via replace. Once released: drop the replace and bump pion/mdns/v2.

What's advertised

  • Instance: the device hostname (e.g. jetkvm-abc123)
  • Host: jetkvm-abc123.local. — existing local-name resolution preserved
  • Port: 80, or 443 when TLS is enabled
  • TXT:
    • version=builtAppVersion
    • id=GetDeviceID()
    • setup=true|false — flips after the user finishes POST /device/setup

Implementation

Uses pion/mdns/v2, which is already in the tree transitively via pion/webrtc. pion takes the IPv4 and IPv6 multicast packet conns as separate args, so the existing MDNSMode=ipv4_only/ipv6_only config keeps working — we just don't bind the disabled family. No second mDNS library, no custom responder. The responder is switched from the legacy Server() (A/AAAA only) to NewServer() so PTR/SRV/TXT are answered when a service is configured.

The diff against internal/mdns is purely additive: the existing A/AAAA path, packet-conn setup, and locking are untouched; the change adds an MDNSService option that maps to pion's WithService.

Lifecycle

  • Starts on the first networkStateChanged event (existing behavior).
  • Refreshes after handleSetup so the setup= TXT flips promptly.
  • Refreshes after setTLSState so the advertised port follows TLS.
  • SIGINT/SIGTERM calls mDNS.Stop(), which closes the multicast sockets. Browsers age the entry out via record TTL (pion does not emit DNS-SD goodbye packets).

Checklist

  • Ran make test_e2e locally and passed
  • One problem per PR (no unrelated changes)
  • Lints pass (go vet ./... green via the buildkit container)

Test plan (pending hardware, after the fork dep merges)

  • dns-sd -B _jetkvm._tcp (macOS) shows the device with the configured hostname, port, and TXT.
  • avahi-browse -r _jetkvm._tcp (Linux) shows the same.
  • Two devices on the same LAN appear as separate entries.
  • MDNSMode=ipv4_only / ipv6_only advertises on that family only; disabled advertises nothing.
  • POST /device/setup flips TXT setup from false to true on the next browse.
  • dig @224.0.0.251 -p 5353 jetkvm.local still resolves.

🤖 Generated with Claude Code

Comment thread internal/mdns/mdns.go Outdated
Comment thread internal/mdns/mdns.go Outdated
mcuelenaere added a commit to mcuelenaere/kvm that referenced this pull request May 9, 2026
Cursorbot review on jetkvm#1454 flagged two issues:

1. hashicorp/mdns.NewServer always binds both 224.0.0.251:5353 and
   [ff02::fb]:5353 with no toggle, so MDNSMode=ipv4_only/ipv6_only
   was silently ignored — clients on the disabled family could still
   browse and discover a service whose A/AAAA they cannot resolve.
   Replace the call with a small custom responder that only binds
   the requested transports. Record generation still uses
   hashicorp/mdns.MDNSService via the existing jetkvmZone, and the
   query/response loop mirrors hashicorp/mdns to keep on-the-wire
   behavior identical otherwise.

2. DefaultAddressIPv4 / DefaultAddressIPv6 were leftover unused
   exports from the pion era. Removed.

Also corrected misleading comments that claimed Stop() emits DNS-SD
goodbye packets — neither hashicorp/mdns nor this responder do; we
rely on TTL expiry like before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread internal/mdns/server.go Outdated
Comment thread internal/mdns/server.go Outdated
Comment thread internal/mdns/server.go Outdated
Comment thread internal/mdns/server.go Outdated
@mcuelenaere mcuelenaere marked this pull request as draft May 9, 2026 00:39
Comment thread internal/mdns/mdns.go Outdated
Comment thread internal/mdns/mdns.go
Comment thread mdns.go
Comment thread internal/mdns/mdns.go Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 846b975. Configure here.

Comment thread internal/mdns/mdns.go Outdated
mcuelenaere added a commit to mcuelenaere/kvm that referenced this pull request May 11, 2026
Cursorbot caught a real race I missed: SetOptions was acquiring the
lock, updating fields, releasing the lock, then calling Restart()
which re-acquires the same lock inside start(true). Between the
unlock and the re-lock, a concurrent Stop() (e.g. SIGINT during a
networkStateChanged) could shut the server down and set
m.server = nil, after which the pending Restart() would build a
fresh server — silently undoing the Stop().

Extract startLocked() as the body of start() assuming the caller
already holds m.lock, and have SetOptions hold the lock continuously
across the field update and the restart. Public Start() / Restart()
still go through start() which acquires the lock as before.

Cursor: jetkvm#1454 (comment)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@mcuelenaere mcuelenaere marked this pull request as ready for review May 12, 2026 20:20
@mcuelenaere
Copy link
Copy Markdown
Contributor Author

I'm moving this PR back to draft: an easier way forward should be to wait for pion/mdns#277 to get merged and then just build on top of that

@mcuelenaere mcuelenaere marked this pull request as draft May 27, 2026 13:13
JetKVM devices now advertise themselves as the DNS-SD service type
`_jetkvm._tcp` on the local network, so macOS/iOS clients can discover
every device on the LAN via
NWBrowser(for: .bonjour(type: "_jetkvm._tcp", domain: nil)), and
`dns-sd -B _jetkvm._tcp` / `avahi-browse -r _jetkvm._tcp` work too.

The advertised record carries:
  - instance: the device hostname (e.g. jetkvm-abc123)
  - host:     <hostname>.local (existing A/AAAA resolution preserved)
  - port:     80, or 443 when TLS is enabled
  - TXT:      version=<fw>, id=<deviceId>, setup=<true|false>

Implementation uses pion/mdns/v2 (already in the tree transitively via
pion/webrtc). pion takes the IPv4 and IPv6 multicast packet conns
separately, so the existing MDNSMode=ipv4_only / ipv6_only config is
honored by simply not binding the disabled family — no custom
responder, no second mDNS library. We switch from the legacy Server()
(A/AAAA only) to NewServer() so PTR/SRV/TXT are answered too.

Lifecycle:
  - starts on the first networkStateChanged once the network is up
  - refreshes after device setup completes so the `setup` TXT flips
  - refreshes after a TLS mode change so the advertised port follows
  - Stop() closes the sockets on shutdown

NOTE: DNS-SD TXT publication needs an exported TXTEntry API that is not
yet in a tagged pion/mdns release; go.mod pins pion/mdns/v2 to a fork
branch via `replace` until pion/mdns#277 merges and ships. This PR
stays in draft until then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mcuelenaere mcuelenaere force-pushed the claude/keen-hodgkin-0657ba branch from 7c807df to a57e2e4 Compare May 29, 2026 13:49
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.

1 participant