all: add support for EdgeOS

This commit is contained in:
Cuong Manh Le
2023-05-25 22:05:39 +07:00
committed by Cuong Manh Le
parent ee53db1e35
commit 54e63ccf9b
7 changed files with 194 additions and 33 deletions
+4 -1
View File
@@ -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 {
+9
View File
@@ -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) {
+71 -13
View File
@@ -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, "%")
+37 -6
View File
@@ -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)
}
})
}
+3 -1
View File
@@ -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:
+48
View File
@@ -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
}
+22 -12
View File
@@ -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 ""
}