mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-25 23:30:41 +01:00
fix(darwin): handle mDNSResponder on port 53 to avoid bind conflicts
When mDNSResponder is using port 53 on macOS, adjust listener config to use 0.0.0.0:53, stop mDNSResponder before binding, and run cleanup on install and uninstall so the DNS server can start reliably.
This commit is contained in:
committed by
Cuong Manh Le
parent
40c68a13a1
commit
a4f0418811
@@ -901,6 +901,9 @@ func selfCheckStatus(ctx context.Context, s service.Service, sockDir string) (bo
|
||||
|
||||
lc := cfg.FirstListener()
|
||||
addr := net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
|
||||
if needMdnsResponderHack {
|
||||
addr = "127.0.0.1:53"
|
||||
}
|
||||
|
||||
mainLog.Load().Debug().Msgf("performing listener test, sending queries to %s", addr)
|
||||
|
||||
@@ -1113,6 +1116,10 @@ func uninstall(p *prog, s service.Service) {
|
||||
// Stop already did router.Cleanup and report any error if happens,
|
||||
// ignoring error here to prevent false positive.
|
||||
_ = p.router.Cleanup()
|
||||
|
||||
// Run mDNS responder cleanup if necessary
|
||||
doMdnsResponderCleanup()
|
||||
|
||||
mainLog.Load().Notice().Msg("Service uninstalled")
|
||||
return
|
||||
}
|
||||
@@ -1230,6 +1237,8 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, noti
|
||||
nextdnsMode := nextdns != ""
|
||||
// For Windows server with local Dns server running, we can only try on random local IP.
|
||||
hasLocalDnsServer := hasLocalDnsServerRunning()
|
||||
// For Macos with mDNSResponder running on port 53, we must use 0.0.0.0 to prevent conflicting.
|
||||
needMdnsResponderHack := needMdnsResponderHack
|
||||
notRouter := router.Name() == ""
|
||||
isDesktop := ctrld.IsDesktopPlatform()
|
||||
for n, listener := range cfg.Listener {
|
||||
@@ -1263,6 +1272,12 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, noti
|
||||
lcc[n].Port = false
|
||||
}
|
||||
}
|
||||
if needMdnsResponderHack {
|
||||
listener.IP = "0.0.0.0"
|
||||
listener.Port = 53
|
||||
lcc[n].IP = false
|
||||
lcc[n].Port = false
|
||||
}
|
||||
updated = updated || lcc[n].IP || lcc[n].Port
|
||||
}
|
||||
|
||||
@@ -1295,6 +1310,9 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, noti
|
||||
// Created listeners will be kept in listeners slice above, and close
|
||||
// before function finished.
|
||||
tryListen := func(addr string) error {
|
||||
if needMdnsResponderHack {
|
||||
killMdnsResponder()
|
||||
}
|
||||
udpLn, udpErr := net.ListenPacket("udp", addr)
|
||||
if udpLn != nil {
|
||||
closers = append(closers, udpLn)
|
||||
@@ -1358,6 +1376,9 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, noti
|
||||
}
|
||||
attempts := 0
|
||||
maxAttempts := 10
|
||||
if needMdnsResponderHack {
|
||||
maxAttempts = 1
|
||||
}
|
||||
for {
|
||||
if attempts == maxAttempts {
|
||||
notifyFunc()
|
||||
|
||||
@@ -359,6 +359,10 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
|
||||
initInteractiveLogging()
|
||||
tasks := []task{
|
||||
{func() error {
|
||||
doMdnsResponderCleanup()
|
||||
return nil
|
||||
}, false, "Cleanup service before installation"},
|
||||
{func() error {
|
||||
// Save current DNS so we can restore later.
|
||||
withEachPhysicalInterfaces("", "saveCurrentStaticDNS", func(i *net.Interface) error {
|
||||
@@ -374,6 +378,10 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
}, false, "Configure service failure actions"},
|
||||
{s.Start, true, "Start"},
|
||||
{noticeWritingControlDConfig, false, "Notice writing ControlD config"},
|
||||
{func() error {
|
||||
doMdnsResponderHackPostInstall()
|
||||
return nil
|
||||
}, false, "Configure service post installation"},
|
||||
}
|
||||
mainLog.Load().Notice().Msg("Starting existing ctrld service")
|
||||
if doTasks(tasks) {
|
||||
@@ -437,6 +445,10 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
}
|
||||
|
||||
tasks := []task{
|
||||
{func() error {
|
||||
doMdnsResponderCleanup()
|
||||
return nil
|
||||
}, false, "Cleanup service before installation"},
|
||||
{s.Stop, false, "Stop"},
|
||||
{func() error { return doGenerateNextDNSConfig(nextdns) }, true, "Checking config"},
|
||||
{func() error { return ensureUninstall(s) }, false, "Ensure uninstall"},
|
||||
@@ -459,6 +471,10 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
// Note that startCmd do not actually write ControlD config, but the config file was
|
||||
// generated after s.Start, so we notice users here for consistent with nextdns mode.
|
||||
{noticeWritingControlDConfig, false, "Notice writing ControlD config"},
|
||||
{func() error {
|
||||
doMdnsResponderHackPostInstall()
|
||||
return nil
|
||||
}, false, "Configure service post installation"},
|
||||
}
|
||||
mainLog.Load().Notice().Msg("Starting service")
|
||||
if doTasks(tasks) {
|
||||
|
||||
@@ -101,6 +101,15 @@ func (p *prog) serveDNS(listenerNum string) error {
|
||||
_ = w.WriteMsg(answer)
|
||||
return
|
||||
}
|
||||
// When mDNSResponder hack has been done, ctrld was listening on 0.0.0.0:53, but only requests
|
||||
// to 127.0.0.1:53 are accepted. Since binding to 0.0.0.0 will make the IP info of the local address
|
||||
// hidden (appeared as [::]), we checked for requests originated from 127.0.0.1 instead.
|
||||
if needMdnsResponderHack && !strings.HasPrefix(w.RemoteAddr().String(), "127.0.0.1:") {
|
||||
answer := new(dns.Msg)
|
||||
answer.SetRcode(m, dns.RcodeRefused)
|
||||
_ = w.WriteMsg(answer)
|
||||
return
|
||||
}
|
||||
listenerConfig := p.cfg.Listener[listenerNum]
|
||||
reqId := requestID()
|
||||
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
|
||||
@@ -854,6 +863,9 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
if needMdnsResponderHack {
|
||||
killMdnsResponder()
|
||||
}
|
||||
if err := s.ListenAndServe(); err != nil {
|
||||
s.NotifyStartedFunc()
|
||||
mainLog.Load().Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
|
||||
|
||||
154
cmd/cli/mdnsresponder_hack_darwin.go
Normal file
154
cmd/cli/mdnsresponder_hack_darwin.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
// On macOS, the system daemon mDNSResponder (used for proxy/mDNS/Bonjour discovery)
|
||||
// listens on UDP and TCP port 53. That conflicts with ctrld when it needs to
|
||||
// run a DNS proxy on port 53. The kernel does not allow two processes to bind
|
||||
// the same address/port, so ctrld would fail with "address already in use" if we
|
||||
// did nothing.
|
||||
//
|
||||
// If ctrld started before mDNSResponder and listened only on 127.0.0.1, mDNSResponder
|
||||
// would bind port 53 on other interfaces, so system processes would use it as the
|
||||
// DNS resolver instead of ctrld, leading to inconsistent behavior.
|
||||
//
|
||||
// This file implements a Darwin-only workaround:
|
||||
//
|
||||
// - We detect at startup whether mDNSResponder is using port 53 (or a
|
||||
// persisted marker file exists from a previous run).
|
||||
// - When the workaround is active, we force the listener to 0.0.0.0:53 and,
|
||||
// before binding, run killall mDNSResponder so that ctrld can bind to port 53.
|
||||
// - We use SO_REUSEPORT (see listener setup) so that the socket can be bound
|
||||
// even when the port was recently used.
|
||||
// - On install we create a marker file in the user's home directory so that
|
||||
// the workaround is applied on subsequent starts; on uninstall we remove
|
||||
// that file and bounce the en0 interface to restore normal mDNSResponder
|
||||
// behavior.
|
||||
//
|
||||
// Without this, users on macOS would be unable to run ctrld as the system DNS
|
||||
// on port 53 when mDNSResponder is active.
|
||||
|
||||
var (
|
||||
|
||||
// needMdnsResponderHack determines if a system-specific workaround for mDNSResponder is necessary at runtime.
|
||||
needMdnsResponderHack = mDNSResponderHack()
|
||||
mDNSResponderHackFilename = ".mdnsResponderHack"
|
||||
)
|
||||
|
||||
// mDNSResponderHack checks if the mDNSResponder process and its environments meet specific criteria for operation.
|
||||
func mDNSResponderHack() bool {
|
||||
if st, err := os.Stat(mDNSResponderFile()); err == nil && st.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
out, err := lsofCheckPort53()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !isMdnsResponderListeningPort53(strings.NewReader(out)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// mDNSResponderFile constructs and returns the absolute path to the mDNSResponder hack file in the user's home directory.
|
||||
func mDNSResponderFile() string {
|
||||
if d, err := userHomeDir(); err == nil && d != "" {
|
||||
return filepath.Join(d, mDNSResponderHackFilename)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// doMdnsResponderCleanup performs cleanup tasks for the mDNSResponder hack file and resets the network interface "en0".
|
||||
func doMdnsResponderCleanup() {
|
||||
fn := mDNSResponderFile()
|
||||
if fn == "" {
|
||||
return
|
||||
}
|
||||
if st, err := os.Stat(fn); err != nil || !st.Mode().IsRegular() {
|
||||
return
|
||||
}
|
||||
if err := os.Remove(fn); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to remove mDNSResponder hack file")
|
||||
}
|
||||
|
||||
ifName := "en0"
|
||||
if din, err := netmon.DefaultRouteInterface(); err == nil {
|
||||
ifName = din
|
||||
}
|
||||
if err := exec.Command("ifconfig", ifName, "down").Run(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to disable en0")
|
||||
}
|
||||
if err := exec.Command("ifconfig", ifName, "up").Run(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to enable en0")
|
||||
}
|
||||
}
|
||||
|
||||
// doMdnsResponderHackPostInstall creates a hack file for mDNSResponder if required and logs debug or error messages.
|
||||
func doMdnsResponderHackPostInstall() {
|
||||
if !needMdnsResponderHack {
|
||||
return
|
||||
}
|
||||
fn := mDNSResponderFile()
|
||||
if fn == "" {
|
||||
return
|
||||
}
|
||||
if f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0400); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msgf("Could not create %s", fn)
|
||||
} else {
|
||||
if err := f.Close(); err != nil {
|
||||
mainLog.Load().Warn().Err(err).Msgf("Could not close %s", fn)
|
||||
} else {
|
||||
mainLog.Load().Debug().Msgf("Created %s", fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// killMdnsResponder attempts to terminate the mDNSResponder process by running the "killall" command multiple times.
|
||||
// Logs any accumulated errors if the attempts to terminate the process fail.
|
||||
func killMdnsResponder() {
|
||||
numAttempts := 10
|
||||
errs := make([]error, 0, numAttempts)
|
||||
for range numAttempts {
|
||||
if err := exec.Command("killall", "mDNSResponder").Run(); err != nil {
|
||||
// Exit code 1 means the process not found, do not log it.
|
||||
if !strings.Contains(err.Error(), "exit status 1") {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
mainLog.Load().Debug().Err(errors.Join(errs...)).Msg("failed to kill mDNSResponder")
|
||||
}
|
||||
}
|
||||
|
||||
// lsofCheckPort53 executes the lsof command to check if any process is listening on port 53 and returns the output.
|
||||
func lsofCheckPort53() (string, error) {
|
||||
cmd := exec.Command("lsof", "+c0", "-i:53", "-n", "-P")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// isMdnsResponderListeningPort53 checks if the output provided by the reader contains an mDNSResponder process.
|
||||
func isMdnsResponderListeningPort53(r io.Reader) bool {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) > 0 && strings.EqualFold(fields[0], "mDNSResponder") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
21
cmd/cli/mdnsresponder_hack_others.go
Normal file
21
cmd/cli/mdnsresponder_hack_others.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build !darwin
|
||||
|
||||
package cli
|
||||
|
||||
// needMdnsResponderHack determines if a system-specific workaround for mDNSResponder is necessary at runtime.
|
||||
var needMdnsResponderHack = mDNSResponderHack()
|
||||
|
||||
// mDNSResponderHack checks if the mDNSResponder process and its environments meet specific criteria for operation.
|
||||
func mDNSResponderHack() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// killMdnsResponder attempts to terminate the mDNSResponder process by running the "killall" command multiple times.
|
||||
// Logs any accumulated errors if the attempts to terminate the process fail.
|
||||
func killMdnsResponder() {}
|
||||
|
||||
// doMdnsResponderCleanup performs cleanup tasks for the mDNSResponder hack file and resets the network interface "en0".
|
||||
func doMdnsResponderCleanup() {}
|
||||
|
||||
// doMdnsResponderHackPostInstall creates a hack file for mDNSResponder if required and logs debug or error messages.
|
||||
func doMdnsResponderHackPostInstall() {}
|
||||
Reference in New Issue
Block a user