all: rework routers ntp waiting mechanism

Currently, on routers that require NTP waiting, ctrld makes the cleanup
process, and restart dnsmasq for restoring default DNS config, so ntpd
can query the NTP servers. It did work, but the code will depends on
router platforms.

Instead, we can spawn a plain DNS listener before PreRun on routers,
this listener will serve NTP dns queries and once ntp is configured, the
listener is terminated and ctrld will start serving using its configured
upstreams.

While at it, also fix the userHomeDir function on freshtomato, which
must return the binary directory for routers that requires JFFS.
This commit is contained in:
Cuong Manh Le
2023-06-02 20:25:11 +07:00
committed by Cuong Manh Le
parent 2d950eecdf
commit 1cd54a48e9
6 changed files with 81 additions and 79 deletions
+6 -2
View File
@@ -165,9 +165,13 @@ func initCLI() {
initLogging() initLogging()
if setupRouter { if setupRouter {
if err := router.PreStart(); err != nil { s, _ := runDNSServerForNTPD()
if err := router.PreRun(); err != nil {
mainLog.Fatal().Err(err).Msg("failed to perform router pre-start check") mainLog.Fatal().Err(err).Msg("failed to perform router pre-start check")
} }
if err := s.Shutdown(); err != nil {
mainLog.Fatal().Err(err).Msg("failed to shutdown dns server for ntpd")
}
} }
processCDFlags() processCDFlags()
@@ -909,7 +913,7 @@ func unsupportedPlatformHelp(cmd *cobra.Command) {
func userHomeDir() (string, error) { func userHomeDir() (string, error) {
switch router.Name() { switch router.Name() {
case router.DDWrt, router.Merlin: case router.DDWrt, router.Merlin, router.Tomato:
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
return "", err return "", err
+42
View File
@@ -459,3 +459,45 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha
waitLock.Lock() waitLock.Lock()
return s, errCh return s, errCh
} }
func runDNSServerForNTPD() (*dns.Server, <-chan error) {
dnsResolver := ctrld.NewBootstrapResolver()
s := &dns.Server{
Addr: router.ListenAddress(),
Net: "udp",
Handler: dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
mainLog.Debug().Msg("Serving query for ntpd")
resolveCtx, cancel := context.WithCancel(context.Background())
defer cancel()
if osUpstreamConfig.Timeout > 0 {
timeoutCtx, cancel := context.WithTimeout(resolveCtx, time.Millisecond*time.Duration(osUpstreamConfig.Timeout))
defer cancel()
resolveCtx = timeoutCtx
}
answer, err := dnsResolver.Resolve(resolveCtx, m)
if err != nil {
mainLog.Error().Err(err).Msgf("could not resolve: %v", m)
return
}
if err := w.WriteMsg(answer); err != nil {
mainLog.Error().Err(err).Msg("runDNSServerForNTPD: failed to send DNS response")
}
}),
}
waitLock := sync.Mutex{}
waitLock.Lock()
s.NotifyStartedFunc = waitLock.Unlock
errCh := make(chan error)
go func() {
defer close(errCh)
if err := s.ListenAndServe(); err != nil {
waitLock.Unlock()
mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
errCh <- err
}
}()
waitLock.Lock()
return s, errCh
}
-45
View File
@@ -2,16 +2,11 @@ package router
import ( import (
"bytes" "bytes"
"context"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
"time"
"unicode" "unicode"
"tailscale.com/logtail/backoff"
) )
func setupMerlin() error { func setupMerlin() error {
@@ -92,43 +87,3 @@ func merlinParsePostConf(buf []byte) []byte {
} }
return buf return buf
} }
func merlinPreStart() (err error) {
pidFile := "/tmp/ctrld.pid"
// Remove pid file and trigger dnsmasq restart, so NTP can resolve
// server name and perform time synchronization.
pid, err := os.ReadFile(pidFile)
if err != nil {
return fmt.Errorf("PreStart: os.Readfile: %w", err)
}
if err := os.Remove(pidFile); err != nil {
return fmt.Errorf("PreStart: os.Remove: %w", err)
}
defer func() {
if werr := os.WriteFile(pidFile, pid, 0600); werr != nil {
err = errors.Join(err, werr)
return
}
if rerr := restartDNSMasq(); rerr != nil {
err = errors.Join(err, rerr)
return
}
}()
if err := restartDNSMasq(); err != nil {
return fmt.Errorf("PreStart: restartDNSMasqFn: %w", err)
}
// Wait until `ntp_ready=1` set.
b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
for {
out, err := nvram("get", "ntp_ready")
if err != nil {
return fmt.Errorf("PreStart: nvram: %w", err)
}
if out == "1" {
return nil
}
b.BackOff(context.Background(), errors.New("ntp not ready"))
}
}
+19 -6
View File
@@ -2,14 +2,18 @@ package router
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/kardianos/service" "github.com/kardianos/service"
"tailscale.com/logtail/backoff"
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
) )
@@ -106,14 +110,23 @@ func ConfigureService(sc *service.Config) error {
return nil return nil
} }
// PreStart blocks until the router is ready for running ctrld. // PreRun blocks until the router is ready for running ctrld.
func PreStart() (err error) { func PreRun() (err error) {
// On some routers, NTP may out of sync, so waiting for it to be ready. // On some routers, NTP may out of sync, so waiting for it to be ready.
switch Name() { switch Name() {
case Merlin: case Merlin, Tomato:
return merlinPreStart() // Wait until `ntp_ready=1` set.
case Tomato: b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
return tomatoPreStart() for {
out, err := nvram("get", "ntp_ready")
if err != nil {
return fmt.Errorf("PreStart: nvram: %w", err)
}
if out == "1" {
return nil
}
b.BackOff(context.Background(), errors.New("ntp not ready"))
}
default: default:
return nil return nil
} }
-26
View File
@@ -1,13 +1,8 @@
package router package router
import ( import (
"context"
"errors"
"fmt" "fmt"
"os/exec" "os/exec"
"time"
"tailscale.com/logtail/backoff"
) )
const ( const (
@@ -72,27 +67,6 @@ func cleanupTomato() error {
return nil return nil
} }
func tomatoPreStart() (err error) {
// cleanup to trigger dnsmasq restart, so NTP can resolve
// server name and perform time synchronization.
if err = cleanupTomato(); err != nil {
return err
}
// Wait until `ntp_ready=1` set.
b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
for {
out, err := nvram("get", "ntp_ready")
if err != nil {
return fmt.Errorf("PreStart: nvram: %w", err)
}
if out == "1" {
return nil
}
b.BackOff(context.Background(), errors.New("ntp not ready"))
}
}
func tomatoRestartService(name string) error { func tomatoRestartService(name string) error {
return tomatoRestartServiceWithKill(name, false) return tomatoRestartServiceWithKill(name, false)
} }
+14
View File
@@ -201,3 +201,17 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string)
} }
return ips return ips
} }
// NewBootstrapResolver returns an OS resolver, which use following nameservers:
//
// - ControlD bootstrap DNS server.
// - Gateway IP address (depends on OS).
// - Input servers.
func NewBootstrapResolver(servers ...string) Resolver {
resolver := &osResolver{nameservers: nameservers()}
resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...)
for _, ns := range servers {
resolver.nameservers = append([]string{net.JoinHostPort(ns, "53")}, resolver.nameservers...)
}
return resolver
}