diff --git a/internal/router/openwrt/openwrt.go b/internal/router/openwrt/openwrt.go index ad98db9..73f5a06 100644 --- a/internal/router/openwrt/openwrt.go +++ b/internal/router/openwrt/openwrt.go @@ -2,10 +2,13 @@ package openwrt import ( "bytes" + "encoding/json" "errors" "fmt" + "io" "os" "os/exec" + "path/filepath" "strings" "github.com/kardianos/service" @@ -15,10 +18,13 @@ import ( ) const ( - Name = "openwrt" - openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf" + Name = "openwrt" + openwrtDNSMasqConfigName = "ctrld.conf" + openwrtDNSMasqDefaultConfigDir = "/tmp/dnsmasq.d" ) +var openwrtDnsmasqDefaultConfigPath = filepath.Join(openwrtDNSMasqDefaultConfigDir, openwrtDNSMasqConfigName) + type Openwrt struct { cfg *ctrld.Config dnsmasqCacheSize string @@ -67,7 +73,7 @@ func (o *Openwrt) Setup() error { if err != nil { return err } - if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(data), 0600); err != nil { + if err := os.WriteFile(dnsmasqConfPathFromUbus(), []byte(data), 0600); err != nil { return err } // Restart dnsmasq service. @@ -82,7 +88,7 @@ func (o *Openwrt) Cleanup() error { return nil } // Remove the custom dnsmasq config - if err := os.Remove(openwrtDNSMasqConfigPath); err != nil { + if err := os.Remove(dnsmasqConfPathFromUbus()); err != nil { return err } @@ -126,3 +132,60 @@ func uci(args ...string) (string, error) { } return strings.TrimSpace(stdout.String()), nil } + +// openwrtServiceList represents openwrt services config. +type openwrtServiceList struct { + Dnsmasq dnsmasqConf `json:"dnsmasq"` +} + +// dnsmasqConf represents dnsmasq config. +type dnsmasqConf struct { + Instances map[string]confInstances `json:"instances"` +} + +// confInstances represents an instance config of a service. +type confInstances struct { + Mount map[string]string `json:"mount"` +} + +// dnsmasqConfPath returns the dnsmasq config path. +// +// Since version 24.10, openwrt makes some changes to dnsmasq to support +// multiple instances of dnsmasq. This change causes breaking changes to +// software which depends on the default dnsmasq path. +// +// There are some discussion/PRs in openwrt repo to address this: +// +// - https://github.com/openwrt/openwrt/pull/16806 +// - https://github.com/openwrt/openwrt/pull/16890 +// +// In the meantime, workaround this problem by querying the actual config path +// by querying ubus service list. +func dnsmasqConfPath(r io.Reader) string { + var svc openwrtServiceList + if err := json.NewDecoder(r).Decode(&svc); err != nil { + return openwrtDnsmasqDefaultConfigPath + } + for _, inst := range svc.Dnsmasq.Instances { + for mount := range inst.Mount { + dirName := filepath.Base(mount) + parts := strings.Split(dirName, ".") + if len(parts) < 2 { + continue + } + if parts[0] == "dnsmasq" && parts[len(parts)-1] == "d" { + return filepath.Join(mount, openwrtDNSMasqConfigName) + } + } + } + return openwrtDnsmasqDefaultConfigPath +} + +// dnsmasqConfPathFromUbus get dnsmasq config path from ubus service list. +func dnsmasqConfPathFromUbus() string { + output, err := exec.Command("ubus", "call", "service", "list").Output() + if err != nil { + return openwrtDnsmasqDefaultConfigPath + } + return dnsmasqConfPath(bytes.NewReader(output)) +} diff --git a/internal/router/openwrt/openwrt_test.go b/internal/router/openwrt/openwrt_test.go new file mode 100644 index 0000000..8b260e8 --- /dev/null +++ b/internal/router/openwrt/openwrt_test.go @@ -0,0 +1,58 @@ +package openwrt + +import ( + "io" + "path/filepath" + "strings" + "testing" +) + +// Sample output from https://github.com/openwrt/openwrt/pull/16806#issuecomment-2448255734 +const ubusDnsmasqBefore2410 = `{ + "dnsmasq": { + "instances": { + "guest_dns": { + "mount": { + "/tmp/dnsmasq.d": "0", + "/var/run/dnsmasq/": "1" + } + } + } + } +}` + +const ubusDnsmasq2410 = `{ + "dnsmasq": { + "instances": { + "guest_dns": { + "mount": { + "/tmp/dnsmasq.guest_dns.d": "0", + "/var/run/dnsmasq/": "1" + } + } + } + } +}` + +func Test_dnsmasqConfPath(t *testing.T) { + var dnsmasq2410expected = filepath.Join("/tmp/dnsmasq.guest_dns.d", openwrtDNSMasqConfigName) + tests := []struct { + name string + in io.Reader + expected string + }{ + {"empty", strings.NewReader(""), openwrtDnsmasqDefaultConfigPath}, + {"invalid", strings.NewReader("}}"), openwrtDnsmasqDefaultConfigPath}, + {"before 24.10", strings.NewReader(ubusDnsmasqBefore2410), openwrtDnsmasqDefaultConfigPath}, + {"24.10", strings.NewReader(ubusDnsmasq2410), dnsmasq2410expected}, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := dnsmasqConfPath(tc.in); got != tc.expected { + t.Errorf("dnsmasqConfPath() = %v, want %v", got, tc.expected) + } + }) + } +}