mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
internal/router: support Merlin Guest Network Pro VLAN
By looking for any additional dnsmasq configuration files under /tmp/etc, and handling them like default one.
This commit is contained in:
committed by
Cuong Manh Le
parent
b9ece6d7b9
commit
6663925c4d
@@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -607,6 +608,12 @@ func (p *prog) setupClientInfoDiscover(selfIP string) {
|
|||||||
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||||
p.ciTable.AddLeaseFile(leaseFile, format)
|
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||||
}
|
}
|
||||||
|
if leaseFiles := dnsmasq.AdditionalLeaseFiles(); len(leaseFiles) > 0 {
|
||||||
|
mainLog.Load().Debug().Msgf("watching additional lease files: %v", leaseFiles)
|
||||||
|
for _, leaseFile := range leaseFiles {
|
||||||
|
p.ciTable.AddLeaseFile(leaseFile, ctrld.Dnsmasq)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// runClientInfoDiscover runs the client info discover.
|
// runClientInfoDiscover runs the client info discover.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,3 +29,62 @@ func interfaceNameFromReader(r io.Reader) (string, error) {
|
|||||||
}
|
}
|
||||||
return "", errors.New("not found")
|
return "", errors.New("not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdditionalConfigFiles returns a list of Dnsmasq configuration files found in the "/tmp/etc" directory.
|
||||||
|
func AdditionalConfigFiles() []string {
|
||||||
|
if paths, err := filepath.Glob("/tmp/etc/dnsmasq-*.conf"); err == nil {
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdditionalLeaseFiles returns a list of lease file paths corresponding to the Dnsmasq configuration files.
|
||||||
|
func AdditionalLeaseFiles() []string {
|
||||||
|
cfgFiles := AdditionalConfigFiles()
|
||||||
|
if len(cfgFiles) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
leaseFiles := make([]string, 0, len(cfgFiles))
|
||||||
|
for _, cfgFile := range cfgFiles {
|
||||||
|
if leaseFile := leaseFileFromConfigFileName(cfgFile); leaseFile != "" {
|
||||||
|
leaseFiles = append(leaseFiles, leaseFile)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
leaseFiles = append(leaseFiles, defaultLeaseFileFromConfigPath(cfgFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return leaseFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
// leaseFileFromConfigFileName retrieves the DHCP lease file path by reading and parsing the provided configuration file.
|
||||||
|
func leaseFileFromConfigFileName(cfgFile string) string {
|
||||||
|
if f, err := os.Open(cfgFile); err == nil {
|
||||||
|
return leaseFileFromReader(f)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// leaseFileFromReader parses the given io.Reader for the "dhcp-leasefile" configuration and returns its value as a string.
|
||||||
|
func leaseFileFromReader(r io.Reader) string {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
before, after, found := strings.Cut(line, "=")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if before == "dhcp-leasefile" {
|
||||||
|
return after
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultLeaseFileFromConfigPath generates the default lease file path based on the provided configuration file path.
|
||||||
|
func defaultLeaseFileFromConfigPath(path string) string {
|
||||||
|
name := filepath.Base(path)
|
||||||
|
return filepath.Join("/var/lib/misc", strings.TrimSuffix(name, ".conf")+".leases")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dnsmasq
|
package dnsmasq
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -44,3 +45,49 @@ interface=eth0
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_leaseFileFromReader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in io.Reader
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"default",
|
||||||
|
strings.NewReader(`
|
||||||
|
dhcp-script=/sbin/dhcpc_lease
|
||||||
|
dhcp-leasefile=/var/lib/misc/dnsmasq-1.leases
|
||||||
|
script-arp
|
||||||
|
`),
|
||||||
|
"/var/lib/misc/dnsmasq-1.leases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-default",
|
||||||
|
strings.NewReader(`
|
||||||
|
dhcp-script=/sbin/dhcpc_lease
|
||||||
|
dhcp-leasefile=/tmp/var/lib/misc/dnsmasq-1.leases
|
||||||
|
script-arp
|
||||||
|
`),
|
||||||
|
"/tmp/var/lib/misc/dnsmasq-1.leases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"missing",
|
||||||
|
strings.NewReader(`
|
||||||
|
dhcp-script=/sbin/dhcpc_lease
|
||||||
|
script-arp
|
||||||
|
`),
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := leaseFileFromReader(tc.in); got != tc.expected {
|
||||||
|
t.Errorf("leaseFileFromReader() = %v, want %v", got, tc.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ max-cache-ttl=0
|
|||||||
{{- end}}
|
{{- end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
const MerlinConfPath = "/tmp/etc/dnsmasq.conf"
|
const (
|
||||||
const MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
|
MerlinConfPath = "/tmp/etc/dnsmasq.conf"
|
||||||
const MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
MerlinJffsConfDir = "/jffs/configs"
|
||||||
|
MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
|
||||||
|
MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
||||||
|
)
|
||||||
|
|
||||||
const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF`
|
const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF`
|
||||||
const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -20,10 +21,18 @@ import (
|
|||||||
|
|
||||||
const Name = "merlin"
|
const Name = "merlin"
|
||||||
|
|
||||||
|
// nvramKvMap is a map of NVRAM key-value pairs used to configure and manage Merlin-specific settings.
|
||||||
var nvramKvMap = map[string]string{
|
var nvramKvMap = map[string]string{
|
||||||
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
|
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dnsmasqConfig represents configuration paths for dnsmasq operations in Merlin firmware.
|
||||||
|
type dnsmasqConfig struct {
|
||||||
|
confPath string
|
||||||
|
jffsConfPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merlin represents a configuration handler for setting up and managing ctrld on Merlin routers.
|
||||||
type Merlin struct {
|
type Merlin struct {
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
}
|
}
|
||||||
@@ -33,18 +42,22 @@ func New(cfg *ctrld.Config) *Merlin {
|
|||||||
return &Merlin{cfg: cfg}
|
return &Merlin{cfg: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigureService configures the service based on the provided configuration. It returns an error if the configuration fails.
|
||||||
func (m *Merlin) ConfigureService(config *service.Config) error {
|
func (m *Merlin) ConfigureService(config *service.Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Install sets up the necessary configurations and services required for the Merlin instance to function properly.
|
||||||
func (m *Merlin) Install(_ *service.Config) error {
|
func (m *Merlin) Install(_ *service.Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uninstall removes the ctrld-related configurations and services from the Merlin router and reverts to the original state.
|
||||||
func (m *Merlin) Uninstall(_ *service.Config) error {
|
func (m *Merlin) Uninstall(_ *service.Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreRun prepares the Merlin instance for operation by waiting for essential services and directories to become available.
|
||||||
func (m *Merlin) PreRun() error {
|
func (m *Merlin) PreRun() error {
|
||||||
// Wait NTP ready.
|
// Wait NTP ready.
|
||||||
_ = m.Cleanup()
|
_ = m.Cleanup()
|
||||||
@@ -66,6 +79,7 @@ func (m *Merlin) PreRun() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup initializes and configures the Merlin instance for use, including setting up dnsmasq and necessary nvram settings.
|
||||||
func (m *Merlin) Setup() error {
|
func (m *Merlin) Setup() error {
|
||||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
if m.cfg.FirstListener().IsDirectDnsListener() {
|
||||||
return nil
|
return nil
|
||||||
@@ -79,35 +93,10 @@ func (m *Merlin) Setup() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy current dnsmasq config to /jffs/configs/dnsmasq.conf,
|
for _, cfg := range getDnsmasqConfigs() {
|
||||||
// Then we will run postconf script on this file.
|
if err := m.setupDnsmasq(cfg); err != nil {
|
||||||
//
|
return fmt.Errorf("failed to setup dnsmasq: config: %s, error: %w", cfg.confPath, err)
|
||||||
// Normally, adding postconf script is enough. However, we see
|
}
|
||||||
// reports on some Merlin devices that postconf scripts does not
|
|
||||||
// work, but manipulating the config directly via /jffs/configs does.
|
|
||||||
src, err := os.Open(dnsmasq.MerlinConfPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open dnsmasq config: %w", err)
|
|
||||||
}
|
|
||||||
defer src.Close()
|
|
||||||
|
|
||||||
dst, err := os.Create(dnsmasq.MerlinJffsConfPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create %s: %w", dnsmasq.MerlinJffsConfPath, err)
|
|
||||||
}
|
|
||||||
defer dst.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(dst, src); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy current dnsmasq config: %w", err)
|
|
||||||
}
|
|
||||||
if err := dst.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save %s: %w", dnsmasq.MerlinJffsConfPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run postconf script on /jffs/configs/dnsmasq.conf directly.
|
|
||||||
cmd := exec.Command("/bin/sh", dnsmasq.MerlinPostConfPath, dnsmasq.MerlinJffsConfPath)
|
|
||||||
if out, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to run post conf: %s: %w", string(out), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
// Restart dnsmasq service.
|
||||||
@@ -122,6 +111,7 @@ func (m *Merlin) Setup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup restores the original dnsmasq and nvram configurations and restarts dnsmasq if necessary.
|
||||||
func (m *Merlin) Cleanup() error {
|
func (m *Merlin) Cleanup() error {
|
||||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
if m.cfg.FirstListener().IsDirectDnsListener() {
|
||||||
return nil
|
return nil
|
||||||
@@ -143,9 +133,11 @@ func (m *Merlin) Cleanup() error {
|
|||||||
if err := os.WriteFile(dnsmasq.MerlinPostConfPath, merlinParsePostConf(buf), 0750); err != nil {
|
if err := os.WriteFile(dnsmasq.MerlinPostConfPath, merlinParsePostConf(buf), 0750); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Remove /jffs/configs/dnsmasq.conf file.
|
|
||||||
if err := os.Remove(dnsmasq.MerlinJffsConfPath); err != nil && !os.IsNotExist(err) {
|
for _, cfg := range getDnsmasqConfigs() {
|
||||||
return err
|
if err := m.cleanupDnsmasqJffs(cfg); err != nil {
|
||||||
|
return fmt.Errorf("failed to cleanup jffs dnsmasq: config: %s, error: %w", cfg.confPath, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Restart dnsmasq service.
|
// Restart dnsmasq service.
|
||||||
if err := restartDNSMasq(); err != nil {
|
if err := restartDNSMasq(); err != nil {
|
||||||
@@ -154,6 +146,54 @@ func (m *Merlin) Cleanup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupDnsmasq sets up dnsmasq configuration by writing postconf, copying configuration, and running a postconf script.
|
||||||
|
func (m *Merlin) setupDnsmasq(cfg *dnsmasqConfig) error {
|
||||||
|
src, err := os.Open(cfg.confPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil // nothing to do if conf file does not exist.
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open dnsmasq config: %w", err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
// Copy current dnsmasq config to cfg.jffsConfPath,
|
||||||
|
// Then we will run postconf script on this file.
|
||||||
|
//
|
||||||
|
// Normally, adding postconf script is enough. However, we see
|
||||||
|
// reports on some Merlin devices that postconf scripts does not
|
||||||
|
// work, but manipulating the config directly via /jffs/configs does.
|
||||||
|
dst, err := os.Create(cfg.jffsConfPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create %s: %w", cfg.jffsConfPath, err)
|
||||||
|
}
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(dst, src); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy current dnsmasq config: %w", err)
|
||||||
|
}
|
||||||
|
if err := dst.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save %s: %w", cfg.jffsConfPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run postconf script on cfg.jffsConfPath directly.
|
||||||
|
cmd := exec.Command("/bin/sh", dnsmasq.MerlinPostConfPath, cfg.jffsConfPath)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to run post conf: %s: %w", string(out), err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupDnsmasqJffs removes the JFFS configuration file specified in the given dnsmasqConfig, if it exists.
|
||||||
|
func (m *Merlin) cleanupDnsmasqJffs(cfg *dnsmasqConfig) error {
|
||||||
|
// Remove cfg.jffsConfPath file.
|
||||||
|
if err := os.Remove(cfg.jffsConfPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeDnsmasqPostconf writes the requireddnsmasqConfigs post-configuration for dnsmasq to enable custom DNS settings with ctrld.
|
||||||
func (m *Merlin) writeDnsmasqPostconf() error {
|
func (m *Merlin) writeDnsmasqPostconf() error {
|
||||||
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
||||||
// Already setup.
|
// Already setup.
|
||||||
@@ -179,6 +219,8 @@ func (m *Merlin) writeDnsmasqPostconf() error {
|
|||||||
return os.WriteFile(dnsmasq.MerlinPostConfPath, []byte(data), 0750)
|
return os.WriteFile(dnsmasq.MerlinPostConfPath, []byte(data), 0750)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restartDNSMasq restarts the dnsmasq service by executing the appropriate system command using "service".
|
||||||
|
// Returns an error if the command fails or if there is an issue processing the command output.
|
||||||
func restartDNSMasq() error {
|
func restartDNSMasq() error {
|
||||||
if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil {
|
if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err)
|
return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err)
|
||||||
@@ -186,6 +228,22 @@ func restartDNSMasq() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDnsmasqConfigs retrieves a list of dnsmasqConfig containing configuration and JFFS paths for dnsmasq operations.
|
||||||
|
func getDnsmasqConfigs() []*dnsmasqConfig {
|
||||||
|
cfgs := []*dnsmasqConfig{
|
||||||
|
{dnsmasq.MerlinConfPath, dnsmasq.MerlinJffsConfPath},
|
||||||
|
}
|
||||||
|
for _, path := range dnsmasq.AdditionalConfigFiles() {
|
||||||
|
jffsConfPath := filepath.Join(dnsmasq.MerlinJffsConfDir, filepath.Base(path))
|
||||||
|
cfgs = append(cfgs, &dnsmasqConfig{path, jffsConfPath})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// merlinParsePostConf parses the dnsmasq post configuration by removing content after the MerlinPostConfMarker, if present.
|
||||||
|
// If no marker is found, the original buffer is returned unmodified.
|
||||||
|
// Returns nil if the input buffer is empty.
|
||||||
func merlinParsePostConf(buf []byte) []byte {
|
func merlinParsePostConf(buf []byte) []byte {
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -197,6 +255,7 @@ func merlinParsePostConf(buf []byte) []byte {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitDirExists waits until the specified directory exists, polling its existence every second.
|
||||||
func waitDirExists(dir string) {
|
func waitDirExists(dir string) {
|
||||||
for {
|
for {
|
||||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user