all: add support for freshtomato

This commit is contained in:
Cuong Manh Le
2023-05-25 09:52:44 +07:00
committed by Cuong Manh Le
parent fc502b920b
commit ee53db1e35
14 changed files with 532 additions and 76 deletions

View File

@@ -42,12 +42,11 @@ func initRouterCLI() {
if platform == "auto" {
platform = router.Name()
}
switch platform {
case router.DDWrt, router.Merlin, router.OpenWrt, router.Ubios, router.Synology:
default:
if !router.IsSupported(platform) {
unsupportedPlatformHelp(cmd)
os.Exit(1)
}
exe, err := os.Executable()
if err != nil {
mainLog.Fatal().Msgf("could not find executable path: %v", err)

View File

@@ -175,7 +175,7 @@ func (p *prog) setDNS() {
switch router.Name() {
case router.DDWrt, router.OpenWrt, router.Ubios:
// On router, ctrld run as a DNS forwarder, it does not have to change system DNS.
// Except for Merlin, which has WAN DNS setup on boot for NTP.
// Except for Merlin/Tomato, which has WAN DNS setup on boot for NTP.
return
}
if cfg.Listener == nil || cfg.Listener["0"] == nil {

View File

@@ -22,6 +22,7 @@ var clientInfoFiles = []string{
"/mnt/data/udapi-config/dnsmasq.lease", // UDM Pro
"/data/udapi-config/dnsmasq.lease", // UDR
"/etc/dhcpd/dhcpd-leases.log", // Synology
"/tmp/var/lib/misc/dnsmasq.leases", // Tomato
}
func (r *router) watchClientInfoTable() {

View File

@@ -7,9 +7,10 @@ import (
)
const (
nvramCtrldKeyPrefix = "ctrld_"
nvramCtrldSetupKey = "ctrld_setup"
nvramRCStartupKey = "rc_startup"
nvramCtrldKeyPrefix = "ctrld_"
nvramCtrldSetupKey = "ctrld_setup"
nvramCtrldInstallKey = "ctrld_install"
nvramRCStartupKey = "rc_startup"
)
//lint:ignore ST1005 This error is for human.
@@ -29,14 +30,14 @@ func setupDDWrt() error {
return err
}
nvramKvMap := nvramKV()
nvramKvMap := nvramSetupKV()
nvramKvMap["dnsmasq_options"] = data
if err := nvramSetup(nvramKvMap); err != nil {
if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnsmasq service.
if err := ddwrtRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil
@@ -44,11 +45,11 @@ func setupDDWrt() error {
func cleanupDDWrt() error {
// Restore old configs.
if err := nvramRestore(nvramKV()); err != nil {
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
// Restart dnsmasq service.
if err := ddwrtRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil

View File

@@ -49,7 +49,7 @@ func dnsMasqConf() (string, error) {
var sb strings.Builder
var tmplText string
switch Name() {
case DDWrt, OpenWrt, Ubios, Synology:
case DDWrt, OpenWrt, Ubios, Synology, Tomato:
tmplText = dnsMasqConfigContentTmpl
case Merlin:
tmplText = merlinDNSMasqPostConfTmpl
@@ -65,3 +65,21 @@ func dnsMasqConf() (string, error) {
}
return sb.String(), nil
}
func restartDNSMasq() error {
switch Name() {
case DDWrt:
return ddwrtRestartDNSMasq()
case Merlin:
return merlinRestartDNSMasq()
case OpenWrt:
return openwrtRestartDNSMasq()
case Ubios:
return ubiosRestartDNSMasq()
case Synology:
return synologyRestartDNSMasq()
case Tomato:
return tomatoRestartService(tomatoDNSMasqSvcName)
}
panic("not supported platform")
}

View File

@@ -2,11 +2,16 @@ package router
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
"unicode"
"tailscale.com/logtail/backoff"
)
func setupMerlin() error {
@@ -35,11 +40,11 @@ func setupMerlin() error {
return err
}
// Restart dnsmasq service.
if err := merlinRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
if err := nvramSetup(nvramKV()); err != nil {
if err := nvramSetKV(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
@@ -48,7 +53,7 @@ func setupMerlin() error {
func cleanupMerlin() error {
// Restore old configs.
if err := nvramRestore(nvramKV()); err != nil {
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil {
return err
}
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
@@ -60,7 +65,7 @@ func cleanupMerlin() error {
return err
}
// Restart dnsmasq service.
if err := merlinRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil
@@ -87,3 +92,43 @@ func merlinParsePostConf(buf []byte) []byte {
}
return buf
}
func merlinPreStart() (err error) {
pidFile := "/tmp/ctrld.pid"
// Remove pid file and trigger dnsmasq restart, so NTP can resolve
// server name and perform time synchronization.
pid, err := os.ReadFile(pidFile)
if err != nil {
return fmt.Errorf("PreStart: os.Readfile: %w", err)
}
if err := os.Remove(pidFile); err != nil {
return fmt.Errorf("PreStart: os.Remove: %w", err)
}
defer func() {
if werr := os.WriteFile(pidFile, pid, 0600); werr != nil {
err = errors.Join(err, werr)
return
}
if rerr := restartDNSMasq(); rerr != nil {
err = errors.Join(err, rerr)
return
}
}()
if err := restartDNSMasq(); err != nil {
return fmt.Errorf("PreStart: restartDNSMasqFn: %w", err)
}
// Wait until `ntp_ready=1` set.
b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
for {
out, err := nvram("get", "ntp_ready")
if err != nil {
return fmt.Errorf("PreStart: nvram: %w", err)
}
if out == "1" {
return nil
}
b.BackOff(context.Background(), errors.New("ntp not ready"))
}
}

View File

@@ -26,7 +26,7 @@ NOTE:
+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
*/
func nvramKV() map[string]string {
func nvramSetupKV() map[string]string {
switch Name() {
case DDWrt:
return map[string]string{
@@ -39,11 +39,28 @@ func nvramKV() map[string]string {
return map[string]string{
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
}
case Tomato:
return 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
}
}
return nil
}
func nvramSetup(m map[string]string) error {
func nvramInstallKV() map[string]string {
switch Name() {
case Tomato:
return map[string]string{
tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method.
}
}
return nil
}
func nvramSetKV(m map[string]string, setupKey string) error {
// Backup current value, store ctrld's configs.
for key, value := range m {
old, err := nvram("get", key)
@@ -58,7 +75,7 @@ func nvramSetup(m map[string]string) error {
}
}
if out, err := nvram("set", nvramCtrldSetupKey+"=1"); err != nil {
if out, err := nvram("set", setupKey+"=1"); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
// Commit.
@@ -68,7 +85,7 @@ func nvramSetup(m map[string]string) error {
return nil
}
func nvramRestore(m map[string]string) error {
func nvramRestore(m map[string]string, setupKey string) error {
// Restore old configs.
for key := range m {
ctrldKey := nvramCtrldKeyPrefix + key
@@ -82,7 +99,7 @@ func nvramRestore(m map[string]string) error {
}
}
if out, err := nvram("unset", "ctrld_setup"); err != nil {
if out, err := nvram("unset", setupKey); err != nil {
return fmt.Errorf("%s: %w", out, err)
}
// Commit.

View File

@@ -40,7 +40,7 @@ func setupOpenWrt() error {
return err
}
// Restart dnsmasq service.
if err := openwrtRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil
@@ -52,7 +52,7 @@ func cleanupOpenWrt() error {
return err
}
// Restart dnsmasq service.
if err := openwrtRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil

View File

@@ -2,18 +2,14 @@ package router
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"sync"
"sync/atomic"
"time"
"github.com/fsnotify/fsnotify"
"github.com/kardianos/service"
"tailscale.com/logtail/backoff"
"github.com/Control-D-Inc/ctrld"
)
@@ -24,6 +20,7 @@ const (
Merlin = "merlin"
Ubios = "ubios"
Synology = "synology"
Tomato = "tomato"
)
// ErrNotSupported reports the current router is not supported error.
@@ -38,9 +35,18 @@ type router struct {
watcher *fsnotify.Watcher
}
// IsSupported reports whether the given platform is supported by ctrld.
func IsSupported(platform string) bool {
switch platform {
case DDWrt, Merlin, OpenWrt, Ubios, Synology, Tomato:
return true
}
return false
}
// SupportedPlatforms return all platforms that can be configured to run with ctrld.
func SupportedPlatforms() []string {
return []string{DDWrt, Merlin, OpenWrt, Ubios, Synology}
return []string{DDWrt, Merlin, OpenWrt, Ubios, Synology, Tomato}
}
var configureFunc = map[string]func() error{
@@ -49,13 +55,14 @@ var configureFunc = map[string]func() error{
OpenWrt: setupOpenWrt,
Ubios: setupUbiOS,
Synology: setupSynology,
Tomato: setupTomato,
}
// Configure configures things for running ctrld on the router.
func Configure(c *ctrld.Config) error {
name := Name()
switch name {
case DDWrt, Merlin, OpenWrt, Ubios, Synology:
case DDWrt, Merlin, OpenWrt, Ubios, Synology, Tomato:
if c.HasUpstreamSendClientInfo() {
r := routerPlatform.Load()
r.sendClientInfo = true
@@ -90,55 +97,22 @@ func ConfigureService(sc *service.Config) error {
}
case OpenWrt:
sc.Option["SysvScript"] = openWrtScript
case Merlin, Ubios, Synology:
case Merlin, Ubios, Synology, Tomato:
}
return nil
}
// PreStart blocks until the router is ready for running ctrld.
func PreStart() (err error) {
if Name() != Merlin {
// On some routers, NTP may out of sync, so waiting for it to be ready.
switch Name() {
case Merlin:
return merlinPreStart()
case Tomato:
return tomatoPreStart()
default:
return nil
}
pidFile := "/tmp/ctrld.pid"
// On Merlin, NTP may out of sync, so waiting for it to be ready.
//
// Remove pid file and trigger dnsmasq restart, so NTP can resolve
// server name and perform time synchronization.
pid, err := os.ReadFile(pidFile)
if err != nil {
return fmt.Errorf("PreStart: os.Readfile: %w", err)
}
if err := os.Remove(pidFile); err != nil {
return fmt.Errorf("PreStart: os.Remove: %w", err)
}
defer func() {
if werr := os.WriteFile(pidFile, pid, 0600); werr != nil {
err = errors.Join(err, werr)
return
}
if rerr := merlinRestartDNSMasq(); rerr != nil {
err = errors.Join(err, rerr)
return
}
}()
if err := merlinRestartDNSMasq(); err != nil {
return fmt.Errorf("PreStart: merlinRestartDNSMasq: %w", err)
}
// Wait until `ntp_read=1` set.
b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
for {
out, err := nvram("get", "ntp_ready")
if err != nil {
return fmt.Errorf("PreStart: nvram: %w", err)
}
if out == "1" {
return nil
}
b.BackOff(context.Background(), errors.New("ntp not ready"))
}
}
// PostInstall performs task after installing ctrld on router.
@@ -155,6 +129,8 @@ func PostInstall() error {
return postInstallUbiOS()
case Synology:
return postInstallSynology()
case Tomato:
return postInstallTomato()
}
return nil
}
@@ -173,6 +149,8 @@ func Cleanup() error {
return cleanupUbiOS()
case Synology:
return cleanupSynology()
case Tomato:
return cleanupTomato()
}
return nil
}
@@ -181,7 +159,7 @@ func Cleanup() error {
func ListenAddress() string {
name := Name()
switch name {
case DDWrt, Merlin, OpenWrt, Ubios, Synology:
case DDWrt, Merlin, OpenWrt, Ubios, Synology, Tomato:
return "127.0.0.1:5354"
}
return ""
@@ -210,6 +188,8 @@ func distroName() string {
return Ubios
case bytes.HasPrefix(unameU(), []byte("synology")):
return Synology
case bytes.HasPrefix(unameO(), []byte("Tomato")):
return Tomato
}
return ""
}

View File

@@ -48,6 +48,15 @@ func init() {
},
new: newUbiosService,
},
&linuxSystemService{
name: "tomato",
detect: func() bool { return Name() == Tomato },
interactive: func() bool {
is, _ := isInteractive()
return is
},
new: newTomatoService,
},
}
systems = append(systems, service.AvailableSystems()...)
service.ChooseSystem(systems...)

View File

@@ -0,0 +1,278 @@
package router
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"text/template"
"github.com/kardianos/service"
)
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 {
path, err := os.Executable()
if err != nil {
return ""
}
return path + ".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("set", "jffs2_on=1"); err != nil {
return err
}
if _, err := nvram("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 := nvramInstallKV()
old, err := nvram("get", tomatoNvramScriptWanupKey)
if err != nil {
return fmt.Errorf("nvram: %w", err)
}
nvramKvMap[tomatoNvramScriptWanupKey] = strings.Join([]string{old, s.configPath() + " start"}, "\n")
if err := nvramSetKV(nvramKvMap, nvramCtrldInstallKey); 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)
}
// Restore old configs.
if err := nvramRestore(nvramInstallKV(), nvramCtrldInstallKey); 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 1
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
exit 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
`

View File

@@ -22,7 +22,7 @@ func setupSynology() error {
if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil {
return err
}
if err := synologyRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil
@@ -37,7 +37,7 @@ func cleanupSynology() error {
}
// Restart dnsmasq service.
if err := synologyRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil

108
internal/router/tomato.go Normal file
View File

@@ -0,0 +1,108 @@
package router
import (
"context"
"errors"
"fmt"
"os/exec"
"time"
"tailscale.com/logtail/backoff"
)
const (
tomatoDnsCryptProxySvcName = "dnscrypt-proxy"
tomatoStubbySvcName = "stubby"
tomatoDNSMasqSvcName = "dnsmasq"
)
func setupTomato() error {
// Already setup.
if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" {
return nil
}
data, err := dnsMasqConf()
if err != nil {
return err
}
nvramKvMap := nvramSetupKV()
nvramKvMap["dnsmasq_custom"] = data
if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); 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 postInstallTomato() error {
return nil
}
func cleanupTomato() error {
// Restore old configs.
if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); 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 tomatoPreStart() (err error) {
// cleanup to trigger dnsmasq restart, so NTP can resolve
// server name and perform time synchronization.
if err = cleanupTomato(); err != nil {
return err
}
// Wait until `ntp_ready=1` set.
b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second)
for {
out, err := nvram("get", "ntp_ready")
if err != nil {
return fmt.Errorf("PreStart: nvram: %w", err)
}
if out == "1" {
return nil
}
b.BackOff(context.Background(), errors.New("ntp not ready"))
}
}
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
}

View File

@@ -20,7 +20,7 @@ func setupUbiOS() error {
return err
}
// Restart dnsmasq service.
if err := ubiosRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil
@@ -32,7 +32,7 @@ func cleanupUbiOS() error {
return err
}
// Restart dnsmasq service.
if err := ubiosRestartDNSMasq(); err != nil {
if err := restartDNSMasq(); err != nil {
return err
}
return nil