mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
This change improves compatibility with newer UniFi OS versions while maintaining backward compatibility with UniFi OS 4.2 and earlier. The refactoring also reduces code duplication and improves maintainability by centralizing dnsmasq configuration path logic.
210 lines
6.0 KiB
Go
210 lines
6.0 KiB
Go
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
|
|
}
|