From 54e63ccf9b33cc1610a560a0ba808c44882368c3 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 25 May 2023 22:05:39 +0700 Subject: [PATCH] all: add support for EdgeOS --- cmd/ctrld/prog.go | 5 +- cmd/ctrld/prog_linux.go | 9 ++++ internal/router/client_info.go | 84 ++++++++++++++++++++++++----- internal/router/client_info_test.go | 43 ++++++++++++--- internal/router/dnsmasq.go | 4 +- internal/router/edgeos.go | 48 +++++++++++++++++ internal/router/router.go | 34 +++++++----- 7 files changed, 194 insertions(+), 33 deletions(-) create mode 100644 internal/router/edgeos.go diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 1c59530..807d22c 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -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/Tomato, 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 { diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 5cc5d6f..0b49a33 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -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) { diff --git a/internal/router/client_info.go b/internal/router/client_info.go index 04af7ac..db6294f 100644 --- a/internal/router/client_info.go +++ b/internal/router/client_info.go @@ -1,6 +1,7 @@ package router import ( + "bufio" "bytes" "io" "log" @@ -15,14 +16,18 @@ 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 - "/etc/dhcpd/dhcpd-leases.log", // Synology - "/tmp/var/lib/misc/dnsmasq.leases", // Tomato +type readClientInfoFunc func(name string) error + +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 } func (r *router) watchClientInfoTable() { @@ -34,14 +39,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) } } @@ -80,17 +90,17 @@ func GetClientInfoByMac(mac string) *ctrld.ClientInfo { return val.(*ctrld.ClientInfo) } -func readClientInfoFile(name string) error { +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 { +func dnsmasqReadClientInfoReader(reader io.Reader) error { r := routerPlatform.Load() return lineread.Reader(reader, func(line []byte) error { fields := bytes.Fields(line) @@ -113,6 +123,54 @@ func readClientInfoReader(reader io.Reader) error { }) } +func iscDHCPReadClientInfoFile(name string) error { + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + return iscDHCPReadClientInfoReader(f) +} + +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 +} + func normalizeIP(in string) string { // dnsmasq may put ip with interface index in lease file, strip it here. ip, _, found := strings.Cut(in, "%") diff --git a/internal/router/client_info_test.go b/internal/router/client_info_test.go index 2b228c2..5ea8b65 100644 --- a/internal/router/client_info_test.go +++ b/internal/router/client_info_test.go @@ -1,6 +1,7 @@ package router import ( + "io" "strings" "testing" @@ -31,31 +32,59 @@ 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", + }, } 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 +93,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) } }) } diff --git a/internal/router/dnsmasq.go b/internal/router/dnsmasq.go index 17c4879..4d43d20 100644 --- a/internal/router/dnsmasq.go +++ b/internal/router/dnsmasq.go @@ -49,7 +49,7 @@ func dnsMasqConf() (string, error) { var sb strings.Builder var tmplText string switch Name() { - case DDWrt, OpenWrt, Ubios, Synology, Tomato: + case EdgeOS, DDWrt, OpenWrt, Ubios, Synology, Tomato: tmplText = dnsMasqConfigContentTmpl case Merlin: tmplText = merlinDNSMasqPostConfTmpl @@ -68,6 +68,8 @@ func dnsMasqConf() (string, error) { func restartDNSMasq() error { switch Name() { + case EdgeOS: + return edgeOSRestartDNSMasq() case DDWrt: return ddwrtRestartDNSMasq() case Merlin: diff --git a/internal/router/edgeos.go b/internal/router/edgeos.go new file mode 100644 index 0000000..4a8e57e --- /dev/null +++ b/internal/router/edgeos.go @@ -0,0 +1,48 @@ +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 { + 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 +} diff --git a/internal/router/router.go b/internal/router/router.go index aea6202..b5269f1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -21,6 +21,7 @@ const ( Ubios = "ubios" Synology = "synology" Tomato = "tomato" + EdgeOS = "edgeos" ) // ErrNotSupported reports the current router is not supported error. @@ -38,7 +39,7 @@ type router struct { // IsSupported reports whether the given platform is supported by ctrld. func IsSupported(platform string) bool { switch platform { - case DDWrt, Merlin, OpenWrt, Ubios, Synology, Tomato: + case EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios: return true } return false @@ -46,23 +47,24 @@ func IsSupported(platform string) bool { // SupportedPlatforms return all platforms that can be configured to run with ctrld. func SupportedPlatforms() []string { - return []string{DDWrt, Merlin, OpenWrt, Ubios, Synology, Tomato} + return []string{EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios} } var configureFunc = map[string]func() error{ + EdgeOS: setupEdgeOS, DDWrt: setupDDWrt, Merlin: setupMerlin, OpenWrt: setupOpenWrt, - Ubios: setupUbiOS, 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, Synology, Tomato: + case EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios: if c.HasUpstreamSendClientInfo() { r := routerPlatform.Load() r.sendClientInfo = true @@ -72,8 +74,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) } } @@ -97,7 +99,7 @@ func ConfigureService(sc *service.Config) error { } case OpenWrt: sc.Option["SysvScript"] = openWrtScript - case Merlin, Ubios, Synology, Tomato: + case EdgeOS, Merlin, Synology, Tomato, Ubios: } return nil } @@ -119,18 +121,20 @@ func PreStart() (err error) { func PostInstall() error { name := Name() switch name { + case EdgeOS: + return postInstallEdgeOS() case DDWrt: return postInstallDDWrt() case Merlin: return postInstallMerlin() case OpenWrt: return postInstallOpenWrt() - case Ubios: - return postInstallUbiOS() case Synology: return postInstallSynology() case Tomato: return postInstallTomato() + case Ubios: + return postInstallUbiOS() } return nil } @@ -139,18 +143,20 @@ func PostInstall() error { func Cleanup() error { name := Name() switch name { + case EdgeOS: + return cleanupEdgeOS() case DDWrt: return cleanupDDWrt() case Merlin: return cleanupMerlin() case OpenWrt: return cleanupOpenWrt() - case Ubios: - return cleanupUbiOS() case Synology: return cleanupSynology() case Tomato: return cleanupTomato() + case Ubios: + return cleanupUbiOS() } return nil } @@ -159,7 +165,7 @@ func Cleanup() error { func ListenAddress() string { name := Name() switch name { - case DDWrt, Merlin, OpenWrt, Ubios, Synology, Tomato: + case EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios: return "127.0.0.1:5354" } return "" @@ -190,6 +196,10 @@ func distroName() string { 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 } return "" }