Skip to content

Bound multicast writes so a stuck socket cannot hang Shutdown - #11

Open
andig wants to merge 2 commits into
enbility:devfrom
andig:fix/write-deadline
Open

andig wants to merge 2 commits into
enbility:devfrom
andig:fix/write-deadline

Conversation

@andig

@andig andig commented Nov 5, 2026

Copy link
Copy Markdown

On macOS with many multicast interfaces (here 22: anpi*, en*, awdl0, llw0, utun*), a burst of per-interface multicast writes on one shared UDP socket can wedge that socket. sendmsg first fails with ENETUNREACH, then blocks forever. A fresh socket per interface and repeated writes on a single interface do not trigger it.

Server.Shutdown calls unregister, which sends the goodbye multicast synchronously, so Shutdown never returns. Every caller hangs with it. In ship-go, go test ./mdns/ runs into the 10 minute test timeout, both on dev (zeroconf v2) and on enbility/ship-go#68 (v3):

goroutine 1304 [IO wait, 1 minutes]:
internal/poll.(*FD).RawWrite
...
github.com/enbility/zeroconf/v2.(*Server).multicastResponse  server.go:793
github.com/enbility/zeroconf/v2.(*Server).unregister          server.go:659
github.com/enbility/zeroconf/v2.(*Server).Shutdown            server.go:244
github.com/enbility/ship-go/mdns.(*ZeroconfProvider).UnannounceService

Fix

  • ipv4PacketConn.WriteTo and ipv6PacketConn.WriteTo set a 100 ms write deadline per packet. A healthy send takes microseconds.
  • isInterfaceGone treats os.ErrDeadlineExceeded as gone. InterfaceManager.MarkFailed then backs the interface off, so the next send does not stall on it again.

Verification (macOS, behind a corporate VPN)

  • zeroconf go test .: before, TestBasic hit the timeout after 6m40s. After, the suite completes in 28 s. TestBasic, TestSubtype and TestQuickShutdown still fail on this machine because multicast does not work there. They are not hangs, and TestQuickShutdown fails on dev too.
  • ship-go#68 with this branch via replace: go test ./mdns/ before, Test_GoroutineLeakOnShutdown hung into the timeout. After, it completes in 76 s. Only the discovery assertions of Test_ZeroConf fail, for the same multicast reason.
  • New tests: TestWriteToSetsDeadline (fails without the fix) and TestIsInterfaceGone_WriteDeadlineExceeded_ReturnsTrue.

The existing TestIsInterfaceGone_ETIMEDOUT_ReturnsFalse still passes. A kernel ETIMEDOUT stays transient. Only our own write deadline counts as gone.

ship-go still pins zeroconf v2, so this reaches ship-go once enbility/ship-go#68 lands and picks up this commit.

🤖 Generated with Claude Code

andig and others added 2 commits November 5, 2026 10:43
On macOS with many multicast interfaces a burst of per-interface writes
can wedge the shared UDP socket: sendmsg first fails with ENETUNREACH,
then blocks forever. Server.Shutdown calls unregister, which multicasts
synchronously, so it never returns and every caller hangs with it
(e.g. ship-go's mdns tests time out after 10 minutes).

Set a 100ms write deadline per packet and treat a deadline exceeded
error like a gone interface, so the interface manager backs it off
instead of stalling every following send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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