mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-27 12:52:27 +02:00
Removing router platforms support
This commit is contained in:
committed by
Cuong Manh Le
parent
af1a6e9f3a
commit
ba9057e466
@@ -82,8 +82,6 @@ type Table struct {
|
||||
logger *ctrld.Logger
|
||||
|
||||
dhcp *dhcp
|
||||
merlin *merlinDiscover
|
||||
ubios *ubiosDiscover
|
||||
arp *arpDiscover
|
||||
ndp *ndpDiscover
|
||||
ptr *ptrDiscover
|
||||
@@ -206,30 +204,6 @@ func (t *Table) init() {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, process all possible sources in order, that means
|
||||
// the first result of IP/MAC/Hostname lookup will be used.
|
||||
//
|
||||
// Routers custom clients:
|
||||
// - Merlin
|
||||
// - Ubios
|
||||
if t.discoverDHCP() || t.discoverARP() {
|
||||
t.merlin = &merlinDiscover{logger: t.logger}
|
||||
t.ubios = &ubiosDiscover{}
|
||||
discovers := map[string]interface {
|
||||
refresher
|
||||
HostnameResolver
|
||||
}{
|
||||
"Merlin": t.merlin,
|
||||
"Ubios": t.ubios,
|
||||
}
|
||||
for platform, discover := range discovers {
|
||||
if err := discover.refresh(); err != nil {
|
||||
t.logger.Warn().Err(err).Msgf("failed to init %s discover", platform)
|
||||
}
|
||||
t.hostnameResolvers = append(t.hostnameResolvers, discover)
|
||||
t.refreshers = append(t.refreshers, discover)
|
||||
}
|
||||
}
|
||||
// Hosts file mapping.
|
||||
if t.discoverHosts() {
|
||||
t.hf = &hostsFile{logger: t.logger}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"tailscale.com/util/lineread"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
)
|
||||
|
||||
type dhcp struct {
|
||||
@@ -39,10 +38,6 @@ func (d *dhcp) init() error {
|
||||
}
|
||||
d.addSelf()
|
||||
d.watcher = watcher
|
||||
for file, format := range clientInfoFiles {
|
||||
// Ignore errors for default lease files.
|
||||
_ = d.addLeaseFile(file, format)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -50,11 +45,7 @@ func (d *dhcp) watchChanges() {
|
||||
if d.watcher == nil {
|
||||
return
|
||||
}
|
||||
if dir := router.LeaseFilesDir(); dir != "" {
|
||||
if err := d.watcher.Add(dir); err != nil {
|
||||
d.logger.Err(err).Str("dir", dir).Msg("could not watch lease dir")
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-d.watcher.Events:
|
||||
@@ -390,22 +381,4 @@ func (d *dhcp) addSelf() {
|
||||
}
|
||||
}
|
||||
})
|
||||
for _, netIface := range router.SelfInterfaces() {
|
||||
mac := netIface.HardwareAddr.String()
|
||||
if mac == "" {
|
||||
return
|
||||
}
|
||||
d.mac2name.Store(mac, hostname)
|
||||
addrs, _ := netIface.Addrs()
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ip := ipNet.IP
|
||||
d.mac.LoadOrStore(ip.String(), mac)
|
||||
d.ip.LoadOrStore(mac, ip.String())
|
||||
d.ip2name.Store(ip.String(), hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,5 @@ package clientinfo
|
||||
import "github.com/Control-D-Inc/ctrld"
|
||||
|
||||
// clientInfoFiles specifies client info files and how to read them on supported platforms.
|
||||
var clientInfoFiles = map[string]ctrld.LeaseFileFormat{
|
||||
"/tmp/dnsmasq.leases": ctrld.Dnsmasq, // ddwrt
|
||||
"/tmp/dhcp.leases": ctrld.Dnsmasq, // openwrt
|
||||
"/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // merlin
|
||||
"/mnt/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDM Pro
|
||||
"/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDR
|
||||
"/etc/dhcpd/dhcpd-leases.log": ctrld.Dnsmasq, // Synology
|
||||
"/tmp/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // Tomato
|
||||
"/run/dnsmasq-dhcp.leases": ctrld.Dnsmasq, // EdgeOS
|
||||
"/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS
|
||||
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
|
||||
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
|
||||
"/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense
|
||||
}
|
||||
// TODO: cleanup this after server support removal.
|
||||
var clientInfoFiles = map[string]ctrld.LeaseFileFormat{}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package clientinfo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/merlin"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
const merlinNvramCustomClientListKey = "custom_clientlist"
|
||||
|
||||
type merlinDiscover struct {
|
||||
hostname sync.Map // mac => hostname
|
||||
logger *ctrld.Logger
|
||||
}
|
||||
|
||||
func (m *merlinDiscover) refresh() error {
|
||||
if router.Name() != merlin.Name {
|
||||
return nil
|
||||
}
|
||||
out, err := nvram.Run("get", merlinNvramCustomClientListKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.logger.Debug().Msg("reading Merlin custom client list")
|
||||
m.parseMerlinCustomClientList(out)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *merlinDiscover) LookupHostnameByIP(ip string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *merlinDiscover) LookupHostnameByMac(mac string) string {
|
||||
val, ok := m.hostname.Load(mac)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
// "nvram get custom_clientlist" output:
|
||||
//
|
||||
// <client 1>00:00:00:00:00:01>0>4>><client 2>00:00:00:00:00:02>0>24>>...
|
||||
//
|
||||
// So to parse it, do the following steps:
|
||||
//
|
||||
// - Split by "<" => entries
|
||||
// - For each entry, split by ">" => parts
|
||||
// - Empty parts => skip
|
||||
// - Empty parts[0] => skip empty hostname
|
||||
// - Empty parts[1] => skip empty MAC
|
||||
func (m *merlinDiscover) parseMerlinCustomClientList(data string) {
|
||||
entries := strings.Split(data, "<")
|
||||
for _, entry := range entries {
|
||||
parts := strings.SplitN(string(entry), ">", 3)
|
||||
if len(parts) < 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
|
||||
continue
|
||||
}
|
||||
hostname := normalizeHostname(parts[0])
|
||||
mac := strings.ToLower(parts[1])
|
||||
m.hostname.Store(mac, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *merlinDiscover) String() string {
|
||||
return "merlin"
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package clientinfo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseMerlinCustomClientList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
clientList string
|
||||
macList []string
|
||||
hostnameList []string
|
||||
macNotPresentList []string
|
||||
}{
|
||||
{
|
||||
"normal",
|
||||
"<client1>00:00:00:00:00:01>0>4>>",
|
||||
[]string{"00:00:00:00:00:01"},
|
||||
[]string{"client1"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"multiple clients",
|
||||
"<client1>00:00:00:00:00:01>0>4>><client2>00:00:00:00:00:02>0>24>>",
|
||||
[]string{"00:00:00:00:00:01", "00:00:00:00:00:02"},
|
||||
[]string{"client1", "client2"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"empty hostname",
|
||||
"<client1>00:00:00:00:00:01>0>4>><>00:00:00:00:00:02>0>24>>",
|
||||
[]string{"00:00:00:00:00:01"},
|
||||
[]string{"client1"},
|
||||
[]string{"00:00:00:00:00:02"},
|
||||
},
|
||||
{
|
||||
"empty dhcp",
|
||||
"<client1>00:00:00:00:00:01>0>4>><client 1>>>",
|
||||
[]string{"00:00:00:00:00:01"},
|
||||
[]string{"client1"},
|
||||
[]string{""},
|
||||
},
|
||||
{
|
||||
"invalid",
|
||||
"qwerty",
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"empty",
|
||||
"",
|
||||
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := &merlinDiscover{}
|
||||
m.parseMerlinCustomClientList(tc.clientList)
|
||||
for i, mac := range tc.macList {
|
||||
val, ok := m.hostname.Load(mac)
|
||||
if !ok {
|
||||
t.Errorf("missing hostname: %s", mac)
|
||||
}
|
||||
hostname := val.(string)
|
||||
if hostname != tc.hostnameList[i] {
|
||||
t.Errorf("hostname mismatch, want: %q, got: %q", tc.hostnameList[i], hostname)
|
||||
}
|
||||
}
|
||||
for _, mac := range tc.macNotPresentList {
|
||||
if _, ok := m.hostname.Load(mac); ok {
|
||||
t.Errorf("mac2name address %q should not be present", mac)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package clientinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ubios"
|
||||
)
|
||||
|
||||
// ubiosDiscover provides client discovery functionality on Ubios routers.
|
||||
type ubiosDiscover struct {
|
||||
hostname sync.Map // mac => hostname
|
||||
}
|
||||
|
||||
// refresh reloads unifi devices from database.
|
||||
func (u *ubiosDiscover) refresh() error {
|
||||
if router.Name() != ubios.Name {
|
||||
return nil
|
||||
}
|
||||
return u.refreshDevices()
|
||||
}
|
||||
|
||||
// LookupHostnameByIP returns hostname for given IP.
|
||||
func (u *ubiosDiscover) LookupHostnameByIP(ip string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// LookupHostnameByMac returns unifi device custom hostname for the given MAC address.
|
||||
func (u *ubiosDiscover) LookupHostnameByMac(mac string) string {
|
||||
val, ok := u.hostname.Load(mac)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
// refreshDevices updates unifi devices name from local mongodb.
|
||||
func (u *ubiosDiscover) refreshDevices() error {
|
||||
cmd := exec.Command("/usr/bin/mongo", "localhost:27117/ace", "--quiet", "--eval", `
|
||||
DBQuery.shellBatchSize = 256;
|
||||
db.user.find({name: {$exists: true, $ne: ""}}, {_id:0, mac:1, name:1});`)
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("out: %s, err: %w", string(b), err)
|
||||
}
|
||||
return u.storeDevices(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// storeDevices saves unifi devices name for caching.
|
||||
func (u *ubiosDiscover) storeDevices(r io.Reader) error {
|
||||
decoder := json.NewDecoder(r)
|
||||
device := struct {
|
||||
MAC string
|
||||
Name string
|
||||
}{}
|
||||
for {
|
||||
err := decoder.Decode(&device)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mac := strings.ToLower(device.MAC)
|
||||
u.hostname.Store(mac, normalizeHostname(device.Name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns human-readable format of ubiosDiscover.
|
||||
func (u *ubiosDiscover) String() string {
|
||||
return "ubios"
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package clientinfo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ubiosDiscover_storeDevices(t *testing.T) {
|
||||
ud := &ubiosDiscover{}
|
||||
r := strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1" }
|
||||
{ "mac": "00:00:00:00:00:02", "name": "device 2" }
|
||||
`)
|
||||
if err := ud.storeDevices(r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mac string
|
||||
hostname string
|
||||
}{
|
||||
{"device 1", "00:00:00:00:00:01", "device 1"},
|
||||
{"device 2", "00:00:00:00:00:02", "device 2"},
|
||||
{"non-existed", "00:00:00:00:00:03", ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := ud.LookupHostnameByMac(tc.mac); got != tc.hostname {
|
||||
t.Errorf("hostname mismatched, want: %q, got: %q", tc.hostname, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test for invalid input.
|
||||
r = strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1"`)
|
||||
if err := ud.storeDevices(r); err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/certs"
|
||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -271,7 +269,7 @@ func apiTransport(loggerCtx context.Context, cdDev bool) *http.Transport {
|
||||
// Fallback to direct IPv6
|
||||
return dial(ctx, "tcp6", addrsFromPort(apiIpsV6, port))
|
||||
}
|
||||
if router.Name() == ddwrt.Name || runtime.GOOS == "android" {
|
||||
if runtime.GOOS == "android" {
|
||||
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
||||
}
|
||||
return transport
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package ddwrt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
const Name = "ddwrt"
|
||||
|
||||
//lint:ignore ST1005 This error is for human.
|
||||
var errDdwrtJffs2NotEnabled = errors.New(`could not install service without jffs, follow this guide to enable:
|
||||
|
||||
https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System
|
||||
`)
|
||||
|
||||
var nvramKvMap = map[string]string{
|
||||
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
||||
"dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt.
|
||||
"dns_crypt": "0", // Disable DNSCrypt.
|
||||
"dnssec": "0", // Disable DNSSEC.
|
||||
}
|
||||
|
||||
type Ddwrt struct {
|
||||
cfg *ctrld.Config
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers.
|
||||
func New(cfg *ctrld.Config) *Ddwrt {
|
||||
return &Ddwrt{cfg: cfg}
|
||||
}
|
||||
|
||||
func (d *Ddwrt) ConfigureService(config *service.Config) error {
|
||||
if !ddwrtJff2Enabled() {
|
||||
return errDdwrtJffs2NotEnabled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Ddwrt) Install(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Ddwrt) Uninstall(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Ddwrt) PreRun() error {
|
||||
_ = d.Cleanup()
|
||||
return ntp.WaitNvram()
|
||||
}
|
||||
|
||||
func (d *Ddwrt) Setup() error {
|
||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Already setup.
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, d.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nvramKvMap["dnsmasq_options"] = data
|
||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Ddwrt) Cleanup() error {
|
||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
||||
return nil // was restored, nothing to do.
|
||||
}
|
||||
|
||||
nvramKvMap["dnsmasq_options"] = ""
|
||||
// Restore old configs.
|
||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
if out, err := exec.Command("restart_dns").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("restart_dns: %s, %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ddwrtJff2Enabled() bool {
|
||||
out, _ := nvram.Run("get", "enable_jffs2")
|
||||
return out == "1"
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InterfaceNameFromConfig(filename string) (string, error) {
|
||||
buf, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return interfaceNameFromReader(bytes.NewReader(buf))
|
||||
}
|
||||
|
||||
func interfaceNameFromReader(r io.Reader) (string, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
after, found := strings.CutPrefix(line, "interface=")
|
||||
if found {
|
||||
return after, nil
|
||||
}
|
||||
}
|
||||
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,93 +0,0 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_interfaceNameFromReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
wantIface string
|
||||
}{
|
||||
{
|
||||
"good",
|
||||
`interface=lo`,
|
||||
"lo",
|
||||
},
|
||||
{
|
||||
"multiple",
|
||||
`interface=lo
|
||||
interface=eth0
|
||||
`,
|
||||
"lo",
|
||||
},
|
||||
{
|
||||
"no iface",
|
||||
`cache-size=100`,
|
||||
"",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ifaceName, err := interfaceNameFromReader(strings.NewReader(tc.in))
|
||||
if tc.wantIface != "" && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if tc.wantIface != ifaceName {
|
||||
t.Errorf("mismatched, want: %q, got: %q", tc.wantIface, ifaceName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
const CtrldMarker = `# GENERATED BY ctrld - DO NOT MODIFY`
|
||||
|
||||
const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
no-resolv
|
||||
{{- range .Upstreams}}
|
||||
server={{ .IP }}#{{ .Port }}
|
||||
{{- end}}
|
||||
add-mac
|
||||
add-subnet=32,128
|
||||
{{- if .CacheDisabled}}
|
||||
cache-size=0
|
||||
{{- else}}
|
||||
max-cache-ttl=0
|
||||
{{- end}}
|
||||
`
|
||||
|
||||
const (
|
||||
MerlinConfPath = "/tmp/etc/dnsmasq.conf"
|
||||
MerlinJffsConfDir = "/jffs/configs"
|
||||
MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
|
||||
MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
||||
)
|
||||
|
||||
const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF`
|
||||
const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
|
||||
#!/bin/sh
|
||||
|
||||
config_file="$1"
|
||||
. /usr/sbin/helper.sh
|
||||
|
||||
pid=$(cat /tmp/ctrld.pid 2>/dev/null)
|
||||
if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
||||
pc_delete "servers-file" "$config_file" # no WAN DNS settings
|
||||
pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf
|
||||
# use ctrld as upstream
|
||||
pc_delete "server=" "$config_file"
|
||||
{{- range .Upstreams}}
|
||||
pc_append "server={{ .IP }}#{{ .Port }}" "$config_file"
|
||||
{{- end}}
|
||||
pc_delete "add-mac" "$config_file"
|
||||
pc_delete "add-subnet" "$config_file"
|
||||
pc_append "add-mac" "$config_file" # add client mac
|
||||
pc_append "add-subnet=32,128" "$config_file" # add client ip
|
||||
pc_delete "dnssec" "$config_file" # disable DNSSEC
|
||||
pc_delete "trust-anchor=" "$config_file" # disable DNSSEC
|
||||
pc_delete "cache-size=" "$config_file"
|
||||
pc_append "cache-size=0" "$config_file" # disable cache
|
||||
|
||||
# For John fork
|
||||
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
||||
|
||||
# Change /etc/resolv.conf, which may be changed by WAN DNS setup
|
||||
pc_delete "nameserver" /etc/resolv.conf
|
||||
pc_append "nameserver 127.0.0.1" /etc/resolv.conf
|
||||
|
||||
exit 0
|
||||
fi
|
||||
`
|
||||
|
||||
type Upstream struct {
|
||||
IP string
|
||||
Port int
|
||||
}
|
||||
|
||||
// ConfTmpl generates dnsmasq configuration from ctrld config.
|
||||
func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
||||
return ConfTmplWithCacheDisabled(tmplText, cfg, true)
|
||||
}
|
||||
|
||||
// ConfTmplWithCacheDisabled is like ConfTmpl, but the caller can control whether
|
||||
// dnsmasq cache is disabled using cacheDisabled parameter.
|
||||
//
|
||||
// Generally, the caller should use ConfTmpl, but on some routers which dnsmasq config may be changed
|
||||
// after ctrld started (like EdgeOS/Ubios, Firewalla ...), dnsmasq cache should not be disabled because
|
||||
// the cache-size=0 generated by ctrld will conflict with router's generated config.
|
||||
func ConfTmplWithCacheDisabled(tmplText string, cfg *ctrld.Config, cacheDisabled bool) (string, error) {
|
||||
listener := cfg.FirstListener()
|
||||
if listener == nil {
|
||||
return "", errors.New("missing listener")
|
||||
}
|
||||
ip := listener.IP
|
||||
if ip == "0.0.0.0" || ip == "::" || ip == "" {
|
||||
ip = "127.0.0.1"
|
||||
}
|
||||
upstreams := []Upstream{{IP: ip, Port: listener.Port}}
|
||||
return confTmpl(tmplText, upstreams, cacheDisabled)
|
||||
}
|
||||
|
||||
// FirewallaConfTmpl generates dnsmasq config for Firewalla routers.
|
||||
func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
||||
// If ctrld listen on all interfaces, generating config for all of them.
|
||||
if lc := cfg.FirstListener(); lc != nil && (lc.IP == "0.0.0.0" || lc.IP == "") {
|
||||
return confTmpl(tmplText, firewallaUpstreams(lc.Port), false)
|
||||
}
|
||||
// Otherwise, generating config for the specific listener from ctrld's config.
|
||||
return ConfTmplWithCacheDisabled(tmplText, cfg, false)
|
||||
}
|
||||
|
||||
func confTmpl(tmplText string, upstreams []Upstream, cacheDisabled bool) (string, error) {
|
||||
tmpl := template.Must(template.New("").Parse(tmplText))
|
||||
var to = &struct {
|
||||
Upstreams []Upstream
|
||||
CacheDisabled bool
|
||||
}{
|
||||
Upstreams: upstreams,
|
||||
CacheDisabled: cacheDisabled,
|
||||
}
|
||||
var sb strings.Builder
|
||||
if err := tmpl.Execute(&sb, to); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func firewallaUpstreams(port int) []Upstream {
|
||||
ifaces := FirewallaSelfInterfaces()
|
||||
upstreams := make([]Upstream, 0, len(ifaces))
|
||||
for _, netIface := range ifaces {
|
||||
addrs, _ := netIface.Addrs()
|
||||
for _, addr := range addrs {
|
||||
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil {
|
||||
upstreams = append(upstreams, Upstream{
|
||||
IP: netIP.IP.To4().String(),
|
||||
Port: port,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return upstreams
|
||||
}
|
||||
|
||||
// firewallaDnsmasqConfFiles returns dnsmasq config files of all firewalla interfaces.
|
||||
func firewallaDnsmasqConfFiles() ([]string, error) {
|
||||
return filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf")
|
||||
}
|
||||
|
||||
// FirewallaSelfInterfaces returns list of interfaces that will be configured with default dnsmasq setup on Firewalla.
|
||||
func FirewallaSelfInterfaces() []*net.Interface {
|
||||
matches, err := firewallaDnsmasqConfFiles()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ifaces := make([]*net.Interface, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
// Trim prefix and suffix to get the iface name only.
|
||||
ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf")
|
||||
if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil {
|
||||
ifaces = append(ifaces, netIface)
|
||||
}
|
||||
}
|
||||
return ifaces
|
||||
}
|
||||
|
||||
const (
|
||||
ubios43ConfPath = "/run/dnsmasq.dhcp.conf.d"
|
||||
ubios42ConfPath = "/run/dnsmasq.conf.d"
|
||||
ubios43PidFile = "/run/dnsmasq-main.pid"
|
||||
ubios42PidFile = "/run/dnsmasq.pid"
|
||||
UbiosConfName = "zzzctrld.conf"
|
||||
)
|
||||
|
||||
// UbiosConfPath returns the appropriate configuration path based on the system's directory structure.
|
||||
func UbiosConfPath() string {
|
||||
if st, _ := os.Stat(ubios43ConfPath); st != nil && st.IsDir() {
|
||||
return ubios43ConfPath
|
||||
}
|
||||
return ubios42ConfPath
|
||||
}
|
||||
|
||||
// UbiosPidFile returns the appropriate dnsmasq pid file based on the system's directory structure.
|
||||
func UbiosPidFile() string {
|
||||
if st, _ := os.Stat(ubios43PidFile); st != nil && !st.IsDir() {
|
||||
return ubios43PidFile
|
||||
}
|
||||
return ubios42PidFile
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package firewalla
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "firewalla"
|
||||
|
||||
firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local/ctrld"
|
||||
firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d"
|
||||
firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh"
|
||||
)
|
||||
|
||||
type Firewalla struct {
|
||||
cfg *ctrld.Config
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on Firewalla routers.
|
||||
func New(cfg *ctrld.Config) *Firewalla {
|
||||
return &Firewalla{cfg: cfg}
|
||||
}
|
||||
|
||||
func (f *Firewalla) ConfigureService(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalla) Install(_ *service.Config) error {
|
||||
// Writing startup script.
|
||||
if err := writeFirewallStartupScript(); err != nil {
|
||||
return fmt.Errorf("writing startup script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalla) Uninstall(_ *service.Config) error {
|
||||
// Removing startup script.
|
||||
if err := os.Remove(firewallaCtrldInitScriptPath); err != nil {
|
||||
return fmt.Errorf("removing startup script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalla) PreRun() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalla) Setup() error {
|
||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
data, err := dnsmasq.FirewallaConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating dnsmasq config: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(firewallaDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
||||
return fmt.Errorf("writing ctrld config: %w", err)
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return fmt.Errorf("restartDNSMasq: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewalla) Cleanup() error {
|
||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Removing current config.
|
||||
if err := os.Remove(firewallaDNSMasqConfigPath); err != nil {
|
||||
return fmt.Errorf("removing ctrld config: %w", err)
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return fmt.Errorf("restartDNSMasq: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFirewallStartupScript() error {
|
||||
if err := os.MkdirAll(firewallaConfigPostMainDir, 0775); err != nil {
|
||||
return err
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// This is called when "ctrld start ..." runs, so recording
|
||||
// the same command line arguments to use in startup script.
|
||||
argStr := strings.Join(os.Args[1:], " ")
|
||||
script := fmt.Sprintf("#!/bin/bash\n\nsudo %q %s\n", exe, argStr)
|
||||
return os.WriteFile(firewallaCtrldInitScriptPath, []byte(script), 0755)
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
return exec.Command("systemctl", "restart", "firerouter_dns").Run()
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
package merlin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
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{
|
||||
"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 {
|
||||
cfg *ctrld.Config
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on Merlin routers.
|
||||
func New(cfg *ctrld.Config) *Merlin {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreRun prepares the Merlin instance for operation by waiting for essential services and directories to become available.
|
||||
func (m *Merlin) PreRun() error {
|
||||
// Wait NTP ready.
|
||||
_ = m.Cleanup()
|
||||
if err := ntp.WaitNvram(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Wait until directories mounted.
|
||||
for _, dir := range []string{"/tmp", "/proc"} {
|
||||
waitDirExists(dir)
|
||||
}
|
||||
// Wait dnsmasq started.
|
||||
for {
|
||||
out, _ := exec.Command("pidof", "dnsmasq").CombinedOutput()
|
||||
if len(bytes.TrimSpace(out)) > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup initializes and configures the Merlin instance for use, including setting up dnsmasq and necessary nvram settings.
|
||||
func (m *Merlin) Setup() error {
|
||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Already setup.
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.writeDnsmasqPostconf(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfg := range getDnsmasqConfigs() {
|
||||
if err := m.setupDnsmasq(cfg); err != nil {
|
||||
return fmt.Errorf("failed to setup dnsmasq: config: %s, error: %w", cfg.confPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup restores the original dnsmasq and nvram configurations and restarts dnsmasq if necessary.
|
||||
func (m *Merlin) Cleanup() error {
|
||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
||||
return nil // was restored, nothing to do.
|
||||
}
|
||||
|
||||
// Restore old configs.
|
||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// Restore dnsmasq post conf file.
|
||||
if err := os.WriteFile(dnsmasq.MerlinPostConfPath, merlinParsePostConf(buf), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfg := range getDnsmasqConfigs() {
|
||||
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.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
||||
// Already setup.
|
||||
if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) {
|
||||
return nil
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := dnsmasq.ConfTmpl(dnsmasq.MerlinPostConfTmpl, m.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = strings.Join([]string{
|
||||
data,
|
||||
"\n",
|
||||
dnsmasq.MerlinPostConfMarker,
|
||||
"\n",
|
||||
string(buf),
|
||||
}, "\n")
|
||||
// Write dnsmasq post conf file.
|
||||
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 {
|
||||
if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err)
|
||||
}
|
||||
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 {
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
parts := bytes.Split(buf, []byte(dnsmasq.MerlinPostConfMarker))
|
||||
if len(parts) != 1 {
|
||||
return bytes.TrimLeftFunc(parts[1], unicode.IsSpace)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// waitDirExists waits until the specified directory exists, polling its existence every second.
|
||||
func waitDirExists(dir string) {
|
||||
for {
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package merlin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
)
|
||||
|
||||
func Test_merlinParsePostConf(t *testing.T) {
|
||||
origContent := "# foo"
|
||||
data := strings.Join([]string{
|
||||
dnsmasq.MerlinPostConfTmpl,
|
||||
"\n",
|
||||
dnsmasq.MerlinPostConfMarker,
|
||||
"\n",
|
||||
}, "\n")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
expected string
|
||||
}{
|
||||
{"empty", "", ""},
|
||||
{"no ctrld", origContent, origContent},
|
||||
{"ctrld with data", data + origContent, origContent},
|
||||
{"ctrld without data", data, ""},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
//t.Parallel()
|
||||
if got := merlinParsePostConf([]byte(tc.data)); !bytes.Equal(got, []byte(tc.expected)) {
|
||||
t.Errorf("unexpected result, want: %q, got: %q", tc.expected, string(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package netgear
|
||||
|
||||
const openWrtScript = `#!/bin/sh /etc/rc.common
|
||||
USE_PROCD=1
|
||||
# After dnsmasq starts
|
||||
START=61
|
||||
# Before network stops
|
||||
STOP=89
|
||||
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
||||
name="{{.Name}}"
|
||||
pid_file="/var/run/${name}.pid"
|
||||
|
||||
start_service() {
|
||||
echo "Starting ${name}"
|
||||
procd_open_instance
|
||||
procd_set_param command ${cmd}
|
||||
procd_set_param respawn # respawn automatically if something died
|
||||
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
|
||||
procd_close_instance
|
||||
echo "${name} has been started"
|
||||
}
|
||||
`
|
||||
@@ -1,220 +0,0 @@
|
||||
package netgear
|
||||
|
||||
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"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "netgear_orbi_voxel"
|
||||
netgearOrbiVoxelDNSMasqConfigPath = "/etc/dnsmasq.conf"
|
||||
netgearOrbiVoxelHomedir = "/mnt/bitdefender"
|
||||
netgearOrbiVoxelStartupScript = "/mnt/bitdefender/rc.user"
|
||||
netgearOrbiVoxelStartupScriptBackup = "/mnt/bitdefender/rc.user.bak"
|
||||
netgearOrbiVoxelStartupScriptMarker = "\n# GENERATED BY ctrld"
|
||||
)
|
||||
|
||||
var nvramKvMap = map[string]string{
|
||||
"dns_hijack": "0", // Disable dns hijacking
|
||||
}
|
||||
|
||||
type NetgearOrbiVoxel struct {
|
||||
cfg *ctrld.Config
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers.
|
||||
func New(cfg *ctrld.Config) *NetgearOrbiVoxel {
|
||||
return &NetgearOrbiVoxel{cfg: cfg}
|
||||
}
|
||||
|
||||
func (d *NetgearOrbiVoxel) ConfigureService(svc *service.Config) error {
|
||||
if err := d.checkInstalledDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
svc.Option["SysvScript"] = openWrtScript
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NetgearOrbiVoxel) Install(_ *service.Config) error {
|
||||
// Ignoring error here at this moment is ok, since everything will be wiped out on reboot.
|
||||
_ = exec.Command("/etc/init.d/ctrld", "enable").Run()
|
||||
if err := d.checkInstalledDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backupVoxelStartupScript(); err != nil {
|
||||
return fmt.Errorf("backup startup script: %w", err)
|
||||
}
|
||||
if err := writeVoxelStartupScript(); err != nil {
|
||||
return fmt.Errorf("writing startup script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NetgearOrbiVoxel) Uninstall(_ *service.Config) error {
|
||||
if err := os.Remove(netgearOrbiVoxelStartupScript); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
err := os.Rename(netgearOrbiVoxelStartupScriptBackup, netgearOrbiVoxelStartupScript)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NetgearOrbiVoxel) PreRun() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NetgearOrbiVoxel) Setup() error {
|
||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Already setup.
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, d.cfg, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentConfig, _ := os.ReadFile(netgearOrbiVoxelDNSMasqConfigPath)
|
||||
configContent := append(currentConfig, data...)
|
||||
if err := os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, configContent, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *NetgearOrbiVoxel) Cleanup() error {
|
||||
if d.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
||||
return nil // was restored, nothing to do.
|
||||
}
|
||||
|
||||
// Restore old configs.
|
||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore dnsmasq config.
|
||||
if err := restoreDnsmasqConf(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkInstalledDir checks that ctrld binary was installed in the correct directory.
|
||||
func (d *NetgearOrbiVoxel) checkInstalledDir() error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checkHomeDir: failed to get binary path %w", err)
|
||||
}
|
||||
if !strings.HasSuffix(filepath.Dir(exePath), netgearOrbiVoxelHomedir) {
|
||||
return fmt.Errorf("checkHomeDir: could not install service outside %s", netgearOrbiVoxelHomedir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupVoxelStartupScript creates a backup of original startup script if existed.
|
||||
func backupVoxelStartupScript() error {
|
||||
// Do nothing if the startup script was modified by ctrld.
|
||||
script, _ := os.ReadFile(netgearOrbiVoxelStartupScript)
|
||||
if bytes.Contains(script, []byte(netgearOrbiVoxelStartupScriptMarker)) {
|
||||
return nil
|
||||
}
|
||||
err := os.Rename(netgearOrbiVoxelStartupScript, netgearOrbiVoxelStartupScriptBackup)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("backupVoxelStartupScript: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeVoxelStartupScript writes startup script to re-install ctrld upon reboot.
|
||||
// See: https://github.com/SVoxel/ORBI-RBK50/pull/7
|
||||
func writeVoxelStartupScript() error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure service: failed to get binary path %w", err)
|
||||
}
|
||||
// This is called when "ctrld start ..." runs, so recording
|
||||
// the same command line arguments to use in startup script.
|
||||
argStr := strings.Join(os.Args[1:], " ")
|
||||
script, _ := os.ReadFile(netgearOrbiVoxelStartupScriptBackup)
|
||||
script = append(script, fmt.Sprintf("%s\n%q %s\n", netgearOrbiVoxelStartupScriptMarker, exe, argStr)...)
|
||||
f, err := os.Create(netgearOrbiVoxelStartupScript)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create startup script: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.Write(script); err != nil {
|
||||
return fmt.Errorf("failed to write startup script: %w", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("failed to save startup script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreDnsmasqConf restores original dnsmasq configuration.
|
||||
func restoreDnsmasqConf() error {
|
||||
f, err := os.Open(netgearOrbiVoxelDNSMasqConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var bs []byte
|
||||
buf := bytes.NewBuffer(bs)
|
||||
|
||||
removed := false
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == dnsmasq.CtrldMarker {
|
||||
removed = true
|
||||
}
|
||||
if !removed {
|
||||
_, err := buf.WriteString(line + "\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, buf.Bytes(), 0644)
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("restartDNSMasq: %s, %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package ntp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
// WaitNvram waits NTP synced by checking "ntp_ready" value using nvram.
|
||||
func WaitNvram() error {
|
||||
// Wait until `ntp_ready=1` set.
|
||||
b := backoff.NewBackoff("ntp.Wait", func(format string, args ...any) {}, 10*time.Second)
|
||||
for {
|
||||
// ddwrt use "ntp_done": https://github.com/mirror/dd-wrt/blob/a08c693527ab3204bf7bebd408a7c9a83b6ede47/src/router/rc/ntp.c#L100
|
||||
for _, key := range []string{"ntp_ready", "ntp_done"} {
|
||||
out, err := nvram.Run("get", key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("PreStart: nvram: %w", err)
|
||||
}
|
||||
if out == "1" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
b.BackOff(context.Background(), errors.New("ntp not ready"))
|
||||
}
|
||||
}
|
||||
|
||||
// WaitUpstart waits NTP synced by checking upstart task "ntpsync" is in "stop/waiting" state.
|
||||
func WaitUpstart() error {
|
||||
// Wait until `initctl status ntpsync` returns stop state.
|
||||
b := backoff.NewBackoff("ntp.WaitUpstart", func(format string, args ...any) {}, 10*time.Second)
|
||||
for {
|
||||
out, err := exec.Command("initctl", "status", "ntpsync").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec.Command: %w", err)
|
||||
}
|
||||
if bytes.Contains(out, []byte("stop/waiting")) {
|
||||
return nil
|
||||
}
|
||||
b.BackOff(context.Background(), errors.New("ntp not ready"))
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package nvram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
CtrldKeyPrefix = "ctrld_"
|
||||
CtrldSetupKey = "ctrld_setup"
|
||||
CtrldInstallKey = "ctrld_install"
|
||||
RCStartupKey = "rc_startup"
|
||||
)
|
||||
|
||||
// Run runs the given nvram command.
|
||||
func Run(args ...string) (string, error) {
|
||||
cmd := exec.Command("nvram", args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("%s:%w", stderr.String(), err)
|
||||
}
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE:
|
||||
- For Openwrt, DNSSEC is not included in default dnsmasq (require dnsmasq-full).
|
||||
- For Merlin, DNSSEC is configured during postconf script (see merlinDNSMasqPostConfTmpl).
|
||||
- For Ubios UDM Pro/Dream Machine, DNSSEC is not included in their dnsmasq package:
|
||||
+https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca
|
||||
+https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1
|
||||
*/
|
||||
|
||||
// SetKV writes the given key/value from map to nvram.
|
||||
// The given setupKey is set to 1 to indicates key/value set.
|
||||
func SetKV(m map[string]string, setupKey string) error {
|
||||
// Backup current value, store ctrld's configs.
|
||||
for key, value := range m {
|
||||
old, err := Run("get", key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", old, err)
|
||||
}
|
||||
if out, err := Run("set", CtrldKeyPrefix+key+"="+old); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
if out, err := Run("set", key+"="+value); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := Run("set", setupKey+"=1"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
// Commit.
|
||||
if out, err := Run("commit"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore restores the old value of given key from map m.
|
||||
// The given setupKey is set to 0 to indicates key/value restored.
|
||||
func Restore(m map[string]string, setupKey string) error {
|
||||
// Restore old configs.
|
||||
for key := range m {
|
||||
ctrldKey := CtrldKeyPrefix + key
|
||||
old, err := Run("get", ctrldKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", old, err)
|
||||
}
|
||||
_, _ = Run("unset", ctrldKey)
|
||||
if out, err := Run("set", key+"="+old); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := Run("unset", setupKey); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
// Commit.
|
||||
if out, err := Run("commit"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
package openwrt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"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 = "openwrt"
|
||||
openwrtDNSMasqConfigName = "ctrld.conf"
|
||||
openwrtDNSMasqDefaultConfigDir = "/tmp/dnsmasq.d"
|
||||
)
|
||||
|
||||
var openwrtDnsmasqDefaultConfigPath = filepath.Join(openwrtDNSMasqDefaultConfigDir, openwrtDNSMasqConfigName)
|
||||
|
||||
type Openwrt struct {
|
||||
cfg *ctrld.Config
|
||||
dnsmasqCacheSize string
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on Openwrt routers.
|
||||
func New(cfg *ctrld.Config) *Openwrt {
|
||||
return &Openwrt{cfg: cfg}
|
||||
}
|
||||
|
||||
func (o *Openwrt) ConfigureService(svc *service.Config) error {
|
||||
svc.Option["SysvScript"] = openWrtScript
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Openwrt) Install(config *service.Config) error {
|
||||
return exec.Command("/etc/init.d/ctrld", "enable").Run()
|
||||
}
|
||||
|
||||
func (o *Openwrt) Uninstall(config *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Openwrt) PreRun() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Openwrt) Setup() error {
|
||||
if o.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save current dnsmasq config cache size if present.
|
||||
if cs, err := uci("get", "dhcp.@dnsmasq[0].cachesize"); err == nil {
|
||||
o.dnsmasqCacheSize = cs
|
||||
if _, err := uci("delete", "dhcp.@dnsmasq[0].cachesize"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Commit.
|
||||
if _, err := uci("commit", "dhcp"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(dnsmasqConfPathFromUbus(), []byte(data), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Openwrt) Cleanup() error {
|
||||
if o.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Remove the custom dnsmasq config
|
||||
if err := os.Remove(dnsmasqConfPathFromUbus()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore original value if present.
|
||||
if o.dnsmasqCacheSize != "" {
|
||||
if _, err := uci("set", fmt.Sprintf("dhcp.@dnsmasq[0].cachesize=%s", o.dnsmasqCacheSize)); err != nil {
|
||||
return err
|
||||
}
|
||||
// Commit.
|
||||
if _, err := uci("commit", "dhcp"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%s: %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errUCIEntryNotFound = errors.New("uci: Entry not found")
|
||||
|
||||
func uci(args ...string) (string, error) {
|
||||
cmd := exec.Command("uci", args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if strings.HasPrefix(stderr.String(), errUCIEntryNotFound.Error()) {
|
||||
return "", errUCIEntryNotFound
|
||||
}
|
||||
return "", fmt.Errorf("%s:%w", stderr.String(), err)
|
||||
}
|
||||
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))
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package openwrt
|
||||
|
||||
const openWrtScript = `#!/bin/sh /etc/rc.common
|
||||
USE_PROCD=1
|
||||
# After network starts
|
||||
START=21
|
||||
# Before network stops
|
||||
STOP=89
|
||||
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
||||
name="{{.Name}}"
|
||||
pid_file="/var/run/${name}.pid"
|
||||
|
||||
start_service() {
|
||||
echo "Starting ${name}"
|
||||
procd_open_instance
|
||||
procd_set_param command ${cmd}
|
||||
procd_set_param respawn # respawn automatically if something died
|
||||
procd_set_param stdout 1 # forward stdout of the command to logd
|
||||
procd_set_param stderr 1 # same for stderr
|
||||
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
|
||||
procd_set_param term_timeout 10
|
||||
procd_close_instance
|
||||
echo "${name} has been started"
|
||||
}
|
||||
`
|
||||
@@ -1,40 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config represents /conf/config.xml file found on pfsense/opnsense.
|
||||
type Config struct {
|
||||
PfsenseUnbound *string `xml:"unbound>enable,omitempty"`
|
||||
OPNsenseUnbound *string `xml:"OPNsense>unboundplus>general>enabled,omitempty"`
|
||||
Dnsmasq *string `xml:"dnsmasq>enable,omitempty"`
|
||||
}
|
||||
|
||||
// DnsmasqEnabled reports whether dnsmasq is enabled.
|
||||
func (c *Config) DnsmasqEnabled() bool {
|
||||
if isPfsense() { // pfsense only set the attribute if dnsmasq is enabled.
|
||||
return c.Dnsmasq != nil
|
||||
}
|
||||
return c.Dnsmasq != nil && *c.Dnsmasq == "1"
|
||||
}
|
||||
|
||||
// UnboundEnabled reports whether unbound is enabled.
|
||||
func (c *Config) UnboundEnabled() bool {
|
||||
if isPfsense() { // pfsense only set the attribute if unbound is enabled.
|
||||
return c.PfsenseUnbound != nil
|
||||
}
|
||||
return c.OPNsenseUnbound != nil && *c.OPNsenseUnbound == "1"
|
||||
}
|
||||
|
||||
// currentConfig does unmarshalling /conf/config.xml file,
|
||||
// return the corresponding *Config represent it.
|
||||
func currentConfig() (*Config, error) {
|
||||
buf, _ := os.ReadFile("/conf/config.xml")
|
||||
c := Config{}
|
||||
if err := xml.Unmarshal(buf, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
const (
|
||||
osName = "freebsd"
|
||||
rcPath = "/usr/local/etc/rc.d"
|
||||
rcConfPath = "/etc/rc.conf.d/"
|
||||
unboundRcPath = rcPath + "/unbound"
|
||||
dnsmasqRcPath = rcPath + "/dnsmasq"
|
||||
)
|
||||
|
||||
func newOsRouter(cfg *ctrld.Config, cdMode bool) Router {
|
||||
return &osRouter{cfg: cfg, cdMode: cdMode}
|
||||
}
|
||||
|
||||
type osRouter struct {
|
||||
cfg *ctrld.Config
|
||||
svcName string
|
||||
// cdMode indicates whether the router will configure ctrld in cd mode (aka --cd=<uid>).
|
||||
// When ctrld is running on freebsd-like routers, and there's process running on port 53
|
||||
// in cd mode, ctrld will attempt to kill the process and become direct listener.
|
||||
// See details implemenation in osRouter.PreRun method.
|
||||
cdMode bool
|
||||
}
|
||||
|
||||
func (or *osRouter) ConfigureService(svc *service.Config) error {
|
||||
svc.Option["SysvScript"] = bsdInitScript
|
||||
or.svcName = svc.Name
|
||||
rcFile := filepath.Join(rcConfPath, or.svcName)
|
||||
var to = &struct {
|
||||
Name string
|
||||
}{
|
||||
or.svcName,
|
||||
}
|
||||
|
||||
f, err := os.Create(rcFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Create: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := template.Must(template.New("").Parse(rcConfTmpl)).Execute(f, to); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (or *osRouter) Install(_ *service.Config) error {
|
||||
if isPfsense() {
|
||||
// pfsense need ".sh" extension for script to be run at boot.
|
||||
// See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option
|
||||
oldname := filepath.Join(rcPath, or.svcName)
|
||||
newname := filepath.Join(rcPath, or.svcName+".sh")
|
||||
_ = os.Remove(newname)
|
||||
if err := os.Symlink(oldname, newname); err != nil {
|
||||
return fmt.Errorf("os.Symlink: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (or *osRouter) Uninstall(_ *service.Config) error {
|
||||
rcFiles := []string{filepath.Join(rcConfPath, or.svcName)}
|
||||
if isPfsense() {
|
||||
rcFiles = append(rcFiles, filepath.Join(rcPath, or.svcName+".sh"))
|
||||
}
|
||||
for _, filename := range rcFiles {
|
||||
if err := os.Remove(filename); err != nil {
|
||||
return fmt.Errorf("os.Remove: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (or *osRouter) PreRun() error {
|
||||
if or.cdMode {
|
||||
addr := "0.0.0.0:53"
|
||||
udpLn, udpErr := net.ListenPacket("udp", addr)
|
||||
if udpLn != nil {
|
||||
udpLn.Close()
|
||||
}
|
||||
tcpLn, tcpErr := net.Listen("tcp", addr)
|
||||
if tcpLn != nil {
|
||||
tcpLn.Close()
|
||||
}
|
||||
// If we could not listen on :53 for any reason, try killing unbound/dnsmasq, become direct listener
|
||||
if udpErr != nil || tcpErr != nil {
|
||||
_ = exec.Command("killall", "unbound").Run()
|
||||
_ = exec.Command("killall", "dnsmasq").Run()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (or *osRouter) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (or *osRouter) Cleanup() error {
|
||||
if or.cdMode {
|
||||
c, err := currentConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.UnboundEnabled() {
|
||||
_ = exec.Command(unboundRcPath, "onerestart").Run()
|
||||
}
|
||||
if c.DnsmasqEnabled() {
|
||||
_ = exec.Command(dnsmasqRcPath, "onerestart").Run()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPfsense() bool {
|
||||
b, err := os.ReadFile("/etc/platform")
|
||||
return err == nil && bytes.HasPrefix(b, []byte("pfSense"))
|
||||
}
|
||||
|
||||
const bsdInitScript = `#!/bin/sh
|
||||
|
||||
# PROVIDE: {{.Name}}
|
||||
# REQUIRE: SERVERS
|
||||
# REQUIRE: unbound dnsmasq securelevel
|
||||
# KEYWORD: shutdown
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="{{.Name}}"
|
||||
rcvar="${name}_enable"
|
||||
{{.Name}}_env="IS_DAEMON=1"
|
||||
pidfile="/var/run/${name}.pid"
|
||||
child_pidfile="/var/run/${name}_child.pid"
|
||||
command="/usr/sbin/daemon"
|
||||
daemon_args="-r -P ${pidfile} -p ${child_pidfile} -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}"
|
||||
command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||
|
||||
load_rc_config "${name}"
|
||||
run_rc_command "$1"
|
||||
`
|
||||
|
||||
var rcConfTmpl = `# {{.Name}}
|
||||
{{.Name}}_enable="YES"
|
||||
`
|
||||
@@ -1,41 +0,0 @@
|
||||
//go:build !freebsd
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
const osName = ""
|
||||
|
||||
func newOsRouter(cfg *ctrld.Config, cdMode bool) Router {
|
||||
return &osRouter{}
|
||||
}
|
||||
|
||||
type osRouter struct{}
|
||||
|
||||
func (d *osRouter) ConfigureService(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *osRouter) Install(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *osRouter) Uninstall(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *osRouter) PreRun() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *osRouter) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *osRouter) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/certs"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/edgeos"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/firewalla"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/merlin"
|
||||
netgear "github.com/Control-D-Inc/ctrld/internal/router/netgear_orbi_voxel"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/openwrt"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/synology"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/tomato"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ubios"
|
||||
)
|
||||
|
||||
// Service is the interface to manage ctrld service on router.
|
||||
type Service interface {
|
||||
// ConfigureService performs works for installing ctrla as a service on router.
|
||||
ConfigureService(*service.Config) error
|
||||
// Install performs necessary works after service.Install done.
|
||||
Install(*service.Config) error
|
||||
// Uninstall performs necessary works after service.Uninstallation done.
|
||||
Uninstall(*service.Config) error
|
||||
}
|
||||
|
||||
// Router is the interface for managing ctrld running on router.
|
||||
type Router interface {
|
||||
Service
|
||||
|
||||
// PreRun performs works need to be done before ctrld being run on router.
|
||||
// Implementation should only return if the pre-condition was met (e.g: ntp synced).
|
||||
PreRun() error
|
||||
// Setup configures ctrld to be run on the router.
|
||||
Setup() error
|
||||
// Cleanup cleans up works setup on router by ctrld.
|
||||
Cleanup() error
|
||||
}
|
||||
|
||||
// New returns new Router interface.
|
||||
func New(cfg *ctrld.Config, cdMode bool) Router {
|
||||
switch Name() {
|
||||
case ddwrt.Name:
|
||||
return ddwrt.New(cfg)
|
||||
case merlin.Name:
|
||||
return merlin.New(cfg)
|
||||
case openwrt.Name:
|
||||
return openwrt.New(cfg)
|
||||
case edgeos.Name:
|
||||
return edgeos.New(cfg)
|
||||
case ubios.Name:
|
||||
return ubios.New(cfg)
|
||||
case synology.Name:
|
||||
return synology.New(cfg)
|
||||
case tomato.Name:
|
||||
return tomato.New(cfg)
|
||||
case firewalla.Name:
|
||||
return firewalla.New(cfg)
|
||||
case netgear.Name:
|
||||
return netgear.New(cfg)
|
||||
}
|
||||
return newOsRouter(cfg, cdMode)
|
||||
}
|
||||
|
||||
// IsNetGearOrbi reports whether the router is a Netgear Orbi router.
|
||||
func IsNetGearOrbi() bool {
|
||||
return Name() == netgear.Name
|
||||
}
|
||||
|
||||
// IsGLiNet reports whether the router is an GL.iNet router.
|
||||
func IsGLiNet() bool {
|
||||
if Name() != openwrt.Name {
|
||||
return false
|
||||
}
|
||||
buf, _ := os.ReadFile("/proc/version")
|
||||
// The output of /proc/version contains "(glinet@glinet)".
|
||||
return bytes.Contains(buf, []byte(" (glinet"))
|
||||
}
|
||||
|
||||
// IsOldOpenwrt reports whether the router is an "old" version of Openwrt,
|
||||
// aka versions which don't have "service" command.
|
||||
func IsOldOpenwrt() bool {
|
||||
if Name() != openwrt.Name {
|
||||
return false
|
||||
}
|
||||
cmd, _ := exec.LookPath("service")
|
||||
return cmd == ""
|
||||
}
|
||||
|
||||
// WaitProcessExited reports whether the "ctrld stop" command have to wait until ctrld process exited.
|
||||
func WaitProcessExited() bool {
|
||||
return Name() == openwrt.Name
|
||||
}
|
||||
|
||||
var routerPlatform atomic.Pointer[router]
|
||||
|
||||
type router struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// Name returns name of the router platform.
|
||||
func Name() string {
|
||||
if r := routerPlatform.Load(); r != nil {
|
||||
return r.name
|
||||
}
|
||||
r := &router{}
|
||||
r.name = distroName()
|
||||
routerPlatform.Store(r)
|
||||
return r.name
|
||||
}
|
||||
|
||||
// DefaultInterfaceName returns the default interface name of the current router.
|
||||
func DefaultInterfaceName() string {
|
||||
switch Name() {
|
||||
case ubios.Name:
|
||||
return "lo"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LocalResolverIP returns the IP that could be used as nameserver in /etc/resolv.conf file.
|
||||
func LocalResolverIP() string {
|
||||
var iface string
|
||||
switch Name() {
|
||||
case edgeos.Name:
|
||||
// On EdgeOS, dnsmasq is run with "--local-service", so we need to get
|
||||
// the proper interface from dnsmasq config.
|
||||
if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" {
|
||||
iface = name
|
||||
}
|
||||
case firewalla.Name:
|
||||
// On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces.
|
||||
// Thus, we use "br0" as the nameserver in /etc/resolv.conf file.
|
||||
iface = "br0"
|
||||
}
|
||||
if netIface, _ := net.InterfaceByName(iface); netIface != nil {
|
||||
addrs, _ := netIface.Addrs()
|
||||
for _, addr := range addrs {
|
||||
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil {
|
||||
return netIP.IP.To4().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// HomeDir returns the home directory of ctrld on current router.
|
||||
func HomeDir() (string, error) {
|
||||
switch Name() {
|
||||
case ddwrt.Name, firewalla.Name, merlin.Name, netgear.Name, tomato.Name:
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Dir(exe), nil
|
||||
case edgeos.Name:
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Using binary directory as home dir if it is located in /config.
|
||||
// Otherwise, fallback to old behavior for compatibility.
|
||||
if strings.HasPrefix(exe, "/config/") {
|
||||
return filepath.Dir(exe), nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// CertPool returns the system certificate pool of the current router.
|
||||
func CertPool() *x509.CertPool {
|
||||
if Name() == ddwrt.Name {
|
||||
return certs.CACertPool()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanListenLocalhost reports whether the ctrld can listen on localhost with current host.
|
||||
func CanListenLocalhost() bool {
|
||||
switch {
|
||||
case Name() == firewalla.Name:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// SelfInterfaces return list of *net.Interface that will be source of requests from router itself.
|
||||
func SelfInterfaces() []*net.Interface {
|
||||
switch Name() {
|
||||
case firewalla.Name:
|
||||
return dnsmasq.FirewallaSelfInterfaces()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// LeaseFilesDir is the directory which contains lease files.
|
||||
func LeaseFilesDir() string {
|
||||
if Name() == edgeos.Name {
|
||||
edgeos.LeaseFileDir()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ServiceDependencies returns list of dependencies that ctrld services needs on this router.
|
||||
// See https://pkg.go.dev/github.com/kardianos/service#Config for list format.
|
||||
func ServiceDependencies() []string {
|
||||
if Name() == ubios.Name {
|
||||
// On Ubios, ctrld needs to start after unifi-mongodb,
|
||||
// so it can query custom client info mapping.
|
||||
return []string{
|
||||
"Wants=unifi-mongodb.service",
|
||||
"After=unifi-mongodb.service",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func distroName() string {
|
||||
switch {
|
||||
case bytes.HasPrefix(unameO(), []byte("DD-WRT")):
|
||||
return ddwrt.Name
|
||||
case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")):
|
||||
return merlin.Name
|
||||
case haveFile("/etc/openwrt_version"):
|
||||
if haveFile("/bin/config") { // TODO: is there any more reliable way?
|
||||
return netgear.Name
|
||||
}
|
||||
return openwrt.Name
|
||||
case isUbios():
|
||||
return ubios.Name
|
||||
case bytes.HasPrefix(unameU(), []byte("synology")):
|
||||
return synology.Name
|
||||
case bytes.HasPrefix(unameO(), []byte("Tomato")):
|
||||
return tomato.Name
|
||||
case haveDir("/config/scripts/post-config.d"):
|
||||
return edgeos.Name
|
||||
case haveFile("/etc/ubnt/init/vyatta-router"):
|
||||
return edgeos.Name // For 2.x
|
||||
case haveFile("/etc/firewalla_release"):
|
||||
return firewalla.Name
|
||||
}
|
||||
return osName
|
||||
}
|
||||
|
||||
func haveFile(file string) bool {
|
||||
_, err := os.Stat(file)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func haveDir(dir string) bool {
|
||||
fi, _ := os.Stat(dir)
|
||||
return fi != nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func unameO() []byte {
|
||||
out, _ := exec.Command("uname", "-o").Output()
|
||||
return out
|
||||
}
|
||||
|
||||
func unameU() []byte {
|
||||
out, _ := exec.Command("uname", "-u").Output()
|
||||
return out
|
||||
}
|
||||
|
||||
// isUbios reports whether the current machine is running on Ubios.
|
||||
func isUbios() bool {
|
||||
if haveDir("/data/unifi") {
|
||||
return true
|
||||
}
|
||||
if err := exec.Command("ubnt-device-info", "firmware").Run(); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ddwrt"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/merlin"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/tomato"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ubios"
|
||||
)
|
||||
|
||||
func init() {
|
||||
systems := []service.System{
|
||||
&linuxSystemService{
|
||||
name: "ddwrt",
|
||||
detect: func() bool { return Name() == ddwrt.Name },
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newddwrtService,
|
||||
},
|
||||
&linuxSystemService{
|
||||
name: "merlin",
|
||||
detect: func() bool { return Name() == merlin.Name },
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newMerlinService,
|
||||
},
|
||||
&linuxSystemService{
|
||||
name: "ubios",
|
||||
detect: func() bool {
|
||||
if Name() != ubios.Name {
|
||||
return false
|
||||
}
|
||||
out, err := exec.Command("ubnt-device-info", "firmware").CombinedOutput()
|
||||
if err == nil {
|
||||
// For v2/v3, UbiOS use a Debian base with systemd, so it is not
|
||||
// necessary to use custom implementation for supporting init system.
|
||||
return bytes.HasPrefix(out, []byte("1."))
|
||||
}
|
||||
return true
|
||||
},
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newUbiosService,
|
||||
},
|
||||
&linuxSystemService{
|
||||
name: "tomato",
|
||||
detect: func() bool { return Name() == tomato.Name },
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newTomatoService,
|
||||
},
|
||||
}
|
||||
systems = append(systems, service.AvailableSystems()...)
|
||||
service.ChooseSystem(systems...)
|
||||
}
|
||||
|
||||
type linuxSystemService struct {
|
||||
name string
|
||||
detect func() bool
|
||||
interactive func() bool
|
||||
new func(i service.Interface, platform string, c *service.Config) (service.Service, error)
|
||||
}
|
||||
|
||||
func (sc linuxSystemService) String() string {
|
||||
return sc.name
|
||||
}
|
||||
func (sc linuxSystemService) Detect() bool {
|
||||
return sc.detect()
|
||||
}
|
||||
func (sc linuxSystemService) Interactive() bool {
|
||||
return sc.interactive()
|
||||
}
|
||||
func (sc linuxSystemService) New(i service.Interface, c *service.Config) (service.Service, error) {
|
||||
return sc.new(i, sc.String(), c)
|
||||
}
|
||||
|
||||
func isInteractive() (bool, error) {
|
||||
ppid := os.Getppid()
|
||||
if ppid == 1 {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
type ddwrtSvc struct {
|
||||
i service.Interface
|
||||
platform string
|
||||
*service.Config
|
||||
rcStartup string
|
||||
}
|
||||
|
||||
func newddwrtService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||||
s := &ddwrtSvc{
|
||||
i: i,
|
||||
platform: platform,
|
||||
Config: c,
|
||||
}
|
||||
if err := os.MkdirAll("/jffs/etc/config", 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Platform() string {
|
||||
return s.platform
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) configPath() string {
|
||||
return fmt.Sprintf("/jffs/etc/config/%s.startup", s.Config.Name)
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) template() *template.Template {
|
||||
return template.Must(template.New("").Parse(ddwrtSvcScript))
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Install() error {
|
||||
confPath := s.configPath()
|
||||
if _, err := os.Stat(confPath); err == nil {
|
||||
return fmt.Errorf("already installed: %s", confPath)
|
||||
}
|
||||
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "/jffs/") {
|
||||
return errors.New("could not install service outside /jffs")
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*service.Config
|
||||
Path string
|
||||
}{
|
||||
s.Config,
|
||||
path,
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := s.template().Execute(f, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Chmod(confPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
if err := template.Must(template.New("").Parse(ddwrtStartupCmd)).Execute(&sb, to); err != nil {
|
||||
return err
|
||||
}
|
||||
s.rcStartup = sb.String()
|
||||
curVal, err := nvram.Run("get", nvram.RCStartupKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := nvram.Run("set", nvram.CtrldKeyPrefix+nvram.RCStartupKey+"="+curVal); err != nil {
|
||||
return err
|
||||
}
|
||||
val := strings.Join([]string{curVal, s.rcStartup + " &", fmt.Sprintf(`echo $! > "/tmp/%s.pid"`, s.Config.Name)}, "\n")
|
||||
|
||||
if _, err := nvram.Run("set", nvram.RCStartupKey+"="+val); err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := nvram.Run("commit"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Uninstall() error {
|
||||
if err := os.Remove(s.configPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctrldStartupKey := nvram.CtrldKeyPrefix + nvram.RCStartupKey
|
||||
rcStartup, err := nvram.Run("get", ctrldStartupKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = nvram.Run("unset", ctrldStartupKey)
|
||||
if _, err := nvram.Run("set", nvram.RCStartupKey+"="+rcStartup); err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := nvram.Run("commit"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||||
if service.Interactive() {
|
||||
return service.ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||||
// TODO(cuonglm): detect syslog enable and return proper logger?
|
||||
// this at least works with default configuration.
|
||||
if service.Interactive() {
|
||||
return service.ConsoleLogger, nil
|
||||
|
||||
}
|
||||
return &noopLogger{}, nil
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Run() (err error) {
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if interactice, _ := isInteractive(); !interactice {
|
||||
signal.Ignore(syscall.SIGHUP)
|
||||
}
|
||||
var sigChan = make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||
<-sigChan
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Status() (service.Status, error) {
|
||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||||
return service.StatusUnknown, service.ErrNotInstalled
|
||||
}
|
||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||||
if err != nil {
|
||||
return service.StatusUnknown, err
|
||||
}
|
||||
switch string(bytes.TrimSpace(out)) {
|
||||
case "running":
|
||||
return service.StatusRunning, nil
|
||||
default:
|
||||
return service.StatusStopped, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Start() error {
|
||||
return exec.Command(s.configPath(), "start").Run()
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Stop() error {
|
||||
return exec.Command(s.configPath(), "stop").Run()
|
||||
}
|
||||
|
||||
func (s *ddwrtSvc) Restart() error {
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
type noopLogger struct {
|
||||
}
|
||||
|
||||
func (c noopLogger) Error(v ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (c noopLogger) Warning(v ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (c noopLogger) Info(v ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (c noopLogger) Errorf(format string, a ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (c noopLogger) Warningf(format string, a ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
func (c noopLogger) Infof(format string, a ...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const ddwrtStartupCmd = `{{.Path}}{{range .Arguments}} {{.}}{{end}}`
|
||||
const ddwrtSvcScript = `#!/bin/sh
|
||||
|
||||
name="{{.Name}}"
|
||||
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||
pid_file="/tmp/$name.pid"
|
||||
|
||||
get_pid() {
|
||||
cat "$pid_file"
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if is_running; then
|
||||
echo "Already started"
|
||||
else
|
||||
echo "Starting $name"
|
||||
$cmd &
|
||||
echo $! > "$pid_file"
|
||||
chmod 600 "$pid_file"
|
||||
if ! is_running; then
|
||||
echo "Failed to start $name"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if is_running; then
|
||||
echo -n "Stopping $name..."
|
||||
kill "$(get_pid)"
|
||||
for _ in 1 2 3 4 5; do
|
||||
if ! is_running; then
|
||||
echo "stopped"
|
||||
if [ -f "$pid_file" ]; then
|
||||
rm "$pid_file"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
printf "."
|
||||
sleep 2
|
||||
done
|
||||
echo "failed to stop $name"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
status)
|
||||
if is_running; then
|
||||
echo "running"
|
||||
else
|
||||
echo "stopped"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
@@ -1,360 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
const (
|
||||
merlinJFFSScriptPath = "/jffs/scripts/services-start"
|
||||
merlinJFFSServiceEventScriptPath = "/jffs/scripts/service-event"
|
||||
)
|
||||
|
||||
type merlinSvc struct {
|
||||
i service.Interface
|
||||
platform string
|
||||
*service.Config
|
||||
}
|
||||
|
||||
func newMerlinService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||||
s := &merlinSvc{
|
||||
i: i,
|
||||
platform: platform,
|
||||
Config: c,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *merlinSvc) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Platform() string {
|
||||
return s.platform
|
||||
}
|
||||
|
||||
func (s *merlinSvc) configPath() string {
|
||||
bin := s.Config.Executable
|
||||
if bin == "" {
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
bin = path
|
||||
}
|
||||
return bin + ".startup"
|
||||
}
|
||||
|
||||
func (s *merlinSvc) template() *template.Template {
|
||||
return template.Must(template.New("").Parse(merlinSvcScript))
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Install() error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(exePath, "/jffs/") {
|
||||
return errors.New("could not install service outside /jffs")
|
||||
}
|
||||
if _, err := nvram.Run("set", "jffs2_scripts=1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := nvram.Run("commit"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confPath := s.configPath()
|
||||
if _, err := os.Stat(confPath); err == nil {
|
||||
return fmt.Errorf("already installed: %s", confPath)
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*service.Config
|
||||
Path string
|
||||
}{
|
||||
s.Config,
|
||||
exePath,
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Create: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := s.template().Execute(f, to); err != nil {
|
||||
return fmt.Errorf("s.template.Execute: %w", err)
|
||||
}
|
||||
|
||||
if err = os.Chmod(confPath, 0755); err != nil {
|
||||
return fmt.Errorf("os.Chmod: startup script: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(merlinJFFSScriptPath), 0755); err != nil {
|
||||
return fmt.Errorf("os.MkdirAll: %w", err)
|
||||
}
|
||||
|
||||
tmpScript, err := os.CreateTemp("", "ctrld_install")
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.CreateTemp: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpScript.Name())
|
||||
defer tmpScript.Close()
|
||||
|
||||
if _, err := tmpScript.WriteString(merlinAddLineToScript); err != nil {
|
||||
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
||||
}
|
||||
if err := tmpScript.Close(); err != nil {
|
||||
return fmt.Errorf("tmpScript.Close: %w", err)
|
||||
}
|
||||
addLineToScript := func(line, script string) error {
|
||||
if _, err := os.Stat(script); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(script, 0755); err != nil {
|
||||
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
||||
}
|
||||
|
||||
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
||||
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for script, line := range map[string]string{
|
||||
merlinJFFSScriptPath: s.configPath() + " start",
|
||||
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
||||
} {
|
||||
if err := addLineToScript(line, script); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Uninstall() error {
|
||||
if err := os.Remove(s.configPath()); err != nil {
|
||||
return fmt.Errorf("os.Remove: %w", err)
|
||||
}
|
||||
tmpScript, err := os.CreateTemp("", "ctrld_uninstall")
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.CreateTemp: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpScript.Name())
|
||||
defer tmpScript.Close()
|
||||
|
||||
if _, err := tmpScript.WriteString(merlinRemoveLineFromScript); err != nil {
|
||||
return fmt.Errorf("tmpScript.WriteString: %w", err)
|
||||
}
|
||||
if err := tmpScript.Close(); err != nil {
|
||||
return fmt.Errorf("tmpScript.Close: %w", err)
|
||||
}
|
||||
removeLineFromScript := func(line, script string) error {
|
||||
if _, err := os.Stat(script); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(script, 0755); err != nil {
|
||||
return fmt.Errorf("os.Chmod: jffs script: %w", err)
|
||||
}
|
||||
|
||||
if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil {
|
||||
return fmt.Errorf("exec.Command: add startup script: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for script, line := range map[string]string{
|
||||
merlinJFFSScriptPath: s.configPath() + " start",
|
||||
merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`,
|
||||
} {
|
||||
if err := removeLineFromScript(line, script); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||||
if service.Interactive() {
|
||||
return service.ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
|
||||
func (s *merlinSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||||
return newSysLogger(s.Name, errs)
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Run() (err error) {
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if interactice, _ := isInteractive(); !interactice {
|
||||
signal.Ignore(syscall.SIGHUP)
|
||||
}
|
||||
|
||||
var sigChan = make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||
<-sigChan
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Status() (service.Status, error) {
|
||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||||
return service.StatusUnknown, service.ErrNotInstalled
|
||||
}
|
||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||||
if err != nil {
|
||||
return service.StatusUnknown, err
|
||||
}
|
||||
switch string(bytes.TrimSpace(out)) {
|
||||
case "running":
|
||||
return service.StatusRunning, nil
|
||||
default:
|
||||
return service.StatusStopped, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Start() error {
|
||||
return exec.Command(s.configPath(), "start").Run()
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Stop() error {
|
||||
return exec.Command(s.configPath(), "stop").Run()
|
||||
}
|
||||
|
||||
func (s *merlinSvc) Restart() error {
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
const merlinSvcScript = `#!/bin/sh
|
||||
|
||||
name="{{.Name}}"
|
||||
cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||
pid_file="/tmp/$name.pid"
|
||||
|
||||
get_pid() {
|
||||
cat "$pid_file"
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) "
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if is_running; then
|
||||
logger -c "Already started"
|
||||
else
|
||||
logger -c "Starting $name"
|
||||
if [ -f /rom/ca-bundle.crt ]; then
|
||||
# For John’s fork
|
||||
export SSL_CERT_FILE=/rom/ca-bundle.crt
|
||||
fi
|
||||
$cmd &
|
||||
echo $! > "$pid_file"
|
||||
chmod 600 "$pid_file"
|
||||
if ! is_running; then
|
||||
logger -c "Failed to start $name"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if is_running; then
|
||||
logger -c "Stopping $name..."
|
||||
kill "$(get_pid)"
|
||||
for _ in 1 2 3 4 5; do
|
||||
if ! is_running; then
|
||||
logger -c "stopped"
|
||||
if [ -f "$pid_file" ]; then
|
||||
rm "$pid_file"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
printf "."
|
||||
sleep 2
|
||||
done
|
||||
logger -c "failed to stop $name"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
$0 start
|
||||
;;
|
||||
status)
|
||||
if is_running; then
|
||||
echo "running"
|
||||
else
|
||||
echo "stopped"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
service_event)
|
||||
event=$2
|
||||
svc=$3
|
||||
dnsmasq_pid_file=$(sed -n '/pid-file=/s///p' /etc/dnsmasq.conf)
|
||||
|
||||
if [ "$event" = "restart" ] && [ "$svc" = "diskmon" ]; then
|
||||
kill "$(cat "$dnsmasq_pid_file")" >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
|
||||
const merlinAddLineToScript = `#!/bin/sh
|
||||
|
||||
line=$1
|
||||
file=$2
|
||||
|
||||
. /usr/sbin/helper.sh
|
||||
|
||||
pc_append "$line" "$file"
|
||||
`
|
||||
|
||||
const merlinRemoveLineFromScript = `#!/bin/sh
|
||||
|
||||
line=$1
|
||||
file=$2
|
||||
|
||||
. /usr/sbin/helper.sh
|
||||
|
||||
pc_delete "$line" "$file"
|
||||
`
|
||||
@@ -1,289 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
)
|
||||
|
||||
const tomatoNvramScriptWanupKey = "script_wanup"
|
||||
|
||||
type tomatoSvc struct {
|
||||
i service.Interface
|
||||
platform string
|
||||
*service.Config
|
||||
}
|
||||
|
||||
func newTomatoService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||||
s := &tomatoSvc{
|
||||
i: i,
|
||||
platform: platform,
|
||||
Config: c,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Platform() string {
|
||||
return s.platform
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) configPath() string {
|
||||
bin := s.Config.Executable
|
||||
if bin == "" {
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
bin = path
|
||||
}
|
||||
return bin + ".startup"
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) template() *template.Template {
|
||||
return template.Must(template.New("").Parse(tomatoSvcScript))
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Install() error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(exePath, "/jffs/") {
|
||||
return errors.New("could not install service outside /jffs")
|
||||
}
|
||||
if _, err := nvram.Run("set", "jffs2_on=1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := nvram.Run("commit"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confPath := s.configPath()
|
||||
if _, err := os.Stat(confPath); err == nil {
|
||||
return fmt.Errorf("already installed: %s", confPath)
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*service.Config
|
||||
Path string
|
||||
}{
|
||||
s.Config,
|
||||
exePath,
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Create: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := s.template().Execute(f, to); err != nil {
|
||||
return fmt.Errorf("s.template.Execute: %w", err)
|
||||
}
|
||||
|
||||
if err = os.Chmod(confPath, 0755); err != nil {
|
||||
return fmt.Errorf("os.Chmod: startup script: %w", err)
|
||||
}
|
||||
|
||||
nvramKvMap := map[string]string{
|
||||
tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method.
|
||||
}
|
||||
old, err := nvram.Run("get", tomatoNvramScriptWanupKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nvram: %w", err)
|
||||
}
|
||||
nvramKvMap[tomatoNvramScriptWanupKey] = strings.Join([]string{old, s.configPath() + " start"}, "\n")
|
||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldInstallKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Uninstall() error {
|
||||
if err := os.Remove(s.configPath()); err != nil {
|
||||
return fmt.Errorf("os.Remove: %w", err)
|
||||
}
|
||||
nvramKvMap := map[string]string{
|
||||
tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method.
|
||||
}
|
||||
// Restore old configs.
|
||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldInstallKey); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||||
if service.Interactive() {
|
||||
return service.ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||||
return newSysLogger(s.Name, errs)
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Run() (err error) {
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if interactice, _ := isInteractive(); !interactice {
|
||||
signal.Ignore(syscall.SIGHUP)
|
||||
}
|
||||
|
||||
var sigChan = make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||
<-sigChan
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Status() (service.Status, error) {
|
||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||||
return service.StatusUnknown, service.ErrNotInstalled
|
||||
}
|
||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||||
if err != nil {
|
||||
return service.StatusUnknown, err
|
||||
}
|
||||
switch string(bytes.TrimSpace(out)) {
|
||||
case "running":
|
||||
return service.StatusRunning, nil
|
||||
default:
|
||||
return service.StatusStopped, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Start() error {
|
||||
return exec.Command(s.configPath(), "start").Run()
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Stop() error {
|
||||
return exec.Command(s.configPath(), "stop").Run()
|
||||
}
|
||||
|
||||
func (s *tomatoSvc) Restart() error {
|
||||
return exec.Command(s.configPath(), "restart").Run()
|
||||
}
|
||||
|
||||
// https://wiki.freshtomato.org/doku.php/freshtomato_zerotier?s[]=%2Aservice%2A
|
||||
const tomatoSvcScript = `#!/bin/sh
|
||||
|
||||
|
||||
NAME="{{.Name}}"
|
||||
CMD="{{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||
LOG_FILE="/var/log/${NAME}.log"
|
||||
PID_FILE="/tmp/$NAME.pid"
|
||||
|
||||
|
||||
alias elog="logger -t $NAME -s"
|
||||
|
||||
|
||||
COND=$1
|
||||
[ $# -eq 0 ] && COND="start"
|
||||
|
||||
get_pid() {
|
||||
cat "$PID_FILE"
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[ -f "$PID_FILE" ] && ps | grep -q "^ *$(get_pid) "
|
||||
}
|
||||
|
||||
start() {
|
||||
if is_running; then
|
||||
elog "$NAME is already running."
|
||||
exit 1
|
||||
fi
|
||||
elog "Starting $NAME Services: "
|
||||
$CMD &
|
||||
echo $! > "$PID_FILE"
|
||||
chmod 600 "$PID_FILE"
|
||||
if is_running; then
|
||||
elog "succeeded."
|
||||
else
|
||||
elog "failed."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
stop() {
|
||||
if ! is_running; then
|
||||
elog "$NAME is not running."
|
||||
exit 0
|
||||
fi
|
||||
elog "Shutting down $NAME Services: "
|
||||
kill -SIGTERM "$(get_pid)"
|
||||
for _ in 1 2 3 4 5; do
|
||||
if ! is_running; then
|
||||
if [ -f "$pid_file" ]; then
|
||||
rm "$pid_file"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
printf "."
|
||||
sleep 2
|
||||
done
|
||||
if ! is_running; then
|
||||
elog "succeeded."
|
||||
else
|
||||
elog "failed."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
do_restart() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
|
||||
do_status() {
|
||||
if ! is_running; then
|
||||
echo "stopped"
|
||||
else
|
||||
echo "running"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
case "$COND" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
do_restart
|
||||
;;
|
||||
status)
|
||||
do_status
|
||||
;;
|
||||
*)
|
||||
elog "Usage: $0 (start|stop|restart|status)"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
@@ -1,340 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
)
|
||||
|
||||
// This is a copy of https://github.com/kardianos/service/blob/v1.2.1/service_sysv_linux.go,
|
||||
// with modification for supporting ubios v1 init system.
|
||||
|
||||
type ubiosSvc struct {
|
||||
i service.Interface
|
||||
platform string
|
||||
*service.Config
|
||||
}
|
||||
|
||||
func newUbiosService(i service.Interface, platform string, c *service.Config) (service.Service, error) {
|
||||
s := &ubiosSvc{
|
||||
i: i,
|
||||
platform: platform,
|
||||
Config: c,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) String() string {
|
||||
if len(s.DisplayName) > 0 {
|
||||
return s.DisplayName
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Platform() string {
|
||||
return s.platform
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) configPath() string {
|
||||
return "/etc/init.d/" + s.Config.Name
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) execPath() (string, error) {
|
||||
if len(s.Executable) != 0 {
|
||||
return filepath.Abs(s.Executable)
|
||||
}
|
||||
return os.Executable()
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) template() *template.Template {
|
||||
return template.Must(template.New("").Funcs(tf).Parse(ubiosSvcScript))
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Install() error {
|
||||
confPath := s.configPath()
|
||||
if _, err := os.Stat(confPath); err == nil {
|
||||
return fmt.Errorf("init already exists: %s", confPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(confPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create config path: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
path, err := s.execPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get exec path: %w", err)
|
||||
}
|
||||
|
||||
var to = &struct {
|
||||
*service.Config
|
||||
Path string
|
||||
DnsMasqConfPath string
|
||||
}{
|
||||
s.Config,
|
||||
path,
|
||||
filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName),
|
||||
}
|
||||
|
||||
if err := s.template().Execute(f, to); err != nil {
|
||||
return fmt.Errorf("failed to create init script: %w", err)
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("failed to save init script: %w", err)
|
||||
}
|
||||
|
||||
if err = os.Chmod(confPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to set init script executable: %w", err)
|
||||
}
|
||||
|
||||
// Enable on boot
|
||||
script, err := os.CreateTemp("", "ctrld_boot.service")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create boot service tmp file: %w", err)
|
||||
}
|
||||
defer script.Close()
|
||||
|
||||
svcConfig := *to.Config
|
||||
svcConfig.Arguments = os.Args[1:]
|
||||
to.Config = &svcConfig
|
||||
if err := template.Must(template.New("").Funcs(tf).Parse(ubiosBootSystemdService)).Execute(script, &to); err != nil {
|
||||
return fmt.Errorf("failed to create boot service file: %w", err)
|
||||
}
|
||||
if err := script.Close(); err != nil {
|
||||
return fmt.Errorf("failed to save boot service file: %w", err)
|
||||
}
|
||||
|
||||
// Copy the boot script to container and start.
|
||||
cmd := exec.Command("podman", "cp", "--pause=false", script.Name(), "unifi-os:/lib/systemd/system/ctrld-boot.service")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to copy boot script, out: %s, err: %v", string(out), err)
|
||||
}
|
||||
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "enable", "--now", "ctrld-boot.service")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to start ctrld boot script, out: %s, err: %v", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Uninstall() error {
|
||||
if err := os.Remove(s.configPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove ctrld-boot service inside unifi-os container.
|
||||
cmd := exec.Command("podman", "exec", "unifi-os", "systemctl", "disable", "ctrld-boot.service")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to disable ctrld-boot service, out: %s, err: %v", string(out), err)
|
||||
}
|
||||
cmd = exec.Command("podman", "exec", "unifi-os", "rm", "/lib/systemd/system/ctrld-boot.service")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to remove ctrld-boot service file, out: %s, err: %v", string(out), err)
|
||||
}
|
||||
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "daemon-reload")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to reload systemd service, out: %s, err: %v", string(out), err)
|
||||
}
|
||||
cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "reset-failed")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to reset-failed systemd service, out: %s, err: %v", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Logger(errs chan<- error) (service.Logger, error) {
|
||||
if service.Interactive() {
|
||||
return service.ConsoleLogger, nil
|
||||
}
|
||||
return s.SystemLogger(errs)
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) SystemLogger(errs chan<- error) (service.Logger, error) {
|
||||
return newSysLogger(s.Name, errs)
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Run() (err error) {
|
||||
err = s.i.Start(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if interactice, _ := isInteractive(); !interactice {
|
||||
signal.Ignore(syscall.SIGHUP)
|
||||
}
|
||||
|
||||
var sigChan = make(chan os.Signal, 3)
|
||||
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
||||
<-sigChan
|
||||
|
||||
return s.i.Stop(s)
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Status() (service.Status, error) {
|
||||
if _, err := os.Stat(s.configPath()); os.IsNotExist(err) {
|
||||
return service.StatusUnknown, service.ErrNotInstalled
|
||||
}
|
||||
out, err := exec.Command(s.configPath(), "status").CombinedOutput()
|
||||
if err != nil {
|
||||
return service.StatusUnknown, err
|
||||
}
|
||||
switch string(bytes.TrimSpace(out)) {
|
||||
case "Running":
|
||||
return service.StatusRunning, nil
|
||||
default:
|
||||
return service.StatusStopped, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Start() error {
|
||||
return exec.Command(s.configPath(), "start").Run()
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Stop() error {
|
||||
return exec.Command(s.configPath(), "stop").Run()
|
||||
}
|
||||
|
||||
func (s *ubiosSvc) Restart() error {
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
const ubiosBootSystemdService = `[Unit]
|
||||
Description=Run ctrld On Startup UDM
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
Wants=unifi-mongodb
|
||||
After=unifi-mongodb
|
||||
StartLimitIntervalSec=500
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
ExecStart=/sbin/ssh-proxy '[ -f "{{.DnsMasqConfPath}}" ] || {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}'
|
||||
RemainAfterExit=true
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
const ubiosSvcScript = `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: - 99 01
|
||||
# description: {{.Description}}
|
||||
# processname: {{.Path}}
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: {{.Path}}
|
||||
# Required-Start:
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: {{.DisplayName}}
|
||||
# Description: {{.Description}}
|
||||
### END INIT INFO
|
||||
|
||||
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
|
||||
|
||||
name=$(basename $(readlink -f $0))
|
||||
pid_file="/var/run/$name.pid"
|
||||
stdout_log="/var/log/$name.log"
|
||||
stderr_log="/var/log/$name.err"
|
||||
|
||||
[ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name
|
||||
|
||||
get_pid() {
|
||||
cat "$pid_file"
|
||||
}
|
||||
|
||||
is_running() {
|
||||
[ -f "$pid_file" ] && cat /proc/$(get_pid)/stat > /dev/null 2>&1
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
if is_running; then
|
||||
echo "Already started"
|
||||
else
|
||||
echo "Starting $name"
|
||||
{{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}
|
||||
$cmd >> "$stdout_log" 2>> "$stderr_log" &
|
||||
echo $! > "$pid_file"
|
||||
if ! is_running; then
|
||||
echo "Unable to start, see $stdout_log and $stderr_log"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if is_running; then
|
||||
echo -n "Stopping $name.."
|
||||
kill $(get_pid)
|
||||
for i in $(seq 1 10)
|
||||
do
|
||||
if ! is_running; then
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
echo
|
||||
if is_running; then
|
||||
echo "Not stopped; may still be shutting down or shutdown may have failed"
|
||||
exit 1
|
||||
else
|
||||
echo "Stopped"
|
||||
if [ -f "$pid_file" ]; then
|
||||
rm "$pid_file"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Not running"
|
||||
fi
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
if is_running; then
|
||||
echo "Unable to stop, will not attempt to start"
|
||||
exit 1
|
||||
fi
|
||||
$0 start
|
||||
;;
|
||||
status)
|
||||
if is_running; then
|
||||
echo "Running"
|
||||
else
|
||||
echo "Stopped"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
|
||||
var tf = map[string]interface{}{
|
||||
"cmd": func(s string) string {
|
||||
return `"` + strings.Replace(s, `"`, `\"`, -1) + `"`
|
||||
},
|
||||
"cmdEscape": func(s string) string {
|
||||
return strings.Replace(s, " ", `\x20`, -1)
|
||||
},
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package synology
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
"tailscale.com/logtail/backoff"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "synology"
|
||||
|
||||
synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf"
|
||||
synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info"
|
||||
)
|
||||
|
||||
type Synology struct {
|
||||
cfg *ctrld.Config
|
||||
useUpstart bool
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on Ubios routers.
|
||||
func New(cfg *ctrld.Config) *Synology {
|
||||
return &Synology{
|
||||
cfg: cfg,
|
||||
useUpstart: service.Platform() == "linux-upstart",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Synology) ConfigureService(svc *service.Config) error {
|
||||
svc.Option["LogOutput"] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Synology) Install(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Synology) Uninstall(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Synology) PreRun() error {
|
||||
if s.useUpstart {
|
||||
if err := ntp.WaitUpstart(); err != nil {
|
||||
return err
|
||||
}
|
||||
return waitDhcpServer()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Synology) Setup() error {
|
||||
if s.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, s.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Synology) Cleanup() error {
|
||||
if s.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Remove the custom config files.
|
||||
for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} {
|
||||
if err := os.Remove(f); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
if out, err := exec.Command("/etc/rc.network", "nat-restart-dhcp").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("synologyRestartDNSMasq: %s - %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitDhcpServer() error {
|
||||
// Wait until `initctl status dhcpserver` returns running state.
|
||||
b := backoff.NewBackoff("waitDhcpServer", func(format string, args ...any) {}, 10*time.Second)
|
||||
for {
|
||||
out, err := exec.Command("initctl", "status", "dhcpserver").CombinedOutput()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Unknown job") {
|
||||
// dhcpserver service does not exist.
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("exec.Command: %w", err)
|
||||
}
|
||||
if bytes.Contains(out, []byte("start/running")) {
|
||||
return nil
|
||||
}
|
||||
b.BackOff(context.Background(), errors.New("ntp not ready"))
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
//go:build linux || darwin || freebsd
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/syslog"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
|
||||
w, err := syslog.New(syslog.LOG_INFO, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sysLogger{w, errs}, nil
|
||||
}
|
||||
|
||||
type sysLogger struct {
|
||||
*syslog.Writer
|
||||
errs chan<- error
|
||||
}
|
||||
|
||||
func (s sysLogger) send(err error) error {
|
||||
if err != nil && s.errs != nil {
|
||||
s.errs <- err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s sysLogger) Error(v ...interface{}) error {
|
||||
return s.send(s.Writer.Err(fmt.Sprint(v...)))
|
||||
}
|
||||
func (s sysLogger) Warning(v ...interface{}) error {
|
||||
return s.send(s.Writer.Warning(fmt.Sprint(v...)))
|
||||
}
|
||||
func (s sysLogger) Info(v ...interface{}) error {
|
||||
return s.send(s.Writer.Info(fmt.Sprint(v...)))
|
||||
}
|
||||
func (s sysLogger) Errorf(format string, a ...interface{}) error {
|
||||
return s.send(s.Writer.Err(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
func (s sysLogger) Warningf(format string, a ...interface{}) error {
|
||||
return s.send(s.Writer.Warning(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
func (s sysLogger) Infof(format string, a ...interface{}) error {
|
||||
return s.send(s.Writer.Info(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package router
|
||||
|
||||
import "github.com/kardianos/service"
|
||||
|
||||
func newSysLogger(name string, errs chan<- error) (service.Logger, error) {
|
||||
return service.ConsoleLogger, nil
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package tomato
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/ntp"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/nvram"
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
const (
|
||||
Name = "freshtomato"
|
||||
|
||||
tomatoDnsCryptProxySvcName = "dnscrypt-proxy"
|
||||
tomatoStubbySvcName = "stubby"
|
||||
tomatoDNSMasqSvcName = "dnsmasq"
|
||||
)
|
||||
|
||||
var nvramKvMap = map[string]string{
|
||||
"dnsmasq_custom": "", // Configuration of dnsmasq set by ctrld, filled by setupTomato.
|
||||
"dnscrypt_proxy": "0", // Disable DNSCrypt.
|
||||
"dnssec_enable": "0", // Disable DNSSEC.
|
||||
"stubby_proxy": "0", // Disable Stubby
|
||||
}
|
||||
|
||||
type FreshTomato struct {
|
||||
cfg *ctrld.Config
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on Ubios routers.
|
||||
func New(cfg *ctrld.Config) *FreshTomato {
|
||||
return &FreshTomato{cfg: cfg}
|
||||
}
|
||||
|
||||
func (f *FreshTomato) ConfigureService(config *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FreshTomato) Install(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FreshTomato) Uninstall(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FreshTomato) PreRun() error {
|
||||
_ = f.Cleanup()
|
||||
return ntp.WaitNvram()
|
||||
}
|
||||
|
||||
func (f *FreshTomato) Setup() error {
|
||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Already setup.
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nvramKvMap["dnsmasq_custom"] = data
|
||||
if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart dnscrypt-proxy service.
|
||||
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart stubby service.
|
||||
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FreshTomato) Cleanup() error {
|
||||
if f.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" {
|
||||
return nil // was restored, nothing to do.
|
||||
}
|
||||
|
||||
nvramKvMap["dnsmasq_custom"] = ""
|
||||
// Restore old configs.
|
||||
if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart dnscrypt-proxy service.
|
||||
if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart stubby service.
|
||||
if err := tomatoRestartService(tomatoStubbySvcName); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tomatoRestartService(name string) error {
|
||||
return tomatoRestartServiceWithKill(name, false)
|
||||
}
|
||||
|
||||
func tomatoRestartServiceWithKill(name string, killBeforeRestart bool) error {
|
||||
if killBeforeRestart {
|
||||
_, _ = exec.Command("killall", name).CombinedOutput()
|
||||
}
|
||||
if out, err := exec.Command("service", name, "restart").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("service restart %s: %s, %w", name, string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
return tomatoRestartService(tomatoDNSMasqSvcName)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package ubios
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/edgeos"
|
||||
)
|
||||
|
||||
const Name = "ubios"
|
||||
|
||||
type Ubios struct {
|
||||
cfg *ctrld.Config
|
||||
dnsmasqConfPath string
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on Ubios routers.
|
||||
func New(cfg *ctrld.Config) *Ubios {
|
||||
return &Ubios{
|
||||
cfg: cfg,
|
||||
dnsmasqConfPath: filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Ubios) ConfigureService(config *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ubios) Install(config *service.Config) error {
|
||||
// See comment in (*edgeos.EdgeOS).Install method.
|
||||
if edgeos.ContentFilteringEnabled() {
|
||||
return edgeos.ErrContentFilteringEnabled
|
||||
}
|
||||
// See comment in (*edgeos.EdgeOS).Install method.
|
||||
if edgeos.DnsShieldEnabled() {
|
||||
return edgeos.ErrDnsShieldEnabled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ubios) Uninstall(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ubios) PreRun() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ubios) Setup() error {
|
||||
if u.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, u.cfg, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(u.dnsmasqConfPath, []byte(data), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Ubios) Cleanup() error {
|
||||
if u.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
}
|
||||
// Remove the custom dnsmasq config
|
||||
if err := os.Remove(u.dnsmasqConfPath); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
buf, err := os.ReadFile(dnsmasq.UbiosPidFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proc, err := os.FindProcess(int(pid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return proc.Kill()
|
||||
}
|
||||
Reference in New Issue
Block a user