mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-15 00:50:25 +02:00
all: implement router setup for ddwrt
This commit is contained in:
committed by
Cuong Manh Le
parent
c94be0df35
commit
8a2cdbfaa3
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed cacert.pem
|
||||
caRoots []byte
|
||||
caCertPoolOnce sync.Once
|
||||
caCertPool *x509.CertPool
|
||||
)
|
||||
|
||||
func CACertPool() *x509.CertPool {
|
||||
caCertPoolOnce.Do(func() {
|
||||
caCertPool = x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caRoots)
|
||||
})
|
||||
return caCertPool
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCACertPool(t *testing.T) {
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: CACertPool(),
|
||||
},
|
||||
},
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
resp, err := c.Get("https://freedns.controld.com/p1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if !resp.TLS.HandshakeComplete {
|
||||
t.Error("TLS handshake is not complete")
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package controld
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -13,7 +14,9 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -114,6 +117,10 @@ func FetchResolverConfig(uid string) (*ResolverConfig, error) {
|
||||
}
|
||||
return ctrldnet.Dialer.DialContext(ctx, proto, addr)
|
||||
}
|
||||
|
||||
if router.Name() == router.DDWrt {
|
||||
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
||||
}
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: transport,
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
nvramCtrldKeyPrefix = "ctrld_"
|
||||
nvramCtrldSetupKey = "ctrld_setup"
|
||||
nvramRCStartupKey = "rc_startup"
|
||||
)
|
||||
|
||||
var ddwrtJffs2NotEnabledErr = 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 nvramKeys = map[string]string{
|
||||
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
||||
"dnsmasq_options": dnsMasqConfigContent, // Configuration of dnsmasq set by ctrld.
|
||||
"dns_crypt": "0", // Disable DNSCrypt.
|
||||
}
|
||||
|
||||
func setupDDWrt() error {
|
||||
// Already setup.
|
||||
if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Backup current value, store ctrld's configs.
|
||||
for key, value := range nvramKeys {
|
||||
old, err := nvram("get", key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", old, err)
|
||||
}
|
||||
if out, err := nvram("set", nvramCtrldKeyPrefix+key+"="+old); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
if out, err := nvram("set", key+"="+value); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := nvram("set", nvramCtrldSetupKey+"=1"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
// Commit.
|
||||
if out, err := nvram("commit"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := ddwrtRestartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupDDWrt() error {
|
||||
// Restore old configs.
|
||||
for key := range nvramKeys {
|
||||
ctrldKey := nvramCtrldKeyPrefix + key
|
||||
old, err := nvram("get", ctrldKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", old, err)
|
||||
}
|
||||
_, _ = nvram("unset", ctrldKey)
|
||||
if out, err := nvram("set", key+"="+old); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := nvram("unset", "ctrld_setup"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
// Commit.
|
||||
if out, err := nvram("commit"); err != nil {
|
||||
return fmt.Errorf("%s: %w", out, err)
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := ddwrtRestartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func postInstallDDWrt() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func nvram(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
|
||||
}
|
||||
|
||||
func ddwrtRestartDNSMasq() 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("get", "enable_jffs2")
|
||||
return out == "1"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package router
|
||||
|
||||
const dnsMasqConfigContent = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
no-resolv
|
||||
server=127.0.0.1#5353
|
||||
`
|
||||
@@ -12,9 +12,6 @@ import (
|
||||
var errUCIEntryNotFound = errors.New("uci: Entry not found")
|
||||
|
||||
const openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf"
|
||||
const openwrtDNSMasqConfigContent = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
port=0
|
||||
`
|
||||
|
||||
func setupOpenWrt() error {
|
||||
// Delete dnsmasq port if set.
|
||||
@@ -22,7 +19,7 @@ func setupOpenWrt() error {
|
||||
return err
|
||||
}
|
||||
// Disable dnsmasq as DNS server.
|
||||
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(openwrtDNSMasqConfigContent), 0600); err != nil {
|
||||
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Commit.
|
||||
@@ -30,7 +27,7 @@ func setupOpenWrt() error {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
if err := openwrtRestartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -42,7 +39,7 @@ func cleanupOpenWrt() error {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
if err := openwrtRestartDNSMasq(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -66,7 +63,7 @@ func uci(args ...string) (string, error) {
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
func openwrtRestartDNSMasq() error {
|
||||
if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%s: %w", string(out), err)
|
||||
}
|
||||
|
||||
@@ -38,9 +38,11 @@ func SupportedPlatforms() []string {
|
||||
func Configure(c *ctrld.Config) error {
|
||||
name := Name()
|
||||
switch name {
|
||||
case DDWrt:
|
||||
return setupDDWrt()
|
||||
case OpenWrt:
|
||||
return setupOpenWrt()
|
||||
case DDWrt, Merlin, Ubios:
|
||||
case Merlin, Ubios:
|
||||
default:
|
||||
return ErrNotSupported
|
||||
}
|
||||
@@ -50,22 +52,29 @@ func Configure(c *ctrld.Config) error {
|
||||
}
|
||||
|
||||
// ConfigureService performs necessary setup for running ctrld as a service on router.
|
||||
func ConfigureService(sc *service.Config) {
|
||||
func ConfigureService(sc *service.Config) error {
|
||||
name := Name()
|
||||
switch name {
|
||||
case DDWrt:
|
||||
if !ddwrtJff2Enabled() {
|
||||
return ddwrtJffs2NotEnabledErr
|
||||
}
|
||||
case OpenWrt:
|
||||
sc.Option["SysvScript"] = openWrtScript
|
||||
case DDWrt, Merlin, Ubios:
|
||||
case Merlin, Ubios:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostInstall performs task after installing ctrld on router.
|
||||
func PostInstall() error {
|
||||
name := Name()
|
||||
switch name {
|
||||
case DDWrt:
|
||||
return postInstallDDWrt()
|
||||
case OpenWrt:
|
||||
return postInstallOpenWrt()
|
||||
case DDWrt, Merlin, Ubios:
|
||||
case Merlin, Ubios:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -76,7 +85,9 @@ func Cleanup() error {
|
||||
switch name {
|
||||
case OpenWrt:
|
||||
return cleanupOpenWrt()
|
||||
case DDWrt, Merlin, Ubios:
|
||||
case DDWrt:
|
||||
return cleanupDDWrt()
|
||||
case Merlin, Ubios:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -85,9 +96,9 @@ func Cleanup() error {
|
||||
func ListenAddress() string {
|
||||
name := Name()
|
||||
switch name {
|
||||
case OpenWrt:
|
||||
return ":53"
|
||||
case DDWrt, Merlin, Ubios:
|
||||
case DDWrt, OpenWrt:
|
||||
return "127.0.0.1:5353"
|
||||
case Merlin, Ubios:
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
func init() {
|
||||
system := &linuxSystemService{
|
||||
name: "ddwrt",
|
||||
detect: func() bool { return Name() == DDWrt },
|
||||
interactive: func() bool {
|
||||
is, _ := isInteractive()
|
||||
return is
|
||||
},
|
||||
new: newddwrtService,
|
||||
}
|
||||
systems := append([]service.System{system}, 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
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
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("get", nvramRCStartupKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := nvram("set", nvramCtrldKeyPrefix+nvramRCStartupKey+"="+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("set", nvramRCStartupKey+"="+val); err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := nvram("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 := nvramCtrldKeyPrefix + nvramRCStartupKey
|
||||
rcStartup, err := nvram("get", ctrldStartupKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = nvram("unset", ctrldStartupKey)
|
||||
if _, err := nvram("set", nvramRCStartupKey+"="+rcStartup); err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := nvram("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
|
||||
}
|
||||
|
||||
var sigChan = make(chan os.Signal, 3)
|
||||
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 1
|
||||
;;
|
||||
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
|
||||
`
|
||||
Reference in New Issue
Block a user