Merge pull request #51 from Control-D-Inc/release-branch-v1.2.1

Release branch v1.2.1
This commit is contained in:
Yegor S
2023-06-08 00:19:07 -04:00
committed by GitHub
33 changed files with 1196 additions and 166 deletions
+34 -24
View File
@@ -36,8 +36,6 @@ import (
"github.com/Control-D-Inc/ctrld/internal/router" "github.com/Control-D-Inc/ctrld/internal/router"
) )
const selfCheckFQDN = "verify.controld.com"
var ( var (
version = "dev" version = "dev"
commit = "none" commit = "none"
@@ -138,16 +136,7 @@ func initCLI() {
mainLog.Fatal().Err(err).Msg("failed create new service") mainLog.Fatal().Err(err).Msg("failed create new service")
} }
s = newService(s) s = newService(s)
serviceLogger, err := s.Logger(nil)
if err != nil {
mainLog.Error().Err(err).Msg("failed to get service logger")
return
}
if err := s.Run(); err != nil { if err := s.Run(); err != nil {
if sErr := serviceLogger.Error(err); sErr != nil {
mainLog.Error().Err(sErr).Msg("failed to write service log")
}
mainLog.Error().Err(err).Msg("failed to start service") mainLog.Error().Err(err).Msg("failed to start service")
} }
}() }()
@@ -176,9 +165,13 @@ func initCLI() {
initLogging() initLogging()
if setupRouter { if setupRouter {
if err := router.PreStart(); err != nil { s, errCh := runDNSServerForNTPD(router.ListenAddress())
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 && errCh != nil {
mainLog.Fatal().Err(err).Msg("failed to shutdown dns server for ntpd")
}
} }
processCDFlags() processCDFlags()
@@ -241,6 +234,8 @@ func initCLI() {
runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
runCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
_ = runCmd.Flags().MarkHidden("dev")
runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "") runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "")
_ = runCmd.Flags().MarkHidden("homedir") _ = runCmd.Flags().MarkHidden("homedir")
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
@@ -298,6 +293,10 @@ func initCLI() {
processCDFlags() processCDFlags()
if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil {
mainLog.Fatal().Msgf("invalid config: %v", err)
}
// Explicitly passing config, so on system where home directory could not be obtained, // Explicitly passing config, so on system where home directory could not be obtained,
// or sub-process env is different with the parent, we still behave correctly and use // or sub-process env is different with the parent, we still behave correctly and use
// the expected config file. // the expected config file.
@@ -319,7 +318,7 @@ func initCLI() {
{s.Start, true}, {s.Start, true},
} }
if doTasks(tasks) { if doTasks(tasks) {
if err := router.PostInstall(); err != nil { if err := router.PostInstall(svcConfig); err != nil {
mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error") mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error")
return return
} }
@@ -329,7 +328,8 @@ func initCLI() {
return return
} }
status = selfCheckStatus(status) domain := cfg.Upstream["0"].VerifyDomain()
status = selfCheckStatus(status, domain)
switch status { switch status {
case service.StatusRunning: case service.StatusRunning:
mainLog.Notice().Msg("Service started") mainLog.Notice().Msg("Service started")
@@ -354,6 +354,8 @@ func initCLI() {
startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
_ = startCmd.Flags().MarkHidden("dev")
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
startCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`) startCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`)
_ = startCmd.Flags().MarkHidden("router") _ = startCmd.Flags().MarkHidden("router")
@@ -474,7 +476,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
} }
prog.resetDNS() prog.resetDNS()
mainLog.Debug().Msg("Router cleanup") mainLog.Debug().Msg("Router cleanup")
if err := router.Cleanup(); err != nil { if err := router.Cleanup(svcConfig); err != nil {
mainLog.Warn().Err(err).Msg("could not cleanup router") mainLog.Warn().Err(err).Msg("could not cleanup router")
} }
mainLog.Notice().Msg("Service uninstalled") mainLog.Notice().Msg("Service uninstalled")
@@ -708,7 +710,7 @@ func processCDFlags() {
} }
logger := mainLog.With().Str("mode", "cd").Logger() logger := mainLog.With().Str("mode", "cd").Logger()
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version) resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev)
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode { if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
s, err := service.New(&prog{}, svcConfig) s, err := service.New(&prog{}, svcConfig)
if err != nil { if err != nil {
@@ -854,17 +856,25 @@ func netInterface(ifaceName string) (*net.Interface, error) {
func defaultIfaceName() string { func defaultIfaceName() string {
dri, err := interfaces.DefaultRouteInterface() dri, err := interfaces.DefaultRouteInterface()
if err != nil { if err != nil {
// On WSL 1, the route table does not have any default route. But the fact that
// it only uses /etc/resolv.conf for setup DNS, so we can use "lo" here.
if oi := osinfo.New(); strings.Contains(oi.String(), "Microsoft") {
return "lo"
}
mainLog.Fatal().Err(err).Msg("failed to get default route interface") mainLog.Fatal().Err(err).Msg("failed to get default route interface")
} }
return dri return dri
} }
func selfCheckStatus(status service.Status) service.Status { func selfCheckStatus(status service.Status, domain string) service.Status {
if domain == "" {
// Nothing to do, return the status as-is.
return status
}
c := new(dns.Client) c := new(dns.Client)
bo := backoff.NewBackoff("self-check", logf, 10*time.Second) bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
bo.LogLongerThan = 500 * time.Millisecond bo.LogLongerThan = 500 * time.Millisecond
ctx := context.Background() ctx := context.Background()
err := errors.New("query failed")
maxAttempts := 20 maxAttempts := 20
mainLog.Debug().Msg("Performing self-check") mainLog.Debug().Msg("Performing self-check")
var ( var (
@@ -888,16 +898,16 @@ func selfCheckStatus(status service.Status) service.Status {
} }
mu.Unlock() mu.Unlock()
m := new(dns.Msg) m := new(dns.Msg)
m.SetQuestion(selfCheckFQDN+".", dns.TypeA) m.SetQuestion(domain+".", dns.TypeA)
m.RecursionDesired = true m.RecursionDesired = true
r, _, _ := c.ExchangeContext(ctx, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))) r, _, err := c.ExchangeContext(ctx, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)))
if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 { if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 {
mainLog.Debug().Msgf("self-check against %q succeeded", selfCheckFQDN) mainLog.Debug().Msgf("self-check against %q succeeded", domain)
return status return status
} }
bo.BackOff(ctx, err) bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", err))
} }
mainLog.Debug().Msgf("self-check against %q failed", selfCheckFQDN) mainLog.Debug().Msgf("self-check against %q failed", domain)
return service.StatusUnknown return service.StatusUnknown
} }
@@ -907,7 +917,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
@@ -1,3 +1,5 @@
//go:build linux || freebsd
package main package main
import ( import (
@@ -42,12 +44,11 @@ func initRouterCLI() {
if platform == "auto" { if platform == "auto" {
platform = router.Name() platform = router.Name()
} }
switch platform { if !router.IsSupported(platform) {
case router.DDWrt, router.Merlin, router.OpenWrt, router.Ubios:
default:
unsupportedPlatformHelp(cmd) unsupportedPlatformHelp(cmd)
os.Exit(1) os.Exit(1)
} }
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
mainLog.Fatal().Msgf("could not find executable path: %v", err) mainLog.Fatal().Msgf("could not find executable path: %v", err)
@@ -76,6 +77,8 @@ func initRouterCLI() {
routerCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") routerCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
routerCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") routerCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
routerCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") routerCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
routerCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
_ = routerCmd.Flags().MarkHidden("dev")
routerCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) routerCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
tmpl := routerCmd.UsageTemplate() tmpl := routerCmd.UsageTemplate()
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !linux //go:build !linux && !freebsd
package main package main
+73 -2
View File
@@ -50,11 +50,12 @@ func (p *prog) serveDNS(listenerNum string) error {
q := m.Question[0] q := m.Question[0]
domain := canonicalName(q.Name) domain := canonicalName(q.Name)
reqId := requestID() reqId := requestID()
fmtSrcToDest := fmtRemoteToLocal(listenerNum, w.RemoteAddr().String(), w.LocalAddr().String()) remoteAddr := spoofRemoteAddr(w.RemoteAddr(), router.GetClientInfoByMac(macFromMsg(m)))
fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String())
t := time.Now() t := time.Now()
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId) ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
ctrld.Log(ctx, mainLog.Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain) ctrld.Log(ctx, mainLog.Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain)
upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, w.RemoteAddr(), domain) upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, domain)
var answer *dns.Msg var answer *dns.Msg
if !matched && listenerConfig.Restricted { if !matched && listenerConfig.Restricted {
answer = new(dns.Msg) answer = new(dns.Msg)
@@ -418,6 +419,28 @@ func macFromMsg(msg *dns.Msg) string {
return "" return ""
} }
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
if ci != nil && ci.IP != "" {
switch addr := addr.(type) {
case *net.UDPAddr:
udpAddr := &net.UDPAddr{
IP: net.ParseIP(ci.IP),
Port: addr.Port,
Zone: addr.Zone,
}
return udpAddr
case *net.TCPAddr:
udpAddr := &net.TCPAddr{
IP: net.ParseIP(ci.IP),
Port: addr.Port,
Zone: addr.Zone,
}
return udpAddr
}
}
return addr
}
// runDNSServer starts a DNS server for given address and network, // runDNSServer starts a DNS server for given address and network,
// with the given handler. It ensures the server has started listening. // with the given handler. It ensures the server has started listening.
// Any error will be reported to the caller via returned channel. // Any error will be reported to the caller via returned channel.
@@ -446,3 +469,51 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha
waitLock.Lock() waitLock.Lock()
return s, errCh return s, errCh
} }
// runDNSServerForNTPD starts a DNS server listening on router.ListenAddress(). It must only be called when ctrld
// running on router, before router.PreRun() to serve DNS request for NTP synchronization. The caller must call
// s.Shutdown() explicitly when NTP is synced successfully.
func runDNSServerForNTPD(addr string) (*dns.Server, <-chan error) {
if addr == "" {
return &dns.Server{}, nil
}
dnsResolver := ctrld.NewBootstrapResolver()
s := &dns.Server{
Addr: addr,
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
}
+26 -1
View File
@@ -174,7 +174,7 @@ func Test_macFromMsg(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
m := new(dns.Msg) m := new(dns.Msg)
m.SetQuestion(selfCheckFQDN+".", dns.TypeA) m.SetQuestion("example.com.", dns.TypeA)
o := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}} o := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}}
if tc.wantMac { if tc.wantMac {
ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw} ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw}
@@ -191,3 +191,28 @@ func Test_macFromMsg(t *testing.T) {
}) })
} }
} }
func Test_remoteAddrFromMsg(t *testing.T) {
loopbackIP := net.ParseIP("127.0.0.1")
tests := []struct {
name string
addr net.Addr
ci *ctrld.ClientInfo
want string
}{
{"tcp", &net.TCPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{IP: "192.168.1.10"}, "192.168.1.10:12345"},
{"udp", &net.UDPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{IP: "192.168.1.11"}, "192.168.1.11:12345"},
{"nil client info", &net.UDPAddr{IP: loopbackIP, Port: 12345}, nil, "127.0.0.1:12345"},
{"empty ip", &net.UDPAddr{IP: loopbackIP, Port: 12345}, &ctrld.ClientInfo{}, "127.0.0.1:12345"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
addr := spoofRemoteAddr(tc.addr, tc.ci)
if addr.String() != tc.want {
t.Errorf("unexpected result, want: %q, got: %q", tc.want, addr.String())
}
})
}
}
+1
View File
@@ -27,6 +27,7 @@ var (
verbose int verbose int
silent bool silent bool
cdUID string cdUID string
cdDev bool
iface string iface string
ifaceStartStop string ifaceStartStop string
setupRouter bool setupRouter bool
+4 -1
View File
@@ -175,7 +175,10 @@ func (p *prog) setDNS() {
switch router.Name() { switch router.Name() {
case router.DDWrt, router.OpenWrt, router.Ubios: case router.DDWrt, router.OpenWrt, router.Ubios:
// On router, ctrld run as a DNS forwarder, it does not have to change system DNS. // On router, ctrld run as a DNS forwarder, it does not have to change system DNS.
// Except for Merlin, which has WAN DNS setup on boot for NTP. // Except for:
// + EdgeOS, which /etc/resolv.conf could be managed by vyatta_update_resolv.pl script.
// + Merlin/Tomato, which has WAN DNS setup on boot for NTP.
// + Synology, which /etc/resolv.conf is not configured to point to localhost.
return return
} }
if cfg.Listener == nil || cfg.Listener["0"] == nil { if cfg.Listener == nil || cfg.Listener["0"] == nil {
+9
View File
@@ -2,6 +2,8 @@ package main
import ( import (
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/Control-D-Inc/ctrld/internal/router"
) )
func (p *prog) preRun() { func (p *prog) preRun() {
@@ -17,6 +19,13 @@ func setDependencies(svc *service.Config) {
"Wants=NetworkManager-wait-online.service", "Wants=NetworkManager-wait-online.service",
"After=NetworkManager-wait-online.service", "After=NetworkManager-wait-online.service",
} }
// On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file.
if router.Name() == router.EdgeOS {
svc.Dependencies = append(svc.Dependencies, "Wants=vyatta-dhcpd.service")
svc.Dependencies = append(svc.Dependencies, "After=vyatta-dhcpd.service")
svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service")
svc.Dependencies = append(svc.Dependencies, "After=dnsmasq.service")
}
} }
func setWorkingDirectory(svc *service.Config, dir string) { func setWorkingDirectory(svc *service.Config, dir string) {
+2 -1
View File
@@ -13,7 +13,8 @@ import (
func newService(s service.Service) service.Service { func newService(s service.Service) service.Service {
// TODO: unify for other SysV system. // TODO: unify for other SysV system.
if router.IsGLiNet() { switch {
case router.IsGLiNet(), router.IsOldOpenwrt():
return &sysV{s} return &sysV{s}
} }
return s return s
+48 -9
View File
@@ -24,14 +24,31 @@ import (
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
) )
// IpStackBoth ...
const ( const (
IpStackBoth = "both" // IpStackBoth indicates that ctrld will use either ipv4 or ipv6 for connecting to upstream,
IpStackV4 = "v4" // depending on which stack is available when receiving the DNS query.
IpStackV6 = "v6" IpStackBoth = "both"
// IpStackV4 indicates that ctrld will use only ipv4 for connecting to upstream.
IpStackV4 = "v4"
// IpStackV6 indicates that ctrld will use only ipv6 for connecting to upstream.
IpStackV6 = "v6"
// IpStackSplit indicates that ctrld will use either ipv4 or ipv6 for connecting to upstream,
// depending on the record type of the DNS query.
IpStackSplit = "split" IpStackSplit = "split"
controlDComDomain = "controld.com"
controlDNetDomain = "controld.net"
controlDDevDomain = "controld.dev"
) )
var controldParentDomains = []string{"controld.com", "controld.net", "controld.dev"} var (
controldParentDomains = []string{controlDComDomain, controlDNetDomain, controlDDevDomain}
controldVerifiedDomain = map[string]string{
controlDComDomain: "verify.controld.com",
controlDDevDomain: "verify.controld.dev",
}
)
// SetConfigName set the config name that ctrld will look for. // SetConfigName set the config name that ctrld will look for.
// DEPRECATED: use SetConfigNameWithPath instead. // DEPRECATED: use SetConfigNameWithPath instead.
@@ -201,6 +218,23 @@ func (uc *UpstreamConfig) Init() {
} }
} }
// VerifyDomain returns the domain name that could be resolved by the upstream endpoint.
// It returns empty for non-ControlD upstream endpoint.
func (uc *UpstreamConfig) VerifyDomain() string {
domain := uc.Domain
if domain == "" {
if u, err := url.Parse(uc.Endpoint); err == nil {
domain = u.Hostname()
}
}
for _, parent := range controldParentDomains {
if dns.IsSubDomain(parent, domain) {
return controldVerifiedDomain[parent]
}
}
return ""
}
// UpstreamSendClientInfo reports whether the upstream is // UpstreamSendClientInfo reports whether the upstream is
// configured to send client info to Control D DNS server. // configured to send client info to Control D DNS server.
// //
@@ -224,6 +258,7 @@ func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
return false return false
} }
// BootstrapIPs returns the bootstrap IPs list of upstreams.
func (uc *UpstreamConfig) BootstrapIPs() []string { func (uc *UpstreamConfig) BootstrapIPs() []string {
return uc.bootstrapIPs return uc.bootstrapIPs
} }
@@ -347,9 +382,7 @@ func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() {
uc.transport = uc.newDOHTransport(uc.bootstrapIPs6) uc.transport = uc.newDOHTransport(uc.bootstrapIPs6)
case IpStackSplit: case IpStackSplit:
uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4) uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) if hasIPv6() {
defer cancel()
if ctrldnet.IPv6Available(ctx) {
uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6) uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6)
} else { } else {
uc.transport6 = uc.transport4 uc.transport6 = uc.transport4
@@ -419,7 +452,10 @@ func (uc *UpstreamConfig) bootstrapIPForDNSType(dnsType uint16) string {
case dns.TypeA: case dns.TypeA:
return pick(uc.bootstrapIPs4) return pick(uc.bootstrapIPs4)
default: default:
return pick(uc.bootstrapIPs6) if hasIPv6() {
return pick(uc.bootstrapIPs6)
}
return pick(uc.bootstrapIPs4)
} }
} }
return pick(uc.bootstrapIPs) return pick(uc.bootstrapIPs)
@@ -438,7 +474,10 @@ func (uc *UpstreamConfig) netForDNSType(dnsType uint16) (string, string) {
case dns.TypeA: case dns.TypeA:
return "tcp4-tls", "udp4" return "tcp4-tls", "udp4"
default: default:
return "tcp6-tls", "udp6" if hasIPv6() {
return "tcp6-tls", "udp6"
}
return "tcp4-tls", "udp4"
} }
} }
return "tcp-tls", "udp" return "tcp-tls", "udp"
+33
View File
@@ -190,6 +190,39 @@ func TestUpstreamConfig_Init(t *testing.T) {
} }
} }
func TestUpstreamConfig_VerifyDomain(t *testing.T) {
tests := []struct {
name string
uc *UpstreamConfig
verifyDomain string
}{
{
controlDComDomain,
&UpstreamConfig{Endpoint: "https://freedns.controld.com/p2"},
controldVerifiedDomain[controlDComDomain],
},
{
controlDDevDomain,
&UpstreamConfig{Endpoint: "https://freedns.controld.dev/p2"},
controldVerifiedDomain[controlDDevDomain],
},
{
"non-ControlD upstream",
&UpstreamConfig{Endpoint: "https://dns.google/dns-query"},
"",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.uc.VerifyDomain(); got != tc.verifyDomain {
t.Errorf("unexpected verify domain, want: %q, got: %q", tc.verifyDomain, got)
}
})
}
}
func ptrBool(b bool) *bool { func ptrBool(b bool) *bool {
return &b return &b
} }
+1
View File
@@ -109,6 +109,7 @@ type parallelDialerResult struct {
type quicParallelDialer struct{} type quicParallelDialer struct{}
// Dial performs parallel dialing to the given address list.
func (d *quicParallelDialer) Dial(ctx context.Context, domain string, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { func (d *quicParallelDialer) Dial(ctx context.Context, domain string, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
if len(addrs) == 0 { if len(addrs) == 0 {
return nil, errors.New("empty addresses") return nil, errors.New("empty addresses")
+6 -7
View File
@@ -13,10 +13,9 @@ import (
) )
const ( const (
DoHMacHeader = "x-cd-mac" dohMacHeader = "x-cd-mac"
DoHIPHeader = "x-cd-ip" dohIPHeader = "x-cd-ip"
DoHHostHeader = "x-cd-host" dohHostHeader = "x-cd-host"
headerApplicationDNS = "application/dns-message" headerApplicationDNS = "application/dns-message"
) )
@@ -101,13 +100,13 @@ func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
if sendClientInfo { if sendClientInfo {
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil { if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
if ci.Mac != "" { if ci.Mac != "" {
req.Header.Set(DoHMacHeader, ci.Mac) req.Header.Set(dohMacHeader, ci.Mac)
} }
if ci.IP != "" { if ci.IP != "" {
req.Header.Set(DoHIPHeader, ci.IP) req.Header.Set(dohIPHeader, ci.IP)
} }
if ci.Hostname != "" { if ci.Hostname != "" {
req.Header.Set(DoHHostHeader, ci.Hostname) req.Header.Set(dohHostHeader, ci.Hostname)
} }
} }
} }
+1
View File
@@ -33,6 +33,7 @@ func (r *dotResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
endpoint := r.uc.Endpoint endpoint := r.uc.Endpoint
if r.uc.BootstrapIP != "" { if r.uc.BootstrapIP != "" {
dnsClient.TLSConfig.ServerName = r.uc.Domain dnsClient.TLSConfig.ServerName = r.uc.Domain
dnsClient.Net = "tcp-tls"
_, port, _ := net.SplitHostPort(endpoint) _, port, _ := net.SplitHostPort(endpoint)
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port) endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
} }
+15 -5
View File
@@ -17,9 +17,11 @@ import (
) )
const ( const (
apiDomain = "api.controld.com" apiDomainCom = "api.controld.com"
resolverDataURL = "https://api.controld.com/utility" apiDomainDev = "api.controld.dev"
InvalidConfigCode = 40401 resolverDataURLCom = "https://api.controld.com/utility"
resolverDataURLDev = "https://api.controld.dev/utility"
InvalidConfigCode = 40401
) )
// ResolverConfig represents Control D resolver data. // ResolverConfig represents Control D resolver data.
@@ -54,9 +56,13 @@ type utilityRequest struct {
} }
// FetchResolverConfig fetch Control D config for given uid. // FetchResolverConfig fetch Control D config for given uid.
func FetchResolverConfig(uid, version string) (*ResolverConfig, error) { func FetchResolverConfig(uid, version string, cdDev bool) (*ResolverConfig, error) {
body, _ := json.Marshal(utilityRequest{UID: uid}) body, _ := json.Marshal(utilityRequest{UID: uid})
req, err := http.NewRequest("POST", resolverDataURL, bytes.NewReader(body)) apiUrl := resolverDataURLCom
if cdDev {
apiUrl = resolverDataURLDev
}
req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(body))
if err != nil { if err != nil {
return nil, fmt.Errorf("http.NewRequest: %w", err) return nil, fmt.Errorf("http.NewRequest: %w", err)
} }
@@ -67,6 +73,10 @@ func FetchResolverConfig(uid, version string) (*ResolverConfig, error) {
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
transport := http.DefaultTransport.(*http.Transport).Clone() transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
apiDomain := apiDomainCom
if cdDev {
apiDomain = apiDomainDev
}
ips := ctrld.LookupIP(apiDomain) ips := ctrld.LookupIP(apiDomain)
if len(ips) == 0 { if len(ips) == 0 {
ctrld.ProxyLog.Warn().Msgf("No IPs found for %s, connecting to %s", apiDomain, addr) ctrld.ProxyLog.Warn().Msgf("No IPs found for %s, connecting to %s", apiDomain, addr)
+5 -3
View File
@@ -13,16 +13,18 @@ func TestFetchResolverConfig(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
uid string uid string
dev bool
wantErr bool wantErr bool
}{ }{
{"valid", "p2", false}, {"valid com", "p2", false, false},
{"invalid uid", "abcd1234", true}, {"valid dev", "p2", true, false},
{"invalid uid", "abcd1234", false, true},
} }
for _, tc := range tests { for _, tc := range tests {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
got, err := FetchResolverConfig(tc.uid, "dev-test") got, err := FetchResolverConfig(tc.uid, "dev-test", tc.dev)
require.False(t, (err != nil) != tc.wantErr, err) require.False(t, (err != nil) != tc.wantErr, err)
if !tc.wantErr { if !tc.wantErr {
assert.NotEmpty(t, got.DOH) assert.NotEmpty(t, got.DOH)
+84 -12
View File
@@ -1,6 +1,7 @@
package router package router
import ( import (
"bufio"
"bytes" "bytes"
"io" "io"
"log" "log"
@@ -15,14 +16,25 @@ import (
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
) )
var clientInfoFiles = []string{ // readClientInfoFunc represents the function for reading client info.
"/tmp/dnsmasq.leases", // ddwrt type readClientInfoFunc func(name string) error
"/tmp/dhcp.leases", // openwrt
"/var/lib/misc/dnsmasq.leases", // merlin // clientInfoFiles specifies client info files and how to read them on supported platforms.
"/mnt/data/udapi-config/dnsmasq.lease", // UDM Pro var clientInfoFiles = map[string]readClientInfoFunc{
"/data/udapi-config/dnsmasq.lease", // UDR "/tmp/dnsmasq.leases": dnsmasqReadClientInfoFile, // ddwrt
"/tmp/dhcp.leases": dnsmasqReadClientInfoFile, // openwrt
"/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // merlin
"/mnt/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDM Pro
"/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDR
"/etc/dhcpd/dhcpd-leases.log": dnsmasqReadClientInfoFile, // Synology
"/tmp/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // Tomato
"/run/dnsmasq-dhcp.leases": dnsmasqReadClientInfoFile, // EdgeOS
"/run/dhcpd.leases": iscDHCPReadClientInfoFile, // EdgeOS
"/var/dhcpd/var/db/dhcpd.leases": iscDHCPReadClientInfoFile, // Pfsense
} }
// watchClientInfoTable watches changes happens in dnsmasq/dhcpd
// lease files, perform updating to mac table if necessary.
func (r *router) watchClientInfoTable() { func (r *router) watchClientInfoTable() {
if r.watcher == nil { if r.watcher == nil {
return return
@@ -32,14 +44,19 @@ func (r *router) watchClientInfoTable() {
select { select {
case <-timer.C: case <-timer.C:
for _, name := range r.watcher.WatchList() { for _, name := range r.watcher.WatchList() {
_ = readClientInfoFile(name) _ = clientInfoFiles[name](name)
} }
case event, ok := <-r.watcher.Events: case event, ok := <-r.watcher.Events:
if !ok { if !ok {
return return
} }
if event.Has(fsnotify.Write) { if event.Has(fsnotify.Write) {
if err := readClientInfoFile(event.Name); err != nil && !os.IsNotExist(err) { readFunc := clientInfoFiles[event.Name]
if readFunc == nil {
log.Println("unknown file format:", event.Name)
continue
}
if err := readFunc(event.Name); err != nil && !os.IsNotExist(err) {
log.Println("could not read client info file:", err) log.Println("could not read client info file:", err)
} }
} }
@@ -52,6 +69,7 @@ func (r *router) watchClientInfoTable() {
} }
} }
// Stop performs tasks need to be done before the router stopped.
func Stop() error { func Stop() error {
if Name() == "" { if Name() == "" {
return nil return nil
@@ -65,6 +83,7 @@ func Stop() error {
return nil return nil
} }
// GetClientInfoByMac returns ClientInfo for the client associated with the given mac.
func GetClientInfoByMac(mac string) *ctrld.ClientInfo { func GetClientInfoByMac(mac string) *ctrld.ClientInfo {
if mac == "" { if mac == "" {
return nil return nil
@@ -78,21 +97,23 @@ func GetClientInfoByMac(mac string) *ctrld.ClientInfo {
return val.(*ctrld.ClientInfo) return val.(*ctrld.ClientInfo)
} }
func readClientInfoFile(name string) error { // dnsmasqReadClientInfoFile populates mac table with client info reading from dnsmasq lease file.
func dnsmasqReadClientInfoFile(name string) error {
f, err := os.Open(name) f, err := os.Open(name)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() defer f.Close()
return readClientInfoReader(f) return dnsmasqReadClientInfoReader(f)
} }
func readClientInfoReader(reader io.Reader) error { // dnsmasqReadClientInfoReader likes dnsmasqReadClientInfoFile, but reading from an io.Reader instead of file.
func dnsmasqReadClientInfoReader(reader io.Reader) error {
r := routerPlatform.Load() r := routerPlatform.Load()
return lineread.Reader(reader, func(line []byte) error { return lineread.Reader(reader, func(line []byte) error {
fields := bytes.Fields(line) fields := bytes.Fields(line)
if len(fields) != 5 { if len(fields) < 4 {
return nil return nil
} }
mac := string(fields[1]) mac := string(fields[1])
@@ -111,6 +132,57 @@ func readClientInfoReader(reader io.Reader) error {
}) })
} }
// iscDHCPReadClientInfoFile populates mac table with client info reading from isc-dhcpd lease file.
func iscDHCPReadClientInfoFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
return iscDHCPReadClientInfoReader(f)
}
// iscDHCPReadClientInfoReader likes iscDHCPReadClientInfoFile, but reading from an io.Reader instead of file.
func iscDHCPReadClientInfoReader(reader io.Reader) error {
r := routerPlatform.Load()
s := bufio.NewScanner(reader)
var ip, mac, hostname string
for s.Scan() {
line := s.Text()
if strings.HasPrefix(line, "}") {
if mac != "" {
r.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname})
ip, mac, hostname = "", "", ""
}
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
switch fields[0] {
case "lease":
ip = normalizeIP(strings.ToLower(fields[1]))
if net.ParseIP(ip) == nil {
log.Printf("invalid ip address entry: %q", ip)
ip = ""
}
case "hardware":
if len(fields) >= 3 {
mac = strings.ToLower(strings.TrimRight(fields[2], ";"))
if _, err := net.ParseMAC(mac); err != nil {
// Invalid mac, skip.
mac = ""
}
}
case "client-hostname":
hostname = strings.Trim(fields[1], `";`)
}
}
return nil
}
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
func normalizeIP(in string) string { func normalizeIP(in string) string {
// dnsmasq may put ip with interface index in lease file, strip it here. // dnsmasq may put ip with interface index in lease file, strip it here.
ip, _, found := strings.Cut(in, "%") ip, _, found := strings.Cut(in, "%")
+43 -6
View File
@@ -1,6 +1,7 @@
package router package router
import ( import (
"io"
"strings" "strings"
"testing" "testing"
@@ -31,31 +32,65 @@ func Test_normalizeIP(t *testing.T) {
func Test_readClientInfoReader(t *testing.T) { func Test_readClientInfoReader(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
in string in string
mac string readFunc func(r io.Reader) error
mac string
}{ }{
{ {
"good", "good dnsmasq",
`1683329857 e6:20:59:b8:c1:6d 192.168.1.186 * 01:e6:20:59:b8:c1:6d `1683329857 e6:20:59:b8:c1:6d 192.168.1.186 * 01:e6:20:59:b8:c1:6d
`, `,
dnsmasqReadClientInfoReader,
"e6:20:59:b8:c1:6d", "e6:20:59:b8:c1:6d",
}, },
{ {
"bad seen on UDMdream machine", "bad dnsmasq seen on UDMdream machine",
`1683329857 e6:20:59:b8:c1:6e 192.168.1.111 * 01:e6:20:59:b8:c1:6e `1683329857 e6:20:59:b8:c1:6e 192.168.1.111 * 01:e6:20:59:b8:c1:6e
duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c
1683322985 117442354 2600:4040:b0e6:b700::111 ASDASD 00:01:00:01:2a:d0:b9:81:00:07:32:4c:1c:07 1683322985 117442354 2600:4040:b0e6:b700::111 ASDASD 00:01:00:01:2a:d0:b9:81:00:07:32:4c:1c:07
`, `,
dnsmasqReadClientInfoReader,
"e6:20:59:b8:c1:6e", "e6:20:59:b8:c1:6e",
}, },
{
"isc-dhcpd good",
`lease 192.168.1.1 {
hardware ethernet 00:00:00:00:00:01;
client-hostname "host-1";
}
`,
iscDHCPReadClientInfoReader,
"00:00:00:00:00:01",
},
{
"isc-dhcpd bad mac",
`lease 192.168.1.1 {
hardware ethernet invalid-mac;
client-hostname "host-1";
}
lease 192.168.1.2 {
hardware ethernet 00:00:00:00:00:02;
client-hostname "host-2";
}
`,
iscDHCPReadClientInfoReader,
"00:00:00:00:00:02",
},
{
"",
`1685794060 00:00:00:00:00:04 192.168.0.209 cuonglm-ThinkPad-X1-Carbon-Gen-9 00:00:00:00:00:04 9`,
dnsmasqReadClientInfoReader,
"00:00:00:00:00:04",
},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := routerPlatform.Load() r := routerPlatform.Load()
r.mac.Delete(tc.mac) r.mac.Delete(tc.mac)
if err := readClientInfoReader(strings.NewReader(tc.in)); err != nil { if err := tc.readFunc(strings.NewReader(tc.in)); err != nil {
t.Errorf("readClientInfoReader() error = %v", err) t.Errorf("readClientInfoReader() error = %v", err)
} }
info, existed := r.mac.Load(tc.mac) info, existed := r.mac.Load(tc.mac)
@@ -64,6 +99,8 @@ duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c
} }
if ci, ok := info.(*ctrld.ClientInfo); ok && existed && ci.Mac != tc.mac { if ci, ok := info.(*ctrld.ClientInfo); ok && existed && ci.Mac != tc.mac {
t.Errorf("mac mismatched, got: %q, want: %q", ci.Mac, tc.mac) t.Errorf("mac mismatched, got: %q, want: %q", ci.Mac, tc.mac)
} else {
t.Log(ci)
} }
}) })
} }
+9 -8
View File
@@ -7,9 +7,10 @@ import (
) )
const ( const (
nvramCtrldKeyPrefix = "ctrld_" nvramCtrldKeyPrefix = "ctrld_"
nvramCtrldSetupKey = "ctrld_setup" nvramCtrldSetupKey = "ctrld_setup"
nvramRCStartupKey = "rc_startup" nvramCtrldInstallKey = "ctrld_install"
nvramRCStartupKey = "rc_startup"
) )
//lint:ignore ST1005 This error is for human. //lint:ignore ST1005 This error is for human.
@@ -29,14 +30,14 @@ func setupDDWrt() error {
return err return err
} }
nvramKvMap := nvramKV() nvramKvMap := nvramSetupKV()
nvramKvMap["dnsmasq_options"] = data nvramKvMap["dnsmasq_options"] = data
if err := nvramSetup(nvramKvMap); err != nil { if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := ddwrtRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
return nil return nil
@@ -44,11 +45,11 @@ func setupDDWrt() error {
func cleanupDDWrt() error { func cleanupDDWrt() error {
// Restore old configs. // Restore old configs.
if err := nvramRestore(nvramKV()); err != nil { if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := ddwrtRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
return nil return nil
+21 -1
View File
@@ -49,7 +49,7 @@ func dnsMasqConf() (string, error) {
var sb strings.Builder var sb strings.Builder
var tmplText string var tmplText string
switch Name() { switch Name() {
case DDWrt, OpenWrt, Ubios: case EdgeOS, DDWrt, OpenWrt, Ubios, Synology, Tomato:
tmplText = dnsMasqConfigContentTmpl tmplText = dnsMasqConfigContentTmpl
case Merlin: case Merlin:
tmplText = merlinDNSMasqPostConfTmpl tmplText = merlinDNSMasqPostConfTmpl
@@ -65,3 +65,23 @@ func dnsMasqConf() (string, error) {
} }
return sb.String(), nil return sb.String(), nil
} }
func restartDNSMasq() error {
switch Name() {
case EdgeOS:
return edgeOSRestartDNSMasq()
case DDWrt:
return ddwrtRestartDNSMasq()
case Merlin:
return merlinRestartDNSMasq()
case OpenWrt:
return openwrtRestartDNSMasq()
case Ubios:
return ubiosRestartDNSMasq()
case Synology:
return synologyRestartDNSMasq()
case Tomato:
return tomatoRestartService(tomatoDNSMasqSvcName)
}
panic("not supported platform")
}
+56
View File
@@ -0,0 +1,56 @@
package router
import (
"fmt"
"os"
"os/exec"
)
const edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf"
func setupEdgeOS() error {
// Disable dnsmasq as DNS server.
dnsMasqConfigContent, err := dnsMasqConf()
if err != nil {
return err
}
if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func cleanupEdgeOS() error {
// Remove the custom dnsmasq config
if err := os.Remove(edgeOSDNSMasqConfigPath); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallEdgeOS() error {
// If "Content Filtering" is enabled, UniFi OS will create firewall rules to intercept all DNS queries
// from outside, and route those queries to separated interfaces (e.g: dnsfilter-2@if79) created by UniFi OS.
// Thus, those queries will never reach ctrld listener. UniFi OS does not provide any mechanism to toggle this
// feature via command line, so there's nothing ctrld can do to disable this feature. For now, reporting an
// error and guiding users to disable the feature using UniFi OS web UI.
if contentFilteringEnabled() {
return errContentFilteringEnabled
}
return nil
}
func edgeOSRestartDNSMasq() error {
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
return fmt.Errorf("edgeosRestartDNSMasq: %s, %w", string(out), err)
}
return nil
}
+4 -4
View File
@@ -35,11 +35,11 @@ func setupMerlin() error {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := merlinRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
if err := nvramSetup(nvramKV()); err != nil { if err := nvramSetKV(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err return err
} }
@@ -48,7 +48,7 @@ func setupMerlin() error {
func cleanupMerlin() error { func cleanupMerlin() error {
// Restore old configs. // Restore old configs.
if err := nvramRestore(nvramKV()); err != nil { if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err return err
} }
buf, err := os.ReadFile(merlinDNSMasqPostConfPath) buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
@@ -60,7 +60,7 @@ func cleanupMerlin() error {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := merlinRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
return nil return nil
+22 -5
View File
@@ -26,7 +26,7 @@ NOTE:
+https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca +https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca
+https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1 +https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1
*/ */
func nvramKV() map[string]string { func nvramSetupKV() map[string]string {
switch Name() { switch Name() {
case DDWrt: case DDWrt:
return map[string]string{ return map[string]string{
@@ -39,11 +39,28 @@ func nvramKV() map[string]string {
return map[string]string{ return map[string]string{
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled. "dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
} }
case Tomato:
return map[string]string{
"dnsmasq_custom": "", // Configuration of dnsmasq set by ctrld, filled by setupTomato.
"dnscrypt_proxy": "0", // Disable DNSCrypt.
"dnssec_enable": "0", // Disable DNSSEC.
"stubby_proxy": "0", // Disable Stubby
}
} }
return nil return nil
} }
func nvramSetup(m map[string]string) error { func nvramInstallKV() map[string]string {
switch Name() {
case Tomato:
return map[string]string{
tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method.
}
}
return nil
}
func nvramSetKV(m map[string]string, setupKey string) error {
// Backup current value, store ctrld's configs. // Backup current value, store ctrld's configs.
for key, value := range m { for key, value := range m {
old, err := nvram("get", key) old, err := nvram("get", key)
@@ -58,7 +75,7 @@ func nvramSetup(m map[string]string) error {
} }
} }
if out, err := nvram("set", nvramCtrldSetupKey+"=1"); err != nil { if out, err := nvram("set", setupKey+"=1"); err != nil {
return fmt.Errorf("%s: %w", out, err) return fmt.Errorf("%s: %w", out, err)
} }
// Commit. // Commit.
@@ -68,7 +85,7 @@ func nvramSetup(m map[string]string) error {
return nil return nil
} }
func nvramRestore(m map[string]string) error { func nvramRestore(m map[string]string, setupKey string) error {
// Restore old configs. // Restore old configs.
for key := range m { for key := range m {
ctrldKey := nvramCtrldKeyPrefix + key ctrldKey := nvramCtrldKeyPrefix + key
@@ -82,7 +99,7 @@ func nvramRestore(m map[string]string) error {
} }
} }
if out, err := nvram("unset", "ctrld_setup"); err != nil { if out, err := nvram("unset", setupKey); err != nil {
return fmt.Errorf("%s: %w", out, err) return fmt.Errorf("%s: %w", out, err)
} }
// Commit. // Commit.
+12 -3
View File
@@ -23,12 +23,21 @@ func IsGLiNet() bool {
return bytes.Contains(buf, []byte(" (glinet")) return bytes.Contains(buf, []byte(" (glinet"))
} }
// IsOldOpenwrt reports whether the router is an "old" version of Openwrt,
// aka versions which don't have "service" command.
func IsOldOpenwrt() bool {
if Name() != OpenWrt {
return false
}
cmd, _ := exec.LookPath("service")
return cmd == ""
}
func setupOpenWrt() error { func setupOpenWrt() error {
// Delete dnsmasq port if set. // Delete dnsmasq port if set.
if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) { if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) {
return err return err
} }
// Disable dnsmasq as DNS server.
dnsMasqConfigContent, err := dnsMasqConf() dnsMasqConfigContent, err := dnsMasqConf()
if err != nil { if err != nil {
return err return err
@@ -41,7 +50,7 @@ func setupOpenWrt() error {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := openwrtRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
return nil return nil
@@ -53,7 +62,7 @@ func cleanupOpenWrt() error {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := openwrtRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
return nil return nil
+66
View File
@@ -0,0 +1,66 @@
package router
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/kardianos/service"
)
const (
rcPath = "/usr/local/etc/rc.d"
unboundRcPath = rcPath + "/unbound"
dnsmasqRcPath = rcPath + "/dnsmasq"
)
func setupPfsense() error {
// If Pfsense is in DNS Resolver mode, ensure no unbound processes running.
_ = exec.Command("killall", "unbound").Run()
// If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running.
_ = exec.Command("killall", "dnsmasq").Run()
return nil
}
func cleanupPfsense(svc *service.Config) error {
if err := os.Remove(filepath.Join(rcPath, svc.Name+".sh")); err != nil {
return fmt.Errorf("os.Remove: %w", err)
}
_ = exec.Command(unboundRcPath, "onerestart").Run()
_ = exec.Command(dnsmasqRcPath, "onerestart").Run()
return nil
}
func postInstallPfsense(svc *service.Config) error {
// pfsense need ".sh" extension for script to be run at boot.
// See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option
oldname := filepath.Join(rcPath, svc.Name)
newname := filepath.Join(rcPath, svc.Name+".sh")
_ = os.Remove(newname)
if err := os.Symlink(oldname, newname); err != nil {
return fmt.Errorf("os.Symlink: %w", err)
}
return nil
}
const pfsenseInitScript = `#!/bin/sh
# PROVIDE: {{.Name}}
# REQUIRE: SERVERS
# REQUIRE: unbound dnsmasq securelevel
# KEYWORD: shutdown
. /etc/rc.subr
name="{{.Name}}"
{{.Name}}_env="IS_DAEMON=1"
pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
daemon_args="-P ${pidfile} -r -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}"
command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
run_rc_command "$1"
`
+94 -61
View File
@@ -19,10 +19,14 @@ import (
) )
const ( const (
OpenWrt = "openwrt" OpenWrt = "openwrt"
DDWrt = "ddwrt" DDWrt = "ddwrt"
Merlin = "merlin" Merlin = "merlin"
Ubios = "ubios" Ubios = "ubios"
Synology = "synology"
Tomato = "tomato"
EdgeOS = "edgeos"
Pfsense = "pfsense"
) )
// ErrNotSupported reports the current router is not supported error. // ErrNotSupported reports the current router is not supported error.
@@ -37,23 +41,36 @@ type router struct {
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
} }
// IsSupported reports whether the given platform is supported by ctrld.
func IsSupported(platform string) bool {
switch platform {
case EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios:
return true
}
return false
}
// SupportedPlatforms return all platforms that can be configured to run with ctrld. // SupportedPlatforms return all platforms that can be configured to run with ctrld.
func SupportedPlatforms() []string { func SupportedPlatforms() []string {
return []string{DDWrt, Merlin, OpenWrt, Ubios} return []string{EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios}
} }
var configureFunc = map[string]func() error{ var configureFunc = map[string]func() error{
DDWrt: setupDDWrt, EdgeOS: setupEdgeOS,
Merlin: setupMerlin, DDWrt: setupDDWrt,
OpenWrt: setupOpenWrt, Merlin: setupMerlin,
Ubios: setupUbiOS, OpenWrt: setupOpenWrt,
Pfsense: setupPfsense,
Synology: setupSynology,
Tomato: setupTomato,
Ubios: setupUbiOS,
} }
// Configure configures things for running ctrld on the router. // Configure configures things for running ctrld on the router.
func Configure(c *ctrld.Config) error { func Configure(c *ctrld.Config) error {
name := Name() name := Name()
switch name { switch name {
case DDWrt, Merlin, OpenWrt, Ubios: case EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios:
if c.HasUpstreamSendClientInfo() { if c.HasUpstreamSendClientInfo() {
r := routerPlatform.Load() r := routerPlatform.Load()
r.sendClientInfo = true r.sendClientInfo = true
@@ -63,8 +80,8 @@ func Configure(c *ctrld.Config) error {
} }
r.watcher = watcher r.watcher = watcher
go r.watchClientInfoTable() go r.watchClientInfoTable()
for _, file := range clientInfoFiles { for file, readClienInfoFunc := range clientInfoFiles {
_ = readClientInfoFile(file) _ = readClienInfoFunc(file)
_ = r.watcher.Add(file) _ = r.watcher.Add(file)
} }
} }
@@ -88,67 +105,53 @@ func ConfigureService(sc *service.Config) error {
} }
case OpenWrt: case OpenWrt:
sc.Option["SysvScript"] = openWrtScript sc.Option["SysvScript"] = openWrtScript
case Merlin, Ubios: case Pfsense:
sc.Option["SysvScript"] = pfsenseInitScript
case EdgeOS, Merlin, Synology, Tomato, Ubios:
} }
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) {
if Name() != DDWrt { // On some routers, NTP may out of sync, so waiting for it to be ready.
switch Name() {
case Merlin, Tomato:
// 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"))
}
default:
return nil return nil
} }
pidFile := "/tmp/ctrld.pid"
// On Merlin, NTP may out of sync, so waiting for it to be ready.
//
// 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 := merlinRestartDNSMasq(); rerr != nil {
err = errors.Join(err, rerr)
return
}
}()
if err := merlinRestartDNSMasq(); err != nil {
return fmt.Errorf("PreStart: merlinRestartDNSMasq: %w", err)
}
// Wait until `ntp_read=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"))
}
} }
// PostInstall performs task after installing ctrld on router. // PostInstall performs task after installing ctrld on router.
func PostInstall() error { func PostInstall(svc *service.Config) error {
name := Name() name := Name()
switch name { switch name {
case EdgeOS:
return postInstallEdgeOS()
case DDWrt: case DDWrt:
return postInstallDDWrt() return postInstallDDWrt()
case Merlin: case Merlin:
return postInstallMerlin() return postInstallMerlin()
case OpenWrt: case OpenWrt:
return postInstallOpenWrt() return postInstallOpenWrt()
case Pfsense:
return postInstallPfsense(svc)
case Synology:
return postInstallSynology()
case Tomato:
return postInstallTomato()
case Ubios: case Ubios:
return postInstallUbiOS() return postInstallUbiOS()
} }
@@ -156,15 +159,23 @@ func PostInstall() error {
} }
// Cleanup cleans ctrld setup on the router. // Cleanup cleans ctrld setup on the router.
func Cleanup() error { func Cleanup(svc *service.Config) error {
name := Name() name := Name()
switch name { switch name {
case EdgeOS:
return cleanupEdgeOS()
case DDWrt: case DDWrt:
return cleanupDDWrt() return cleanupDDWrt()
case Merlin: case Merlin:
return cleanupMerlin() return cleanupMerlin()
case OpenWrt: case OpenWrt:
return cleanupOpenWrt() return cleanupOpenWrt()
case Pfsense:
return cleanupPfsense(svc)
case Synology:
return cleanupSynology()
case Tomato:
return cleanupTomato()
case Ubios: case Ubios:
return cleanupUbiOS() return cleanupUbiOS()
} }
@@ -175,8 +186,10 @@ func Cleanup() error {
func ListenAddress() string { func ListenAddress() string {
name := Name() name := Name()
switch name { switch name {
case DDWrt, Merlin, OpenWrt, Ubios: case EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios:
return "127.0.0.1:5354" return "127.0.0.1:5354"
case Pfsense:
// On pfsense, we run ctrld as DNS resolver.
} }
return "" return ""
} }
@@ -194,14 +207,24 @@ func Name() string {
func distroName() string { func distroName() string {
switch { switch {
case bytes.HasPrefix(uname(), []byte("DD-WRT")): case bytes.HasPrefix(unameO(), []byte("DD-WRT")):
return DDWrt return DDWrt
case bytes.HasPrefix(uname(), []byte("ASUSWRT-Merlin")): case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")):
return Merlin return Merlin
case haveFile("/etc/openwrt_version"): case haveFile("/etc/openwrt_version"):
return OpenWrt return OpenWrt
case haveDir("/data/unifi"): case haveDir("/data/unifi"):
return Ubios return Ubios
case bytes.HasPrefix(unameU(), []byte("synology")):
return Synology
case bytes.HasPrefix(unameO(), []byte("Tomato")):
return Tomato
case haveDir("/config/scripts/post-config.d"):
return EdgeOS
case haveFile("/etc/ubnt/init/vyatta-router"):
return EdgeOS // For 2.x
case isPfsense():
return Pfsense
} }
return "" return ""
} }
@@ -216,7 +239,17 @@ func haveDir(dir string) bool {
return fi != nil && fi.IsDir() return fi != nil && fi.IsDir()
} }
func uname() []byte { func unameO() []byte {
out, _ := exec.Command("uname", "-o").Output() out, _ := exec.Command("uname", "-o").Output()
return out return out
} }
func unameU() []byte {
out, _ := exec.Command("uname", "-u").Output()
return out
}
func isPfsense() bool {
b, err := os.ReadFile("/etc/platform")
return err == nil && bytes.HasPrefix(b, []byte("pfSense"))
}
+9
View File
@@ -48,6 +48,15 @@ func init() {
}, },
new: newUbiosService, new: newUbiosService,
}, },
&linuxSystemService{
name: "tomato",
detect: func() bool { return Name() == Tomato },
interactive: func() bool {
is, _ := isInteractive()
return is
},
new: newTomatoService,
},
} }
systems = append(systems, service.AvailableSystems()...) systems = append(systems, service.AvailableSystems()...)
service.ChooseSystem(systems...) service.ChooseSystem(systems...)
+278
View File
@@ -0,0 +1,278 @@
package router
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"text/template"
"github.com/kardianos/service"
)
const tomatoNvramScriptWanupKey = "script_wanup"
type tomatoSvc struct {
i service.Interface
platform string
*service.Config
}
func newTomatoService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
s := &tomatoSvc{
i: i,
platform: platform,
Config: c,
}
return s, nil
}
func (s *tomatoSvc) String() string {
if len(s.DisplayName) > 0 {
return s.DisplayName
}
return s.Name
}
func (s *tomatoSvc) Platform() string {
return s.platform
}
func (s *tomatoSvc) configPath() string {
path, err := os.Executable()
if err != nil {
return ""
}
return path + ".startup"
}
func (s *tomatoSvc) template() *template.Template {
return template.Must(template.New("").Parse(tomatoSvcScript))
}
func (s *tomatoSvc) Install() error {
exePath, err := os.Executable()
if err != nil {
return err
}
if !strings.HasPrefix(exePath, "/jffs/") {
return errors.New("could not install service outside /jffs")
}
if _, err := nvram("set", "jffs2_on=1"); err != nil {
return err
}
if _, err := nvram("commit"); err != nil {
return err
}
confPath := s.configPath()
if _, err := os.Stat(confPath); err == nil {
return fmt.Errorf("already installed: %s", confPath)
}
var to = &struct {
*service.Config
Path string
}{
s.Config,
exePath,
}
f, err := os.Create(confPath)
if err != nil {
return fmt.Errorf("os.Create: %w", err)
}
defer f.Close()
if err := s.template().Execute(f, to); err != nil {
return fmt.Errorf("s.template.Execute: %w", err)
}
if err = os.Chmod(confPath, 0755); err != nil {
return fmt.Errorf("os.Chmod: startup script: %w", err)
}
nvramKvMap := nvramInstallKV()
old, err := nvram("get", tomatoNvramScriptWanupKey)
if err != nil {
return fmt.Errorf("nvram: %w", err)
}
nvramKvMap[tomatoNvramScriptWanupKey] = strings.Join([]string{old, s.configPath() + " start"}, "\n")
if err := nvramSetKV(nvramKvMap, nvramCtrldInstallKey); err != nil {
return err
}
return nil
}
func (s *tomatoSvc) Uninstall() error {
if err := os.Remove(s.configPath()); err != nil {
return fmt.Errorf("os.Remove: %w", err)
}
// Restore old configs.
if err := nvramRestore(nvramInstallKV(), nvramCtrldInstallKey); err != nil {
return err
}
return nil
}
func (s *tomatoSvc) Logger(errs chan<- error) (service.Logger, error) {
if service.Interactive() {
return service.ConsoleLogger, nil
}
return s.SystemLogger(errs)
}
func (s *tomatoSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
return newSysLogger(s.Name, errs)
}
func (s *tomatoSvc) Run() (err error) {
err = s.i.Start(s)
if err != nil {
return err
}
if interactice, _ := isInteractive(); !interactice {
signal.Ignore(syscall.SIGHUP)
}
var sigChan = make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
<-sigChan
return s.i.Stop(s)
}
func (s *tomatoSvc) Status() (service.Status, error) {
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
return service.StatusUnknown, service.ErrNotInstalled
}
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
if err != nil {
return service.StatusUnknown, err
}
switch string(bytes.TrimSpace(out)) {
case "running":
return service.StatusRunning, nil
default:
return service.StatusStopped, nil
}
}
func (s *tomatoSvc) Start() error {
return exec.Command(s.configPath(), "start").Run()
}
func (s *tomatoSvc) Stop() error {
return exec.Command(s.configPath(), "stop").Run()
}
func (s *tomatoSvc) Restart() error {
return exec.Command(s.configPath(), "restart").Run()
}
// https://wiki.freshtomato.org/doku.php/freshtomato_zerotier?s[]=%2Aservice%2A
const tomatoSvcScript = `#!/bin/sh
NAME="{{.Name}}"
CMD="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
LOG_FILE="/var/log/${NAME}.log"
PID_FILE="/tmp/$NAME.pid"
alias elog="logger -t $NAME -s"
COND=$1
[ $# -eq 0 ] && COND="start"
get_pid() {
cat "$PID_FILE"
}
is_running() {
[ -f "$PID_FILE" ] && ps | grep -q "^ *$(get_pid) "
}
start() {
if is_running; then
elog "$NAME is already running."
exit 1
fi
elog "Starting $NAME Services: "
$CMD &
echo $! > "$PID_FILE"
chmod 600 "$PID_FILE"
if is_running; then
elog "succeeded."
else
elog "failed."
fi
}
stop() {
if ! is_running; then
elog "$NAME is not running."
exit 1
fi
elog "Shutting down $NAME Services: "
kill -SIGTERM "$(get_pid)"
for _ in 1 2 3 4 5; do
if ! is_running; then
if [ -f "$pid_file" ]; then
rm "$pid_file"
fi
return 0
fi
printf "."
sleep 2
done
if ! is_running; then
elog "succeeded."
else
elog "failed."
fi
}
do_restart() {
stop
start
}
do_status() {
if ! is_running; then
echo "stopped"
else
echo "running"
fi
}
case "$COND" in
start)
start
;;
stop)
stop
;;
restart)
do_restart
;;
status)
do_status
;;
*)
elog "Usage: $0 (start|stop|restart|status)"
;;
esac
exit 0
`
+55
View File
@@ -0,0 +1,55 @@
package router
import (
"fmt"
"os"
"os/exec"
)
const (
synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf"
synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info"
)
func setupSynology() error {
dnsMasqConfigContent, err := dnsMasqConf()
if err != nil {
return err
}
if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
return err
}
if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil {
return err
}
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func cleanupSynology() error {
// Remove the custom config files.
for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} {
if err := os.Remove(f); err != nil {
return err
}
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallSynology() error {
return nil
}
func synologyRestartDNSMasq() error {
if out, err := exec.Command("/etc/rc.network", "nat-restart-dhcp").CombinedOutput(); err != nil {
return fmt.Errorf("synologyRestartDNSMasq: %s - %w", string(out), err)
}
return nil
}
+82
View File
@@ -0,0 +1,82 @@
package router
import (
"fmt"
"os/exec"
)
const (
tomatoDnsCryptProxySvcName = "dnscrypt-proxy"
tomatoStubbySvcName = "stubby"
tomatoDNSMasqSvcName = "dnsmasq"
)
func setupTomato() error {
// Already setup.
if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" {
return nil
}
data, err := dnsMasqConf()
if err != nil {
return err
}
nvramKvMap := nvramSetupKV()
nvramKvMap["dnsmasq_custom"] = data
if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnscrypt-proxy service.
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
return err
}
// Restart stubby service.
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func postInstallTomato() error {
return nil
}
func cleanupTomato() error {
// Restore old configs.
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnscrypt-proxy service.
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
return err
}
// Restart stubby service.
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
return err
}
// Restart dnsmasq service.
if err := restartDNSMasq(); err != nil {
return err
}
return nil
}
func tomatoRestartService(name string) error {
return tomatoRestartServiceWithKill(name, false)
}
func tomatoRestartServiceWithKill(name string, killBeforeRestart bool) error {
if killBeforeRestart {
_, _ = exec.Command("killall", name).CombinedOutput()
}
if out, err := exec.Command("service", name, "restart").CombinedOutput(); err != nil {
return fmt.Errorf("service restart %s: %s, %w", name, string(out), err)
}
return nil
}
+17 -3
View File
@@ -2,12 +2,17 @@ package router
import ( import (
"bytes" "bytes"
"fmt"
"os" "os"
"strconv" "strconv"
) )
var errContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n
To disable it, folowing instruction here: %s`, toggleContentFilteringLink)
const ( const (
ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf"
toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f"
) )
func setupUbiOS() error { func setupUbiOS() error {
@@ -20,7 +25,7 @@ func setupUbiOS() error {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := ubiosRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
return nil return nil
@@ -32,13 +37,17 @@ func cleanupUbiOS() error {
return err return err
} }
// Restart dnsmasq service. // Restart dnsmasq service.
if err := ubiosRestartDNSMasq(); err != nil { if err := restartDNSMasq(); err != nil {
return err return err
} }
return nil return nil
} }
func postInstallUbiOS() error { func postInstallUbiOS() error {
// See comment in postInstallEdgeOS.
if contentFilteringEnabled() {
return errContentFilteringEnabled
}
return nil return nil
} }
@@ -57,3 +66,8 @@ func ubiosRestartDNSMasq() error {
} }
return proc.Kill() return proc.Kill()
} }
func contentFilteringEnabled() bool {
st, err := os.Stat("/run/dnsfilter/dnsfilter")
return err == nil && !st.IsDir()
}
+46
View File
@@ -0,0 +1,46 @@
package ctrld
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
"tailscale.com/logtail/backoff"
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
)
var (
hasIPv6Once sync.Once
ipv6Available atomic.Bool
)
func hasIPv6() bool {
hasIPv6Once.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
val := ctrldnet.IPv6Available(ctx)
ipv6Available.Store(val)
go probingIPv6(val)
})
return ipv6Available.Load()
}
// TODO(cuonglm): doing poll check natively for supported platforms.
func probingIPv6(old bool) {
b := backoff.NewBackoff("probingIPv6", func(format string, args ...any) {}, 30*time.Second)
bCtx := context.Background()
for {
func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cur := ctrldnet.IPv6Available(ctx)
if ipv6Available.CompareAndSwap(old, cur) {
old = cur
}
}()
b.BackOff(bCtx, errors.New("no change"))
}
}
+33 -6
View File
@@ -12,11 +12,17 @@ import (
) )
const ( const (
ResolverTypeDOH = "doh" // ResolverTypeDOH specifies DoH resolver.
ResolverTypeDOH3 = "doh3" ResolverTypeDOH = "doh"
ResolverTypeDOT = "dot" // ResolverTypeDOH3 specifies DoH3 resolver.
ResolverTypeDOQ = "doq" ResolverTypeDOH3 = "doh3"
ResolverTypeOS = "os" // ResolverTypeDOT specifies DoT resolver.
ResolverTypeDOT = "dot"
// ResolverTypeDOQ specifies DoQ resolver.
ResolverTypeDOQ = "doq"
// ResolverTypeOS specifies OS resolver.
ResolverTypeOS = "os"
// ResolverTypeLegacy specifies legacy resolver.
ResolverTypeLegacy = "legacy" ResolverTypeLegacy = "legacy"
) )
@@ -125,7 +131,14 @@ func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, e
Net: udpNet, Net: udpNet,
Dialer: dialer, Dialer: dialer,
} }
answer, _, err := dnsClient.ExchangeContext(ctx, msg, r.uc.Endpoint) endpoint := r.uc.Endpoint
if r.uc.BootstrapIP != "" {
dnsClient.Net = "udp"
_, port, _ := net.SplitHostPort(endpoint)
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
}
answer, _, err := dnsClient.ExchangeContext(ctx, msg, endpoint)
return answer, err return answer, err
} }
@@ -194,3 +207,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
}