Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bdfa0a0
Port Merlin postconf coexistence to v1.x
maghuro Sep 27, 2026
4fff873
Port Merlin postconf coexistence to v1.x
maghuro Sep 27, 2026
470397c
Port Merlin postconf coexistence to v1.x
maghuro Sep 27, 2026
0cec599
Port Merlin DNS lifecycle fixes to v1.x
maghuro Sep 27, 2026
7132ddd
Port Merlin DNS lifecycle fixes to v1.x
maghuro Sep 27, 2026
780f4f7
Port Merlin DNS lifecycle fixes to v1.x
maghuro Sep 27, 2026
0fabf14
Port Merlin DNS lifecycle fixes to v1.x
maghuro Sep 27, 2026
1ec4877
Port Merlin service lifecycle fixes to v1.x
maghuro Sep 27, 2026
2f2e2bf
Add Merlin service lifecycle regression tests
maghuro Sep 27, 2026
5b52481
Make Merlin service directory sync CI-portable
maghuro Sep 27, 2026
f1701d0
Make mode preservation test umask-safe
maghuro Sep 27, 2026
2d38cce
Make mode preservation test umask-safe
maghuro Sep 27, 2026
23ace09
Sync latest Merlin DNS lifecycle hardening
maghuro Sep 27, 2026
263ece1
Sync latest Merlin DNS regression tests
maghuro Sep 27, 2026
708963a
Sync latest service_merlin.go hardening
maghuro Sep 27, 2026
4fdf701
Sync latest service_merlin_test.go hardening
maghuro Sep 27, 2026
741048d
Sync POSIX Merlin cleanup path semantics
maghuro Sep 27, 2026
d029e68
Sync POSIX Merlin journal regression tests
maghuro Sep 27, 2026
8d8b640
Keep Merlin directory fsync Unix-specific on v1.x
maghuro Sep 27, 2026
5b1123a
Port final Merlin lifecycle service_merlin.go to v1.x
maghuro Sep 27, 2026
d305117
Port final Merlin lifecycle service_merlin_test.go to v1.x
maghuro Sep 27, 2026
17a0e59
Port final Merlin lifecycle service_merlin_lock_linux.go to v1.x
maghuro Sep 27, 2026
e15d232
Port final Merlin lifecycle service_merlin_lock_other.go to v1.x
maghuro Sep 27, 2026
9d70e05
Port final Merlin lifecycle service_merlin_startup_linux.go to v1.x
maghuro Sep 27, 2026
50a3ad9
Port final Merlin lifecycle service_merlin_startup_other.go to v1.x
maghuro Sep 27, 2026
ec36749
Port final Merlin lifecycle service_merlin_unix_test.go to v1.x
maghuro Sep 27, 2026
9f40505
Port final Merlin DNS/SDN safety fixes to v1.x
maghuro Sep 27, 2026
2d79f58
Port final Merlin DNS regression coverage to v1.x
maghuro Sep 27, 2026
f3fbe90
Serialize Merlin restart and upgrade lifecycle on v1.x
maghuro Sep 27, 2026
05ea81e
Migrate Merlin startup script from new v1.x binary
maghuro Sep 27, 2026
c9adc36
Merge latest v1.x postconf base into DNS/SDN port
maghuro Sep 27, 2026
e7196ad
Merge PR #6: preserve Merlin dnsmasq.postconf content
maghuro Sep 27, 2026
4e5b1e1
Merge updated v1.0 base after PR #6
maghuro Sep 27, 2026
597e934
Merge PR #7: use Merlin DNS hooks for main and SDN dnsmasq
maghuro Sep 27, 2026
e3f5dfb
Merge updated v1.0 base after Merlin DNS/SDN changes
maghuro Sep 27, 2026
13634e7
Merge PR #8: harden Asuswrt-Merlin service lifecycle
maghuro Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,18 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
os.Exit(0)
}

// On Merlin, self-upgrade replaces the executable before the old process
// invokes Service.Start. The newly launched `ctrld run` process is therefore
// the first guaranteed execution point for new service-lifecycle code.
// Migrate an exact ctrld-owned legacy startup script here. Failure is
// deliberately non-fatal: the legacy script remains atomically intact and
// the next daemon start retries the migration.
if router.Name() == "merlin" {
if err := router.RefreshMerlinStartupScriptFromRunningBinary(); err != nil {
mainLog.Load().Warn().Err(err).Msg("could not refresh Merlin startup script")
}
}

p.onStarted = append(p.onStarted, func() {
for _, lc := range p.cfg.Listener {
if shouldAllocateLoopbackIP(lc.IP) {
Expand Down
162 changes: 117 additions & 45 deletions cmd/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,19 @@ func initStopCmd() *cobra.Command {
return stopCmd
}


type restartWithBetweenService interface {
RestartWith(func() error) error
}

func restartServiceTransaction(s service.Service, between func() error) (bool, error) {
tx, ok := s.(restartWithBetweenService)
if !ok {
return false, nil
}
return true, tx.RestartWith(between)
}

func initRestartCmd() *cobra.Command {
restartCmd := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -971,49 +984,77 @@ func initRestartCmd() *cobra.Command {
}

doRestart := func() bool {
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second * 1)
return nil
}, false, "Waiting for service to stop"},
}
if doTasks(tasks) {
between := func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
time.Sleep(time.Second)

if router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

loop:
for {
select {
case <-ctx.Done():
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
break loop
return nil
default:
}
time.Sleep(time.Second)
if status, _ := s.Status(); status == service.StatusStopped {
break
}
time.Sleep(time.Second)
}
}
} else {
return false
return nil
}

tasks = []task{
{s.Start, true, "Start"},
if _, ok := s.(restartWithBetweenService); ok {
return doTasks([]task{
{func() error {
_, err := restartServiceTransaction(s, between)
return err
}, true, "Restart"},
})
}

return doTasks(tasks)
// Preserve the existing restart behavior for non-Merlin service
// backends. Only Merlin opts into RestartWith.
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second)
return nil
}, false, "Waiting for service to stop"},
}
if !doTasks(tasks) {
return false
}
if router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

loop:
for {
select {
case <-ctx.Done():
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
break loop
default:
}
time.Sleep(time.Second)
if status, _ := s.Status(); status == service.StatusStopped {
break
}
}
}
return doTasks([]task{{s.Start, true, "Start"}})
}

if doRestart() {
Expand Down Expand Up @@ -1547,25 +1588,58 @@ func initUpgradeCmd() *cobra.Command {
if !svcInstalled {
return true
}
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second * 1)
return nil
}, false, "Waiting for service to stop"},
}
if doTasks(tasks) {

between := func() error {
p.router.Cleanup()
// restore static DNS settings or DHCP
p.resetDNS(false, true)
time.Sleep(time.Second)

if router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

for {
select {
case <-ctx.Done():
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
return nil
default:
}
if status, _ := s.Status(); status == service.StatusStopped {
break
}
time.Sleep(time.Second)
}
}
return nil
}

if _, ok := s.(restartWithBetweenService); ok {
_, err := restartServiceTransaction(s, between)
if err != nil {
mainLog.Load().Error().Err(err).Msg("failed to restart ctrld service")
return false
}
} else {
// Keep the historical upgrade restart path for every non-Merlin
// backend; they do not participate in the new Merlin flock.
tasks := []task{
{s.Stop, true, "Stop"},
{func() error {
p.router.Cleanup()
p.resetDNS(false, true)
return nil
}, false, "Cleanup"},
{func() error {
time.Sleep(time.Second)
return nil
}, false, "Waiting for service to stop"},
}
if doTasks(tasks) && router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

loop:
for {
select {
Expand All @@ -1580,17 +1654,15 @@ func initUpgradeCmd() *cobra.Command {
}
}
}
if !doTasks([]task{{s.Start, true, "Start"}}) {
return false
}
}

tasks = []task{
{s.Start, true, "Start"},
}
if doTasks(tasks) {
if dir, err := socketDir(); err == nil {
if cc := newSocketControlClient(context.TODO(), s, dir); cc != nil {
_, _ = cc.post(ifacePath, nil)
return true
}
if dir, err := socketDir(); err == nil {
if cc := newSocketControlClient(context.TODO(), s, dir); cc != nil {
_, _ = cc.post(ifacePath, nil)
return true
}
}
return false
Expand Down
45 changes: 33 additions & 12 deletions internal/router/dnsmasq/dnsmasq.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package dnsmasq

import (
"errors"
"text/template"
"net"
"os"
"path/filepath"
"strings"
"text/template"

"github.com/Control-D-Inc/ctrld"
)
Expand All @@ -28,22 +28,44 @@ max-cache-ttl=0
`

const (
MerlinConfPath = "/tmp/etc/dnsmasq.conf"
MerlinJffsConfDir = "/jffs/configs"
MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
MerlinConfPath = "/tmp/etc/dnsmasq.conf"
MerlinJffsConfDir = "/jffs/configs"
MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
MerlinSdnPostConfPath = "/jffs/scripts/dnsmasq-sdn.postconf"
)

const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF`
const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY

#!/bin/sh
const (
MerlinPostConfBeginMarker = `# BEGIN ctrld`
MerlinPostConfEndMarker = `# END ctrld`
MerlinSyntheticShebangMarker = `# ctrld: synthetic shebang`
// MerlinPostConfMarker is the legacy end marker used by ctrld <= 1.5.7.
MerlinPostConfMarker = `# GENERATED BY ctrld - EOF`
)

const MerlinPostConfTmpl = `(
config_file="$1"
. /usr/sbin/helper.sh

pid=$(cat /tmp/ctrld.pid 2>/dev/null)
if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
ctrld_cmd=""
case "$pid" in
''|*[!0-9]*) ;;
*)
if [ -r "/proc/${pid}/cmdline" ]; then
ctrld_cmd=$(tr '\000' '\n' < "/proc/${pid}/cmdline" 2>/dev/null | sed -n '1p')
fi
;;
esac
case "$ctrld_cmd" in
ctrld|*/ctrld)
ctrld_running=1
;;
*)
ctrld_running=0
;;
esac
if [ "$ctrld_running" -eq 1 ]; 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
Expand All @@ -66,9 +88,8 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
# 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 {
Expand Down
Loading