Repository navigation
[Bug Report] Panic in ChooseDialTarget during staged DNS controller handoff #1054
Description
Checks
- I have searched the existing issues
- I have read the documentation
Current Behavior
During a staged same-port reload, dae can panic while an old control-plane generation is still handling a TCP connection:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1026870]
goroutine 886759 [running]:
github.com/daeuniverse/dae/control.(*DnsController).HasDnsKnowledge(0x0, ...)
github.com/daeuniverse/dae/control/dns_control.go:935 +0x50
github.com/daeuniverse/dae/control.(*ControlPlane).ChooseDialTarget(...)
github.com/daeuniverse/dae/control/control_plane.go:1720 +0x16a
github.com/daeuniverse/dae/control.(*ControlPlane).chooseProxyDialer(...)
github.com/daeuniverse/dae/control/dial.go:110 +0xbb
github.com/daeuniverse/dae/control.(*ControlPlane).routeDial(...)
github.com/daeuniverse/dae/control/dial.go:234 +0xb4
github.com/daeuniverse/dae/control.(*ControlPlane).handleConn(...)
github.com/daeuniverse/dae/control/tcp.go:233 +0xeb4
github.com/daeuniverse/dae/control.(*ControlPlane).Serve.func2.1(...)
github.com/daeuniverse/dae/control/control_plane.go:3132 +0xb3
The panic happened about 55 ms after the staged generation entered Serve and about 5 ms after restoring the DNS reload cache:
WARN Reload: Received reload signal; prepare to reload
WARN Reload: Prepare staged same-port handoff
WARN Reload: Serve
INFO Restored 80 DNS cache entries from previous control plane
panic: runtime error: invalid memory address or nil pointer dereference
This appears to be a DNS-controller ownership handoff race:
reuseDNSControllerFrompublishes the replacement controller through the old plane's handoff pointer.- It then sets
previous.dnsController = niland assigns the replacement to the new plane. - Old accepted/in-flight TCP work can still call
ChooseDialTargetwhile the old generation is draining. ChooseDialTargetreadsc.dnsControllerdirectly instead of resolving the handoff withc.ActiveDnsController().
The old generation therefore has a valid dnsHandoffController, but c.dnsController is nil. This direct access panics when dial_mode: domain reaches HasDnsKnowledge.
The same direct access and ownership-transfer sequence are present in the v2.0.0 tag and current main:
// control/control_plane.go
if c.dnsController.HasDnsKnowledge(c.dnsController.cacheKey(domain, ...)) {// control/dns_runtime.go
if publishHandoff != nil {
publishHandoff(reusedController)
}
previous.dnsController = nil
r.dnsController = reusedControllerExpected Behavior
Connections still running on the old control-plane generation should resolve the DNS handoff controller and complete or drain without crashing the process.
ChooseDialTarget should take one stable ActiveDnsController() snapshot and handle nil safely, rather than directly reading the ownership field during reload.
Steps to Reproduce
The race is timing-sensitive; a later reload with the same configuration and active traffic succeeded.
- Configure dae with
dial_mode: domainand a same-port reload configuration. - Keep TCP traffic active so the old control plane is accepting or handling connections during handoff.
- Repeatedly run
dae reloadwithout changing the tproxy port. - Occasionally an old-generation connection reaches
ChooseDialTargetafter DNS ownership has transferred and dae panics as above.
A deterministic regression test could:
- Create old and replacement control planes in domain mode.
- Call
newCP.ReuseDNSControllerFrom(oldCP)and verifyoldCPresolves the published handoff while its owneddnsControlleris detached. - Invoke
oldCP.ChooseDialTarget(...)for a non-reserved outbound and a non-IP domain. - Assert that it does not panic and uses the handoff controller.
Environment
- Dae version:
unstable-20260710.r873.f920fa4(downstream build based on upstreamv2.0.0; the relevant code is unchanged fromv2.0.0/currentmain) - Go runtime:
go1.26.3-X:heapminimum512kib linux/amd64 - OS: Debian GNU/Linux 13 (trixie)
- Kernel:
6.12.95+deb13-cloud-amd64 - Architecture: x86_64
- Relevant config:
dial_mode: domain; unchanged tproxy port, so reload used staged same-port handoff
Anything else?
This is related to the staged handoff subsystem discussed in #1013/#1016, but appears to be a distinct userspace lifecycle race. #1016 addresses shared BPF/domain-routing-map mutation and does not change ChooseDialTarget, ActiveDnsController, or the ownership detachment shown above.
The crash is also operationally severe with the packaged systemd unit: a Go panic exits with status 2, while Restart=on-abnormal does not restart a process that exited normally with a non-zero status. That is secondary to the controller race, but it can turn this panic into a prolonged outage.
Activity
dae-prow commentedon Jul 15, 2026
Thanks for opening this issue!
ci: bootstrap fix for daeuniverse#1054
ci: fix daeuniverse#1054 regression fixture
olicesx commentedon Sep 17, 2026
Fixed by the #1099 control-plane rework. The staged same-port handoff this panic rode on no longer exists: reload now swaps routing epochs atomically (double-slot publish with a retirement gate), and a retiring generation cannot serve new dial-target decisions.
HasDnsKnowledgealso gained store guards, so the nil-receiver path from the old handoff cannot be reached.