mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
Compare commits
26 Commits
update-rea
...
issue-44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5553490b27 | ||
|
|
eaf39f48a0 | ||
|
|
a5ddbdcb42 | ||
|
|
0c99d27be5 | ||
|
|
b9eb89c02e | ||
|
|
53f8d006f0 | ||
|
|
929de49c7b | ||
|
|
542c4f7daf | ||
|
|
c941f9c621 | ||
|
|
25eae187db | ||
|
|
726a25a7ea | ||
|
|
a46bb152af | ||
|
|
bbfa7c6c22 | ||
|
|
1cd54a48e9 | ||
|
|
2d950eecdf | ||
|
|
b143e46eb0 | ||
|
|
8fda856e24 | ||
|
|
54e63ccf9b | ||
|
|
ee53db1e35 | ||
|
|
fc502b920b | ||
|
|
20eae82f11 | ||
|
|
d2fc530316 | ||
|
|
7ac5555a84 | ||
|
|
15d397d8a6 | ||
|
|
b471adfb09 | ||
|
|
d7a38363e6 |
12
README.md
12
README.md
@@ -9,6 +9,7 @@ A highly configurable DNS forwarding proxy with support for:
|
|||||||
- Multiple upstreams with fallbacks
|
- Multiple upstreams with fallbacks
|
||||||
- Multiple network policy driven DNS query steering
|
- Multiple network policy driven DNS query steering
|
||||||
- Policy driven domain based "split horizon" DNS with wildcard support
|
- Policy driven domain based "split horizon" DNS with wildcard support
|
||||||
|
- Integrations with common router vendors and firmware
|
||||||
|
|
||||||
## TLDR
|
## TLDR
|
||||||
Proxy legacy DNS traffic to secure DNS upstreams in highly configurable ways.
|
Proxy legacy DNS traffic to secure DNS upstreams in highly configurable ways.
|
||||||
@@ -25,12 +26,14 @@ All DNS protocols are supported, including:
|
|||||||
2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
|
2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
|
||||||
3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
|
3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
|
||||||
4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
|
4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
|
||||||
|
5. Deploy on a router and create LAN client specific DNS routing policies from a web GUI (When using ControlD.com).
|
||||||
|
|
||||||
|
|
||||||
## OS Support
|
## OS Support
|
||||||
- Windows (386, amd64, arm)
|
- Windows (386, amd64, arm)
|
||||||
- Mac (amd64, arm64)
|
- Mac (amd64, arm64)
|
||||||
- Linux (386, amd64, arm, mips)
|
- Linux (386, amd64, arm, mips)
|
||||||
|
- FreeBSD
|
||||||
- Common routers (See Router Mode below)
|
- Common routers (See Router Mode below)
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
@@ -153,11 +156,14 @@ For granular control of the service, run the `service` command. Each sub-command
|
|||||||
|
|
||||||
## Router Mode
|
## Router Mode
|
||||||
You can run `ctrld` on any supported router, which will function similarly to the Service Mode mentioned above. The list of supported routers and firmware includes:
|
You can run `ctrld` on any supported router, which will function similarly to the Service Mode mentioned above. The list of supported routers and firmware includes:
|
||||||
- OpenWRT
|
|
||||||
- DD-WRT
|
|
||||||
- Asus Merlin
|
- Asus Merlin
|
||||||
|
- DD-WRT
|
||||||
|
- FreshTomato
|
||||||
- GL.iNet
|
- GL.iNet
|
||||||
- Ubiquiti
|
- OpenWRT
|
||||||
|
- pfSense
|
||||||
|
- Synology
|
||||||
|
- Ubiquiti (UniFi, EdgeOS)
|
||||||
|
|
||||||
In order to start `ctrld` as a DNS provider, simply run `./ctrld setup auto` command.
|
In order to start `ctrld` as a DNS provider, simply run `./ctrld setup auto` command.
|
||||||
|
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
//go:build !linux
|
//go:build !linux && !freebsd
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
57
config.go
57
config.go
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ The config file allows for advanced configuration of the `ctrld` utility to cove
|
|||||||
|
|
||||||
|
|
||||||
## Config Location
|
## Config Location
|
||||||
`ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `config.toml` found in following order:
|
`ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `ctrld.toml` found in following order:
|
||||||
|
|
||||||
- `$HOME/.ctrld`
|
- `/etc/controld` on *nix.
|
||||||
- Current directory
|
- User's home directory on Windows.
|
||||||
|
- Same directory with `ctrld` binary on these routers:
|
||||||
|
- `ddwrt`
|
||||||
|
- `merlin`
|
||||||
|
- `freshtomato`
|
||||||
|
- Current directory.
|
||||||
|
|
||||||
The user can choose to override default value using command line `--config` or `-c`:
|
The user can choose to override default value using command line `--config` or `-c`:
|
||||||
|
|
||||||
@@ -38,6 +43,8 @@ if it's existed.
|
|||||||
log_path = ""
|
log_path = ""
|
||||||
cache_enable = true
|
cache_enable = true
|
||||||
cache_size = 4096
|
cache_size = 4096
|
||||||
|
cache_ttl_override = 60
|
||||||
|
cache_serve_stale = true
|
||||||
|
|
||||||
[network.0]
|
[network.0]
|
||||||
cidrs = ["0.0.0.0/0"]
|
cidrs = ["0.0.0.0/0"]
|
||||||
@@ -53,6 +60,7 @@ if it's existed.
|
|||||||
name = "Control D - Anti-Malware"
|
name = "Control D - Anti-Malware"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doh"
|
type = "doh"
|
||||||
|
ip_stack = "both"
|
||||||
|
|
||||||
[upstream.1]
|
[upstream.1]
|
||||||
bootstrap_ip = "76.76.2.11"
|
bootstrap_ip = "76.76.2.11"
|
||||||
@@ -60,6 +68,7 @@ if it's existed.
|
|||||||
name = "Control D - No Ads"
|
name = "Control D - No Ads"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doq"
|
type = "doq"
|
||||||
|
ip_stack = "split"
|
||||||
|
|
||||||
[upstream.2]
|
[upstream.2]
|
||||||
bootstrap_ip = "76.76.2.22"
|
bootstrap_ip = "76.76.2.22"
|
||||||
@@ -67,6 +76,7 @@ if it's existed.
|
|||||||
name = "Control D - Private"
|
name = "Control D - Private"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "dot"
|
type = "dot"
|
||||||
|
ip_stack = "v4"
|
||||||
|
|
||||||
[listener.0]
|
[listener.0]
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
@@ -104,8 +114,8 @@ Logging level you wish to enable.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
- Valid values: `debug`, `info`, `warn`, `error`, `fatal`, `panic`
|
- Valid values: `debug`, `info`, `warn`, `notice`, `error`, `fatal`, `panic`
|
||||||
- Default: `info`
|
- Default: `notice`
|
||||||
|
|
||||||
|
|
||||||
### log_path
|
### log_path
|
||||||
@@ -113,12 +123,14 @@ Relative or absolute path of the log file.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### cache_enable
|
### cache_enable
|
||||||
When `cache_enable = true`, all resolved DNS query responses will be cached for duration of the upstream record TTLs.
|
When `cache_enable = true`, all resolved DNS query responses will be cached for duration of the upstream record TTLs.
|
||||||
|
|
||||||
- Type: boolean
|
- Type: boolean
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: false
|
||||||
|
|
||||||
### cache_size
|
### cache_size
|
||||||
The number of cached records, must be a positive integer. Tweaking this value with care depends on your available RAM.
|
The number of cached records, must be a positive integer. Tweaking this value with care depends on your available RAM.
|
||||||
@@ -128,29 +140,22 @@ An invalid `cache_size` value will disable the cache, regardless of `cache_enabl
|
|||||||
|
|
||||||
- Type: int
|
- Type: int
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: 4096
|
||||||
|
|
||||||
### cache_ttl_override
|
### cache_ttl_override
|
||||||
When `cache_ttl_override` is set to a positive value (in seconds), TTLs are overridden to this value and cached for this long.
|
When `cache_ttl_override` is set to a positive value (in seconds), TTLs are overridden to this value and cached for this long.
|
||||||
|
|
||||||
- Type: int
|
- Type: int
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: 0
|
||||||
|
|
||||||
### cache_serve_stale
|
### cache_serve_stale
|
||||||
When `cache_serve_stale = true`, in cases of upstream failures (upstreams not reachable), `ctrld` will keep serving
|
When `cache_serve_stale = true`, in cases of upstream failures (upstreams not reachable), `ctrld` will keep serving
|
||||||
stale cached records (regardless of their TTLs) until upstream comes online.
|
stale cached records (regardless of their TTLs) until upstream comes online.
|
||||||
|
|
||||||
The above config will look like this at query time.
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
```
|
- Default: false
|
||||||
2022-11-14T22:18:53.808 INF Setting bootstrap IP for upstream.0 bootstrap_ip=76.76.2.11
|
|
||||||
2022-11-14T22:18:53.808 INF Starting DNS server on listener.0: 127.0.0.1:53
|
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] 127.0.0.1:53978 -> listener.0: 127.0.0.1:53: received query: verify.controld.com
|
|
||||||
2022-11-14T22:18:56.381 INF [9fd5d3] no policy, no network, no rule -> [upstream.0]
|
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] sending query to upstream.0: Control D - DOH Free
|
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] debug dial context freedns.controld.com:443 - tcp - 76.76.2.0
|
|
||||||
2022-11-14T22:18:56.381 DBG [9fd5d3] sending doh request to: 76.76.2.11:443
|
|
||||||
2022-11-14T22:18:56.420 DBG [9fd5d3] received response of 118 bytes in 39.662597ms
|
|
||||||
```
|
|
||||||
|
|
||||||
## Upstream
|
## Upstream
|
||||||
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
||||||
@@ -162,6 +167,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Control D - DOH"
|
name = "Control D - DOH"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doh"
|
type = "doh"
|
||||||
|
ip_stack = "split"
|
||||||
|
|
||||||
[upstream.1]
|
[upstream.1]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -169,6 +175,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Control D - DOH3"
|
name = "Control D - DOH3"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doh3"
|
type = "doh3"
|
||||||
|
ip_stack = "both"
|
||||||
|
|
||||||
[upstream.2]
|
[upstream.2]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -176,6 +183,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Controld D - DOT"
|
name = "Controld D - DOT"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "dot"
|
type = "dot"
|
||||||
|
ip_stack = "v4"
|
||||||
|
|
||||||
[upstream.3]
|
[upstream.3]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -183,6 +191,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Controld D - DOT"
|
name = "Controld D - DOT"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "doq"
|
type = "doq"
|
||||||
|
ip_stack = "v6"
|
||||||
|
|
||||||
[upstream.4]
|
[upstream.4]
|
||||||
bootstrap_ip = ""
|
bootstrap_ip = ""
|
||||||
@@ -190,6 +199,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
|||||||
name = "Control D - Ad Blocking"
|
name = "Control D - Ad Blocking"
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
type = "legacy"
|
type = "legacy"
|
||||||
|
ip_stack = "both"
|
||||||
```
|
```
|
||||||
|
|
||||||
### bootstrap_ip
|
### bootstrap_ip
|
||||||
@@ -200,6 +210,7 @@ If `bootstrap_ip` is empty, `ctrld` will resolve this itself using its own boots
|
|||||||
|
|
||||||
- type: ip address string
|
- type: ip address string
|
||||||
- required: no
|
- required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### endpoint
|
### endpoint
|
||||||
IP address, hostname or URL of upstream DNS. Used together with `Type` of the endpoint.
|
IP address, hostname or URL of upstream DNS. Used together with `Type` of the endpoint.
|
||||||
@@ -214,6 +225,7 @@ Human-readable name of the upstream.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### timeout
|
### timeout
|
||||||
Timeout in milliseconds before request failsover to the next upstream (if defined).
|
Timeout in milliseconds before request failsover to the next upstream (if defined).
|
||||||
@@ -221,7 +233,8 @@ Timeout in milliseconds before request failsover to the next upstream (if define
|
|||||||
Value `0` means no timeout.
|
Value `0` means no timeout.
|
||||||
|
|
||||||
- Type: number
|
- Type: number
|
||||||
- required: no
|
- Required: no
|
||||||
|
- Default: 0
|
||||||
|
|
||||||
### type
|
### type
|
||||||
The protocol that `ctrld` will use to send DNS requests to upstream.
|
The protocol that `ctrld` will use to send DNS requests to upstream.
|
||||||
@@ -266,12 +279,14 @@ Name of the network.
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### cidrs
|
### cidrs
|
||||||
Specifies the network addresses that the `listener` will accept requests from. You will see more details in the listener policy section.
|
Specifies the network addresses that the `listener` will accept requests from. You will see more details in the listener policy section.
|
||||||
|
|
||||||
- Type: array of network CIDR string
|
- Type: array of network CIDR string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
|
||||||
|
|
||||||
## listener
|
## listener
|
||||||
@@ -291,18 +306,23 @@ The `[listener]` section specifies the ip and port of the local DNS server. You
|
|||||||
### ip
|
### ip
|
||||||
IP address that serves the incoming requests. If `ip` is empty, ctrld will listen on all available addresses.
|
IP address that serves the incoming requests. If `ip` is empty, ctrld will listen on all available addresses.
|
||||||
|
|
||||||
- Type: ip address
|
- Type: ip address string
|
||||||
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### port
|
### port
|
||||||
Port number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen.
|
Port number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen.
|
||||||
|
|
||||||
- Type: number
|
- Type: number
|
||||||
|
- Required: no
|
||||||
|
- Default: 0
|
||||||
|
|
||||||
### restricted
|
### restricted
|
||||||
If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.
|
If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.
|
||||||
|
|
||||||
- Type: bool
|
- Type: bool
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: false
|
||||||
|
|
||||||
### policy
|
### policy
|
||||||
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
|
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
|
||||||
@@ -346,19 +366,30 @@ rules = [
|
|||||||
|
|
||||||
- Type: string
|
- Type: string
|
||||||
- Required: no
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
### networks:
|
### networks:
|
||||||
`networks` is the list of network rules of the policy.
|
`networks` is the list of network rules of the policy.
|
||||||
|
|
||||||
- type: array of networks
|
- Type: array of networks
|
||||||
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
|
||||||
### rules:
|
### rules:
|
||||||
`rules` is the list of domain rules within the policy. Domain can be either FQDN or wildcard domain.
|
`rules` is the list of domain rules within the policy. Domain can be either FQDN or wildcard domain.
|
||||||
|
|
||||||
- type: array of rule
|
- Type: array of rule
|
||||||
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
|
||||||
### failover_rcodes
|
### failover_rcodes
|
||||||
For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`. For example:
|
For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`.
|
||||||
|
|
||||||
|
- Type: array of string
|
||||||
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
-
|
||||||
|
For example:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[listener.0.policy]
|
[listener.0.policy]
|
||||||
|
|||||||
13
doh.go
13
doh.go
@@ -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
dot.go
1
dot.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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, "%")
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
internal/router/edgeos.go
Normal file
56
internal/router/edgeos.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
internal/router/pfsense.go
Normal file
66
internal/router/pfsense.go
Normal 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"
|
||||||
|
`
|
||||||
@@ -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"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
internal/router/service_tomato.go
Normal file
278
internal/router/service_tomato.go
Normal 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
internal/router/synology.go
Normal file
55
internal/router/synology.go
Normal 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
internal/router/tomato.go
Normal file
82
internal/router/tomato.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
net.go
Normal file
46
net.go
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
39
resolver.go
39
resolver.go
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user