package edgeos import ( "bufio" "bytes" "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/kardianos/service" "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" ) const ( Name = "edgeos" edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf" usgDNSMasqConfigPath = "/etc/dnsmasq.conf" usgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak" toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f" toggleDnsShieldLink = "https://community.ui.com/questions/UniFi-OS-3-2-7-DNS-Shield-Missing/d3a85905-4ce0-4fe4-8bf0-6cb04f21371d" ) var ErrContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n To disable it, folowing instruction here: %s`, toggleContentFilteringLink) var ErrDnsShieldEnabled = fmt.Errorf(`the "DNS Shield" feature" is enabled, which is conflicted with ctrld.\n To disable it, folowing screenshot here: %s`, toggleDnsShieldLink) type EdgeOS struct { cfg *ctrld.Config isUSG bool } // New returns a router.Router for configuring/setup/run ctrld on EdgeOS routers. func New(cfg *ctrld.Config) *EdgeOS { e := &EdgeOS{cfg: cfg} e.isUSG = checkUSG() return e } func (e *EdgeOS) ConfigureService(config *service.Config) error { return nil } func (e *EdgeOS) Install(_ *service.Config) 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 } // If "DNS Shield" is enabled, UniFi OS will spawn dnscrypt-proxy process, and route all DNS queries to it. So // reporting an error and guiding users to disable the feature using UniFi OS web UI. if DnsShieldEnabled() { return ErrDnsShieldEnabled } return nil } func (e *EdgeOS) Uninstall(_ *service.Config) error { return nil } func (e *EdgeOS) PreRun() error { return nil } func (e *EdgeOS) Setup() error { if e.cfg.FirstListener().IsDirectDnsListener() { return nil } if e.isUSG { return e.setupUSG() } return e.setupUDM() } func (e *EdgeOS) Cleanup() error { if e.cfg.FirstListener().IsDirectDnsListener() { return nil } if e.isUSG { return e.cleanupUSG() } return e.cleanupUDM() } func (e *EdgeOS) setupUSG() error { // On USG, dnsmasq is configured to forward queries to external provider by default. // So instead of generating config in /etc/dnsmasq.d, we need to create a backup of // the config, then modify it to forward queries to ctrld listener. // Creating a backup. buf, err := os.ReadFile(usgDNSMasqConfigPath) if err != nil { return fmt.Errorf("setupUSG: reading current config: %w", err) } if err := os.WriteFile(usgDNSMasqBackupConfigPath, buf, 0600); err != nil { return fmt.Errorf("setupUSG: backup current config: %w", err) } // Removing all configured upstreams and cache config. var sb strings.Builder scanner := bufio.NewScanner(bytes.NewReader(buf)) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "server=") { continue } if strings.HasPrefix(line, "all-servers") { continue } sb.WriteString(line) } data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false) if err != nil { return err } sb.WriteString("\n") sb.WriteString(data) if err := os.WriteFile(usgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil { return fmt.Errorf("setupUSG: writing dnsmasq config: %w", err) } // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { return fmt.Errorf("setupUSG: restartDNSMasq: %w", err) } return nil } func (e *EdgeOS) setupUDM() error { data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false) if err != nil { return err } if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(data), 0600); err != nil { return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) } // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { return fmt.Errorf("setupUDM: restartDNSMasq: %w", err) } return nil } func (e *EdgeOS) cleanupUSG() error { if err := os.Rename(usgDNSMasqBackupConfigPath, usgDNSMasqConfigPath); err != nil { return fmt.Errorf("cleanupUSG: os.Rename: %w", err) } // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { return fmt.Errorf("cleanupUSG: restartDNSMasq: %w", err) } return nil } func (e *EdgeOS) cleanupUDM() error { // Remove the custom dnsmasq config if err := os.Remove(edgeOSDNSMasqConfigPath); err != nil { return fmt.Errorf("cleanupUDM: os.Remove: %w", err) } // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { return fmt.Errorf("cleanupUDM: restartDNSMasq: %w", err) } return nil } func ContentFilteringEnabled() bool { st, err := os.Stat("/run/dnsfilter/dnsfilter") return err == nil && !st.IsDir() } // DnsShieldEnabled reports whether DNS Shield is enabled. // See: https://community.ui.com/releases/UniFi-OS-Dream-Machines-3-2-7/251dfc1e-f4dd-4264-a080-3be9d8b9e02b func DnsShieldEnabled() bool { buf, err := os.ReadFile(filepath.Join(dnsmasq.UbiosConfPath(), "dns.conf")) if err != nil { return false } return bytes.Contains(buf, []byte("server=127.0.0.1#5053")) } func LeaseFileDir() string { if checkUSG() { return "" } return "/run" } func checkUSG() bool { out, _ := os.ReadFile("/etc/version") return bytes.HasPrefix(out, []byte("UniFiSecurityGateway.")) } func restartDNSMasq() 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 }