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 network policy driven DNS query steering
|
||||
- Policy driven domain based "split horizon" DNS with wildcard support
|
||||
- Integrations with common router vendors and firmware
|
||||
|
||||
## TLDR
|
||||
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.
|
||||
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.
|
||||
5. Deploy on a router and create LAN client specific DNS routing policies from a web GUI (When using ControlD.com).
|
||||
|
||||
|
||||
## OS Support
|
||||
- Windows (386, amd64, arm)
|
||||
- Mac (amd64, arm64)
|
||||
- Linux (386, amd64, arm, mips)
|
||||
- FreeBSD
|
||||
- Common routers (See Router Mode below)
|
||||
|
||||
# Install
|
||||
@@ -153,11 +156,14 @@ For granular control of the service, run the `service` command. Each sub-command
|
||||
|
||||
## 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:
|
||||
- OpenWRT
|
||||
- DD-WRT
|
||||
- Asus Merlin
|
||||
- DD-WRT
|
||||
- FreshTomato
|
||||
- GL.iNet
|
||||
- Ubiquiti
|
||||
- OpenWRT
|
||||
- pfSense
|
||||
- Synology
|
||||
- Ubiquiti (UniFi, EdgeOS)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
const selfCheckFQDN = "verify.controld.com"
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
@@ -138,16 +136,7 @@ func initCLI() {
|
||||
mainLog.Fatal().Err(err).Msg("failed create new service")
|
||||
}
|
||||
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 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")
|
||||
}
|
||||
}()
|
||||
@@ -176,9 +165,13 @@ func initCLI() {
|
||||
initLogging()
|
||||
|
||||
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")
|
||||
}
|
||||
if err := s.Shutdown(); err != nil && errCh != nil {
|
||||
mainLog.Fatal().Err(err).Msg("failed to shutdown dns server for ntpd")
|
||||
}
|
||||
}
|
||||
|
||||
processCDFlags()
|
||||
@@ -241,6 +234,8 @@ func initCLI() {
|
||||
runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
||||
runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
||||
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().MarkHidden("homedir")
|
||||
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||
@@ -298,6 +293,10 @@ func initCLI() {
|
||||
|
||||
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,
|
||||
// or sub-process env is different with the parent, we still behave correctly and use
|
||||
// the expected config file.
|
||||
@@ -319,7 +318,7 @@ func initCLI() {
|
||||
{s.Start, true},
|
||||
}
|
||||
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")
|
||||
return
|
||||
}
|
||||
@@ -329,7 +328,8 @@ func initCLI() {
|
||||
return
|
||||
}
|
||||
|
||||
status = selfCheckStatus(status)
|
||||
domain := cfg.Upstream["0"].VerifyDomain()
|
||||
status = selfCheckStatus(status, domain)
|
||||
switch status {
|
||||
case service.StatusRunning:
|
||||
mainLog.Notice().Msg("Service started")
|
||||
@@ -354,6 +354,8 @@ func initCLI() {
|
||||
startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
||||
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
||||
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().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`)
|
||||
_ = startCmd.Flags().MarkHidden("router")
|
||||
@@ -474,7 +476,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
||||
}
|
||||
prog.resetDNS()
|
||||
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.Notice().Msg("Service uninstalled")
|
||||
@@ -708,7 +710,7 @@ func processCDFlags() {
|
||||
}
|
||||
logger := mainLog.With().Str("mode", "cd").Logger()
|
||||
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 {
|
||||
s, err := service.New(&prog{}, svcConfig)
|
||||
if err != nil {
|
||||
@@ -854,17 +856,25 @@ func netInterface(ifaceName string) (*net.Interface, error) {
|
||||
func defaultIfaceName() string {
|
||||
dri, err := interfaces.DefaultRouteInterface()
|
||||
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")
|
||||
}
|
||||
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)
|
||||
bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
|
||||
bo.LogLongerThan = 500 * time.Millisecond
|
||||
ctx := context.Background()
|
||||
err := errors.New("query failed")
|
||||
maxAttempts := 20
|
||||
mainLog.Debug().Msg("Performing self-check")
|
||||
var (
|
||||
@@ -888,16 +898,16 @@ func selfCheckStatus(status service.Status) service.Status {
|
||||
}
|
||||
mu.Unlock()
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(selfCheckFQDN+".", dns.TypeA)
|
||||
m.SetQuestion(domain+".", dns.TypeA)
|
||||
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 {
|
||||
mainLog.Debug().Msgf("self-check against %q succeeded", selfCheckFQDN)
|
||||
mainLog.Debug().Msgf("self-check against %q succeeded", domain)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -907,7 +917,7 @@ func unsupportedPlatformHelp(cmd *cobra.Command) {
|
||||
|
||||
func userHomeDir() (string, error) {
|
||||
switch router.Name() {
|
||||
case router.DDWrt, router.Merlin:
|
||||
case router.DDWrt, router.Merlin, router.Tomato:
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build linux || freebsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -42,12 +44,11 @@ func initRouterCLI() {
|
||||
if platform == "auto" {
|
||||
platform = router.Name()
|
||||
}
|
||||
switch platform {
|
||||
case router.DDWrt, router.Merlin, router.OpenWrt, router.Ubios:
|
||||
default:
|
||||
if !router.IsSupported(platform) {
|
||||
unsupportedPlatformHelp(cmd)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
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().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
||||
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`)
|
||||
|
||||
tmpl := routerCmd.UsageTemplate()
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !linux
|
||||
//go:build !linux && !freebsd
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -50,11 +50,12 @@ func (p *prog) serveDNS(listenerNum string) error {
|
||||
q := m.Question[0]
|
||||
domain := canonicalName(q.Name)
|
||||
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()
|
||||
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
|
||||
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
|
||||
if !matched && listenerConfig.Restricted {
|
||||
answer = new(dns.Msg)
|
||||
@@ -418,6 +419,28 @@ func macFromMsg(msg *dns.Msg) string {
|
||||
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,
|
||||
// with the given handler. It ensures the server has started listening.
|
||||
// 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()
|
||||
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)
|
||||
}
|
||||
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}}
|
||||
if tc.wantMac {
|
||||
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
|
||||
silent bool
|
||||
cdUID string
|
||||
cdDev bool
|
||||
iface string
|
||||
ifaceStartStop string
|
||||
setupRouter bool
|
||||
|
||||
@@ -175,7 +175,10 @@ func (p *prog) setDNS() {
|
||||
switch router.Name() {
|
||||
case router.DDWrt, router.OpenWrt, router.Ubios:
|
||||
// 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
|
||||
}
|
||||
if cfg.Listener == nil || cfg.Listener["0"] == nil {
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
)
|
||||
|
||||
func (p *prog) preRun() {
|
||||
@@ -17,6 +19,13 @@ func setDependencies(svc *service.Config) {
|
||||
"Wants=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) {
|
||||
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
|
||||
func newService(s service.Service) service.Service {
|
||||
// TODO: unify for other SysV system.
|
||||
if router.IsGLiNet() {
|
||||
switch {
|
||||
case router.IsGLiNet(), router.IsOldOpenwrt():
|
||||
return &sysV{s}
|
||||
}
|
||||
return s
|
||||
|
||||
57
config.go
57
config.go
@@ -24,14 +24,31 @@ import (
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
)
|
||||
|
||||
// IpStackBoth ...
|
||||
const (
|
||||
IpStackBoth = "both"
|
||||
IpStackV4 = "v4"
|
||||
IpStackV6 = "v6"
|
||||
// IpStackBoth indicates that ctrld will use either ipv4 or ipv6 for connecting to upstream,
|
||||
// depending on which stack is available when receiving the DNS query.
|
||||
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"
|
||||
|
||||
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.
|
||||
// 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
|
||||
// configured to send client info to Control D DNS server.
|
||||
//
|
||||
@@ -224,6 +258,7 @@ func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// BootstrapIPs returns the bootstrap IPs list of upstreams.
|
||||
func (uc *UpstreamConfig) BootstrapIPs() []string {
|
||||
return uc.bootstrapIPs
|
||||
}
|
||||
@@ -347,9 +382,7 @@ func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() {
|
||||
uc.transport = uc.newDOHTransport(uc.bootstrapIPs6)
|
||||
case IpStackSplit:
|
||||
uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if ctrldnet.IPv6Available(ctx) {
|
||||
if hasIPv6() {
|
||||
uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6)
|
||||
} else {
|
||||
uc.transport6 = uc.transport4
|
||||
@@ -419,7 +452,10 @@ func (uc *UpstreamConfig) bootstrapIPForDNSType(dnsType uint16) string {
|
||||
case dns.TypeA:
|
||||
return pick(uc.bootstrapIPs4)
|
||||
default:
|
||||
return pick(uc.bootstrapIPs6)
|
||||
if hasIPv6() {
|
||||
return pick(uc.bootstrapIPs6)
|
||||
}
|
||||
return pick(uc.bootstrapIPs4)
|
||||
}
|
||||
}
|
||||
return pick(uc.bootstrapIPs)
|
||||
@@ -438,7 +474,10 @@ func (uc *UpstreamConfig) netForDNSType(dnsType uint16) (string, string) {
|
||||
case dns.TypeA:
|
||||
return "tcp4-tls", "udp4"
|
||||
default:
|
||||
return "tcp6-tls", "udp6"
|
||||
if hasIPv6() {
|
||||
return "tcp6-tls", "udp6"
|
||||
}
|
||||
return "tcp4-tls", "udp4"
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return &b
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ type parallelDialerResult 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) {
|
||||
if len(addrs) == 0 {
|
||||
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
|
||||
`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`
|
||||
- Current directory
|
||||
- `/etc/controld` on *nix.
|
||||
- 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`:
|
||||
|
||||
@@ -38,6 +43,8 @@ if it's existed.
|
||||
log_path = ""
|
||||
cache_enable = true
|
||||
cache_size = 4096
|
||||
cache_ttl_override = 60
|
||||
cache_serve_stale = true
|
||||
|
||||
[network.0]
|
||||
cidrs = ["0.0.0.0/0"]
|
||||
@@ -53,6 +60,7 @@ if it's existed.
|
||||
name = "Control D - Anti-Malware"
|
||||
timeout = 5000
|
||||
type = "doh"
|
||||
ip_stack = "both"
|
||||
|
||||
[upstream.1]
|
||||
bootstrap_ip = "76.76.2.11"
|
||||
@@ -60,6 +68,7 @@ if it's existed.
|
||||
name = "Control D - No Ads"
|
||||
timeout = 5000
|
||||
type = "doq"
|
||||
ip_stack = "split"
|
||||
|
||||
[upstream.2]
|
||||
bootstrap_ip = "76.76.2.22"
|
||||
@@ -67,6 +76,7 @@ if it's existed.
|
||||
name = "Control D - Private"
|
||||
timeout = 5000
|
||||
type = "dot"
|
||||
ip_stack = "v4"
|
||||
|
||||
[listener.0]
|
||||
ip = "127.0.0.1"
|
||||
@@ -104,8 +114,8 @@ Logging level you wish to enable.
|
||||
|
||||
- Type: string
|
||||
- Required: no
|
||||
- Valid values: `debug`, `info`, `warn`, `error`, `fatal`, `panic`
|
||||
- Default: `info`
|
||||
- Valid values: `debug`, `info`, `warn`, `notice`, `error`, `fatal`, `panic`
|
||||
- Default: `notice`
|
||||
|
||||
|
||||
### log_path
|
||||
@@ -113,12 +123,14 @@ Relative or absolute path of the log file.
|
||||
|
||||
- Type: string
|
||||
- Required: no
|
||||
- Default: ""
|
||||
|
||||
### cache_enable
|
||||
When `cache_enable = true`, all resolved DNS query responses will be cached for duration of the upstream record TTLs.
|
||||
|
||||
- Type: boolean
|
||||
- Required: no
|
||||
- Default: false
|
||||
|
||||
### cache_size
|
||||
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
|
||||
- Required: no
|
||||
- Default: 4096
|
||||
|
||||
### 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.
|
||||
|
||||
- Type: int
|
||||
- Required: no
|
||||
- Default: 0
|
||||
|
||||
### cache_serve_stale
|
||||
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.
|
||||
|
||||
The above config will look like this at query time.
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
- Type: boolean
|
||||
- Required: no
|
||||
- Default: false
|
||||
|
||||
## Upstream
|
||||
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"
|
||||
timeout = 5000
|
||||
type = "doh"
|
||||
ip_stack = "split"
|
||||
|
||||
[upstream.1]
|
||||
bootstrap_ip = ""
|
||||
@@ -169,6 +175,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
||||
name = "Control D - DOH3"
|
||||
timeout = 5000
|
||||
type = "doh3"
|
||||
ip_stack = "both"
|
||||
|
||||
[upstream.2]
|
||||
bootstrap_ip = ""
|
||||
@@ -176,6 +183,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
||||
name = "Controld D - DOT"
|
||||
timeout = 5000
|
||||
type = "dot"
|
||||
ip_stack = "v4"
|
||||
|
||||
[upstream.3]
|
||||
bootstrap_ip = ""
|
||||
@@ -183,6 +191,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
||||
name = "Controld D - DOT"
|
||||
timeout = 5000
|
||||
type = "doq"
|
||||
ip_stack = "v6"
|
||||
|
||||
[upstream.4]
|
||||
bootstrap_ip = ""
|
||||
@@ -190,6 +199,7 @@ The `[upstream]` section specifies the DNS upstream servers that `ctrld` will fo
|
||||
name = "Control D - Ad Blocking"
|
||||
timeout = 5000
|
||||
type = "legacy"
|
||||
ip_stack = "both"
|
||||
```
|
||||
|
||||
### bootstrap_ip
|
||||
@@ -200,6 +210,7 @@ If `bootstrap_ip` is empty, `ctrld` will resolve this itself using its own boots
|
||||
|
||||
- type: ip address string
|
||||
- required: no
|
||||
- Default: ""
|
||||
|
||||
### 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
|
||||
- Required: no
|
||||
- Default: ""
|
||||
|
||||
### timeout
|
||||
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.
|
||||
|
||||
- Type: number
|
||||
- required: no
|
||||
- Required: no
|
||||
- Default: 0
|
||||
|
||||
### type
|
||||
The protocol that `ctrld` will use to send DNS requests to upstream.
|
||||
@@ -266,12 +279,14 @@ Name of the network.
|
||||
|
||||
- Type: string
|
||||
- Required: no
|
||||
- Default: ""
|
||||
|
||||
### cidrs
|
||||
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
|
||||
- Required: no
|
||||
- Default: []
|
||||
|
||||
|
||||
## listener
|
||||
@@ -291,18 +306,23 @@ The `[listener]` section specifies the ip and port of the local DNS server. You
|
||||
### ip
|
||||
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 number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen.
|
||||
|
||||
- Type: number
|
||||
- Required: no
|
||||
- Default: 0
|
||||
|
||||
### 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`.
|
||||
|
||||
- Type: bool
|
||||
- Required: no
|
||||
- Default: false
|
||||
|
||||
### policy
|
||||
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
|
||||
@@ -346,19 +366,30 @@ rules = [
|
||||
|
||||
- Type: string
|
||||
- Required: no
|
||||
- Default: ""
|
||||
|
||||
### networks:
|
||||
`networks` is the list of network rules of the policy.
|
||||
|
||||
- type: array of networks
|
||||
- Type: array of networks
|
||||
- Required: no
|
||||
- Default: []
|
||||
|
||||
### rules:
|
||||
`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
|
||||
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
|
||||
[listener.0.policy]
|
||||
|
||||
13
doh.go
13
doh.go
@@ -13,10 +13,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DoHMacHeader = "x-cd-mac"
|
||||
DoHIPHeader = "x-cd-ip"
|
||||
DoHHostHeader = "x-cd-host"
|
||||
|
||||
dohMacHeader = "x-cd-mac"
|
||||
dohIPHeader = "x-cd-ip"
|
||||
dohHostHeader = "x-cd-host"
|
||||
headerApplicationDNS = "application/dns-message"
|
||||
)
|
||||
|
||||
@@ -101,13 +100,13 @@ func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
||||
if sendClientInfo {
|
||||
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
||||
if ci.Mac != "" {
|
||||
req.Header.Set(DoHMacHeader, ci.Mac)
|
||||
req.Header.Set(dohMacHeader, ci.Mac)
|
||||
}
|
||||
if ci.IP != "" {
|
||||
req.Header.Set(DoHIPHeader, ci.IP)
|
||||
req.Header.Set(dohIPHeader, ci.IP)
|
||||
}
|
||||
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
|
||||
if r.uc.BootstrapIP != "" {
|
||||
dnsClient.TLSConfig.ServerName = r.uc.Domain
|
||||
dnsClient.Net = "tcp-tls"
|
||||
_, port, _ := net.SplitHostPort(endpoint)
|
||||
endpoint = net.JoinHostPort(r.uc.BootstrapIP, port)
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
apiDomain = "api.controld.com"
|
||||
resolverDataURL = "https://api.controld.com/utility"
|
||||
InvalidConfigCode = 40401
|
||||
apiDomainCom = "api.controld.com"
|
||||
apiDomainDev = "api.controld.dev"
|
||||
resolverDataURLCom = "https://api.controld.com/utility"
|
||||
resolverDataURLDev = "https://api.controld.dev/utility"
|
||||
InvalidConfigCode = 40401
|
||||
)
|
||||
|
||||
// ResolverConfig represents Control D resolver data.
|
||||
@@ -54,9 +56,13 @@ type utilityRequest struct {
|
||||
}
|
||||
|
||||
// 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})
|
||||
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 {
|
||||
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")
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
apiDomain := apiDomainCom
|
||||
if cdDev {
|
||||
apiDomain = apiDomainDev
|
||||
}
|
||||
ips := ctrld.LookupIP(apiDomain)
|
||||
if len(ips) == 0 {
|
||||
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 {
|
||||
name string
|
||||
uid string
|
||||
dev bool
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", "p2", false},
|
||||
{"invalid uid", "abcd1234", true},
|
||||
{"valid com", "p2", false, false},
|
||||
{"valid dev", "p2", true, false},
|
||||
{"invalid uid", "abcd1234", false, true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
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)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, got.DOH)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
@@ -15,14 +16,25 @@ import (
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
var clientInfoFiles = []string{
|
||||
"/tmp/dnsmasq.leases", // ddwrt
|
||||
"/tmp/dhcp.leases", // openwrt
|
||||
"/var/lib/misc/dnsmasq.leases", // merlin
|
||||
"/mnt/data/udapi-config/dnsmasq.lease", // UDM Pro
|
||||
"/data/udapi-config/dnsmasq.lease", // UDR
|
||||
// readClientInfoFunc represents the function for reading client info.
|
||||
type readClientInfoFunc func(name string) error
|
||||
|
||||
// clientInfoFiles specifies client info files and how to read them on supported platforms.
|
||||
var clientInfoFiles = map[string]readClientInfoFunc{
|
||||
"/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() {
|
||||
if r.watcher == nil {
|
||||
return
|
||||
@@ -32,14 +44,19 @@ func (r *router) watchClientInfoTable() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
for _, name := range r.watcher.WatchList() {
|
||||
_ = readClientInfoFile(name)
|
||||
_ = clientInfoFiles[name](name)
|
||||
}
|
||||
case event, ok := <-r.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -52,6 +69,7 @@ func (r *router) watchClientInfoTable() {
|
||||
}
|
||||
}
|
||||
|
||||
// Stop performs tasks need to be done before the router stopped.
|
||||
func Stop() error {
|
||||
if Name() == "" {
|
||||
return nil
|
||||
@@ -65,6 +83,7 @@ func Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClientInfoByMac returns ClientInfo for the client associated with the given mac.
|
||||
func GetClientInfoByMac(mac string) *ctrld.ClientInfo {
|
||||
if mac == "" {
|
||||
return nil
|
||||
@@ -78,21 +97,23 @@ func GetClientInfoByMac(mac string) *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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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()
|
||||
return lineread.Reader(reader, func(line []byte) error {
|
||||
fields := bytes.Fields(line)
|
||||
if len(fields) != 5 {
|
||||
if len(fields) < 4 {
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
// dnsmasq may put ip with interface index in lease file, strip it here.
|
||||
ip, _, found := strings.Cut(in, "%")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -31,31 +32,65 @@ func Test_normalizeIP(t *testing.T) {
|
||||
|
||||
func Test_readClientInfoReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
mac string
|
||||
name string
|
||||
in 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
|
||||
`,
|
||||
dnsmasqReadClientInfoReader,
|
||||
"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
|
||||
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
|
||||
`,
|
||||
dnsmasqReadClientInfoReader,
|
||||
"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 {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := routerPlatform.Load()
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
t.Errorf("mac mismatched, got: %q, want: %q", ci.Mac, tc.mac)
|
||||
} else {
|
||||
t.Log(ci)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
nvramCtrldKeyPrefix = "ctrld_"
|
||||
nvramCtrldSetupKey = "ctrld_setup"
|
||||
nvramRCStartupKey = "rc_startup"
|
||||
nvramCtrldKeyPrefix = "ctrld_"
|
||||
nvramCtrldSetupKey = "ctrld_setup"
|
||||
nvramCtrldInstallKey = "ctrld_install"
|
||||
nvramRCStartupKey = "rc_startup"
|
||||
)
|
||||
|
||||
//lint:ignore ST1005 This error is for human.
|
||||
@@ -29,14 +30,14 @@ func setupDDWrt() error {
|
||||
return err
|
||||
}
|
||||
|
||||
nvramKvMap := nvramKV()
|
||||
nvramKvMap := nvramSetupKV()
|
||||
nvramKvMap["dnsmasq_options"] = data
|
||||
if err := nvramSetup(nvramKvMap); err != nil {
|
||||
if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := ddwrtRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -44,11 +45,11 @@ func setupDDWrt() error {
|
||||
|
||||
func cleanupDDWrt() error {
|
||||
// Restore old configs.
|
||||
if err := nvramRestore(nvramKV()); err != nil {
|
||||
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := ddwrtRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -49,7 +49,7 @@ func dnsMasqConf() (string, error) {
|
||||
var sb strings.Builder
|
||||
var tmplText string
|
||||
switch Name() {
|
||||
case DDWrt, OpenWrt, Ubios:
|
||||
case EdgeOS, DDWrt, OpenWrt, Ubios, Synology, Tomato:
|
||||
tmplText = dnsMasqConfigContentTmpl
|
||||
case Merlin:
|
||||
tmplText = merlinDNSMasqPostConfTmpl
|
||||
@@ -65,3 +65,23 @@ func dnsMasqConf() (string, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := merlinRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := nvramSetup(nvramKV()); err != nil {
|
||||
if err := nvramSetKV(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func setupMerlin() error {
|
||||
|
||||
func cleanupMerlin() error {
|
||||
// Restore old configs.
|
||||
if err := nvramRestore(nvramKV()); err != nil {
|
||||
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
|
||||
@@ -60,7 +60,7 @@ func cleanupMerlin() error {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := merlinRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
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/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() {
|
||||
case DDWrt:
|
||||
return map[string]string{
|
||||
@@ -39,11 +39,28 @@ func nvramKV() map[string]string {
|
||||
return map[string]string{
|
||||
"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
|
||||
}
|
||||
|
||||
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.
|
||||
for key, value := range m {
|
||||
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)
|
||||
}
|
||||
// Commit.
|
||||
@@ -68,7 +85,7 @@ func nvramSetup(m map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func nvramRestore(m map[string]string) error {
|
||||
func nvramRestore(m map[string]string, setupKey string) error {
|
||||
// Restore old configs.
|
||||
for key := range m {
|
||||
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)
|
||||
}
|
||||
// Commit.
|
||||
|
||||
@@ -23,12 +23,21 @@ func IsGLiNet() bool {
|
||||
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 {
|
||||
// Delete dnsmasq port if set.
|
||||
if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) {
|
||||
return err
|
||||
}
|
||||
// Disable dnsmasq as DNS server.
|
||||
dnsMasqConfigContent, err := dnsMasqConf()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -41,7 +50,7 @@ func setupOpenWrt() error {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := openwrtRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -53,7 +62,7 @@ func cleanupOpenWrt() error {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := openwrtRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
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 (
|
||||
OpenWrt = "openwrt"
|
||||
DDWrt = "ddwrt"
|
||||
Merlin = "merlin"
|
||||
Ubios = "ubios"
|
||||
OpenWrt = "openwrt"
|
||||
DDWrt = "ddwrt"
|
||||
Merlin = "merlin"
|
||||
Ubios = "ubios"
|
||||
Synology = "synology"
|
||||
Tomato = "tomato"
|
||||
EdgeOS = "edgeos"
|
||||
Pfsense = "pfsense"
|
||||
)
|
||||
|
||||
// ErrNotSupported reports the current router is not supported error.
|
||||
@@ -37,23 +41,36 @@ type router struct {
|
||||
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.
|
||||
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{
|
||||
DDWrt: setupDDWrt,
|
||||
Merlin: setupMerlin,
|
||||
OpenWrt: setupOpenWrt,
|
||||
Ubios: setupUbiOS,
|
||||
EdgeOS: setupEdgeOS,
|
||||
DDWrt: setupDDWrt,
|
||||
Merlin: setupMerlin,
|
||||
OpenWrt: setupOpenWrt,
|
||||
Pfsense: setupPfsense,
|
||||
Synology: setupSynology,
|
||||
Tomato: setupTomato,
|
||||
Ubios: setupUbiOS,
|
||||
}
|
||||
|
||||
// Configure configures things for running ctrld on the router.
|
||||
func Configure(c *ctrld.Config) error {
|
||||
name := Name()
|
||||
switch name {
|
||||
case DDWrt, Merlin, OpenWrt, Ubios:
|
||||
case EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios:
|
||||
if c.HasUpstreamSendClientInfo() {
|
||||
r := routerPlatform.Load()
|
||||
r.sendClientInfo = true
|
||||
@@ -63,8 +80,8 @@ func Configure(c *ctrld.Config) error {
|
||||
}
|
||||
r.watcher = watcher
|
||||
go r.watchClientInfoTable()
|
||||
for _, file := range clientInfoFiles {
|
||||
_ = readClientInfoFile(file)
|
||||
for file, readClienInfoFunc := range clientInfoFiles {
|
||||
_ = readClienInfoFunc(file)
|
||||
_ = r.watcher.Add(file)
|
||||
}
|
||||
}
|
||||
@@ -88,67 +105,53 @@ func ConfigureService(sc *service.Config) error {
|
||||
}
|
||||
case OpenWrt:
|
||||
sc.Option["SysvScript"] = openWrtScript
|
||||
case Merlin, Ubios:
|
||||
case Pfsense:
|
||||
sc.Option["SysvScript"] = pfsenseInitScript
|
||||
case EdgeOS, Merlin, Synology, Tomato, Ubios:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreStart blocks until the router is ready for running ctrld.
|
||||
func PreStart() (err error) {
|
||||
if Name() != DDWrt {
|
||||
// PreRun blocks until the router is ready for running ctrld.
|
||||
func PreRun() (err error) {
|
||||
// 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
|
||||
}
|
||||
|
||||
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.
|
||||
func PostInstall() error {
|
||||
func PostInstall(svc *service.Config) error {
|
||||
name := Name()
|
||||
switch name {
|
||||
case EdgeOS:
|
||||
return postInstallEdgeOS()
|
||||
case DDWrt:
|
||||
return postInstallDDWrt()
|
||||
case Merlin:
|
||||
return postInstallMerlin()
|
||||
case OpenWrt:
|
||||
return postInstallOpenWrt()
|
||||
case Pfsense:
|
||||
return postInstallPfsense(svc)
|
||||
case Synology:
|
||||
return postInstallSynology()
|
||||
case Tomato:
|
||||
return postInstallTomato()
|
||||
case Ubios:
|
||||
return postInstallUbiOS()
|
||||
}
|
||||
@@ -156,15 +159,23 @@ func PostInstall() error {
|
||||
}
|
||||
|
||||
// Cleanup cleans ctrld setup on the router.
|
||||
func Cleanup() error {
|
||||
func Cleanup(svc *service.Config) error {
|
||||
name := Name()
|
||||
switch name {
|
||||
case EdgeOS:
|
||||
return cleanupEdgeOS()
|
||||
case DDWrt:
|
||||
return cleanupDDWrt()
|
||||
case Merlin:
|
||||
return cleanupMerlin()
|
||||
case OpenWrt:
|
||||
return cleanupOpenWrt()
|
||||
case Pfsense:
|
||||
return cleanupPfsense(svc)
|
||||
case Synology:
|
||||
return cleanupSynology()
|
||||
case Tomato:
|
||||
return cleanupTomato()
|
||||
case Ubios:
|
||||
return cleanupUbiOS()
|
||||
}
|
||||
@@ -175,8 +186,10 @@ func Cleanup() error {
|
||||
func ListenAddress() string {
|
||||
name := Name()
|
||||
switch name {
|
||||
case DDWrt, Merlin, OpenWrt, Ubios:
|
||||
case EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios:
|
||||
return "127.0.0.1:5354"
|
||||
case Pfsense:
|
||||
// On pfsense, we run ctrld as DNS resolver.
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -194,14 +207,24 @@ func Name() string {
|
||||
|
||||
func distroName() string {
|
||||
switch {
|
||||
case bytes.HasPrefix(uname(), []byte("DD-WRT")):
|
||||
case bytes.HasPrefix(unameO(), []byte("DD-WRT")):
|
||||
return DDWrt
|
||||
case bytes.HasPrefix(uname(), []byte("ASUSWRT-Merlin")):
|
||||
case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")):
|
||||
return Merlin
|
||||
case haveFile("/etc/openwrt_version"):
|
||||
return OpenWrt
|
||||
case haveDir("/data/unifi"):
|
||||
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 ""
|
||||
}
|
||||
@@ -216,7 +239,17 @@ func haveDir(dir string) bool {
|
||||
return fi != nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func uname() []byte {
|
||||
func unameO() []byte {
|
||||
out, _ := exec.Command("uname", "-o").Output()
|
||||
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,
|
||||
},
|
||||
&linuxSystemService{
|
||||
name: "tomato",
|
||||
detect: func() bool { return Name() == Tomato },
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newTomatoService,
|
||||
},
|
||||
}
|
||||
systems = append(systems, service.AvailableSystems()...)
|
||||
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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"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 (
|
||||
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 {
|
||||
@@ -20,7 +25,7 @@ func setupUbiOS() error {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := ubiosRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -32,13 +37,17 @@ func cleanupUbiOS() error {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := ubiosRestartDNSMasq(); err != nil {
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func postInstallUbiOS() error {
|
||||
// See comment in postInstallEdgeOS.
|
||||
if contentFilteringEnabled() {
|
||||
return errContentFilteringEnabled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -57,3 +66,8 @@ func ubiosRestartDNSMasq() error {
|
||||
}
|
||||
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 (
|
||||
ResolverTypeDOH = "doh"
|
||||
ResolverTypeDOH3 = "doh3"
|
||||
ResolverTypeDOT = "dot"
|
||||
ResolverTypeDOQ = "doq"
|
||||
ResolverTypeOS = "os"
|
||||
// ResolverTypeDOH specifies DoH resolver.
|
||||
ResolverTypeDOH = "doh"
|
||||
// ResolverTypeDOH3 specifies DoH3 resolver.
|
||||
ResolverTypeDOH3 = "doh3"
|
||||
// ResolverTypeDOT specifies DoT resolver.
|
||||
ResolverTypeDOT = "dot"
|
||||
// ResolverTypeDOQ specifies DoQ resolver.
|
||||
ResolverTypeDOQ = "doq"
|
||||
// ResolverTypeOS specifies OS resolver.
|
||||
ResolverTypeOS = "os"
|
||||
// ResolverTypeLegacy specifies legacy resolver.
|
||||
ResolverTypeLegacy = "legacy"
|
||||
)
|
||||
|
||||
@@ -125,7 +131,14 @@ func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, e
|
||||
Net: udpNet,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -194,3 +207,17 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string)
|
||||
}
|
||||
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