diff --git a/cmd/netgrip/main.go b/cmd/netgrip/main.go index 5b18661..9cc97b7 100644 --- a/cmd/netgrip/main.go +++ b/cmd/netgrip/main.go @@ -41,6 +41,12 @@ func main() { return } + // Converge the init script on every start, not only on self-update: + // a device updated only by manually copying binaries kept the + // pre-HTTPS init forever and its UCI https toggle stayed a no-op + // (#544). No-op off-router (guarded by /etc/openwrt_release). + modules.SyncInitScriptAtBoot(version) + // The flag always has a value (its default), so only treat it as an // explicit override when it differs from the default endpoint. explicit := "" diff --git a/internal/modules/selfupdate_init.go b/internal/modules/selfupdate_init.go index d3b5def..8294559 100644 --- a/internal/modules/selfupdate_init.go +++ b/internal/modules/selfupdate_init.go @@ -43,18 +43,38 @@ const initMarker = "# netgrip init - managed" // t.TempDir() directory. var initScriptPath = "/etc/init.d/netgrip" +// openwrtReleasePath gates the startup sync: without it the process is not +// on a router (dev machine, demo mode) and there is nothing to heal. +var openwrtReleasePath = "/etc/openwrt_release" + func canonicalInitScript() ([]byte, error) { return initScriptFS.ReadFile("selfupdate_init/netgrip.init") } +// SyncInitScriptAtBoot converges the init script on every process start, +// not only before a self-update restart: a device that only ever receives +// manually copied binaries would otherwise keep a pre-HTTPS script forever +// and its UCI https toggle would stay a no-op (#544). Same safety rules as +// the self-update path: customized scripts are never touched. +func SyncInitScriptAtBoot(currentVersion string) { + if _, err := os.Stat(openwrtReleasePath); err != nil { + return + } + ensureInitScript("startup", currentVersion) +} + // syncInitScript refreshes the init script before the post-update restart. // Best effort on purpose: a failure here must not brick an update whose // binary is already in place; the mismatch only means newer UCI-driven // flags (e.g. HTTPS) stay unavailable until fixed. func syncInitScript(currentVersion string) { + ensureInitScript("self-update", currentVersion) +} + +func ensureInitScript(reason, currentVersion string) { desired, err := canonicalInitScript() if err != nil { - log.Printf("netgrip: self-update: embedded init script unreadable: %v", err) + log.Printf("netgrip: %s: embedded init script unreadable: %v", reason, err) return } @@ -62,19 +82,19 @@ func syncInitScript(currentVersion string) { switch { case errors.Is(err, os.ErrNotExist): if err := writeInitScript(desired); err != nil { - log.Printf("netgrip: self-update: install init script: %v", err) + log.Printf("netgrip: %s: install init script: %v", reason, err) return } - log.Printf("netgrip: self-update: installed missing init script %s", initScriptPath) + log.Printf("netgrip: %s: installed missing init script %s", reason, initScriptPath) case err != nil: - log.Printf("netgrip: self-update: read init script: %v", err) + log.Printf("netgrip: %s: read init script: %v", reason, err) case bytes.Equal(existing, desired): // Already the current script; do not rewrite so the mtime stays. case !strings.Contains(string(existing), initMarker) && !isPackagedLegacyScript(existing): - log.Printf("netgrip: self-update: %s has no %q marker and matches no packaged variant, leaving the customized script untouched", - initScriptPath, initMarker) + log.Printf("netgrip: %s: %s has no %q marker and matches no packaged variant, leaving the customized script untouched", + reason, initScriptPath, initMarker) default: - rewriteInitScript(existing, desired, currentVersion) + rewriteInitScript(existing, desired, reason, currentVersion) } } @@ -104,17 +124,17 @@ func isPackagedLegacyScript(content []byte) bool { // rewriteInitScript replaces the init script with the canonical one, keeping // a versioned backup of the previous content. -func rewriteInitScript(existing, desired []byte, currentVersion string) { +func rewriteInitScript(existing, desired []byte, reason, currentVersion string) { backup := fmt.Sprintf("%s.bak-%s", initScriptPath, strings.TrimPrefix(currentVersion, "v")) if err := os.WriteFile(backup, existing, 0755); err != nil { - log.Printf("netgrip: self-update: backup init script: %v", err) + log.Printf("netgrip: %s: backup init script: %v", reason, err) return } if err := writeInitScript(desired); err != nil { - log.Printf("netgrip: self-update: update init script: %v", err) + log.Printf("netgrip: %s: update init script: %v", reason, err) return } - log.Printf("netgrip: self-update: init script updated (backup at %s)", backup) + log.Printf("netgrip: %s: init script updated (backup at %s)", reason, backup) } func writeInitScript(content []byte) error { diff --git a/internal/modules/selfupdate_init_test.go b/internal/modules/selfupdate_init_test.go index 33fca2e..469b4a4 100644 --- a/internal/modules/selfupdate_init_test.go +++ b/internal/modules/selfupdate_init_test.go @@ -215,3 +215,84 @@ func TestSelfUpdateEmbeddedInitMatchesPackaged(t *testing.T) { t.Errorf("embedded init script diverged from deploy/openwrt/netgrip/files/netgrip.init;\nrefresh internal/modules/selfupdate_init/netgrip.init") } } + +// useTempOpenWrtRelease points openwrtReleasePath at a file inside a +// t.TempDir() (created only when present) and restores it on cleanup. +func useTempOpenWrtRelease(t *testing.T, present bool) { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "openwrt_release") + if present { + if err := os.WriteFile(path, []byte("DISTRIB_ID='OpenWrt'\n"), 0644); err != nil { + t.Fatal(err) + } + } + old := openwrtReleasePath + openwrtReleasePath = path + t.Cleanup(func() { openwrtReleasePath = old }) +} + +// TestBootSyncHealsLegacyPackagedScript: the startup sync behaves like the +// self-update one, healing a pristine legacy packaged script so a device +// that only ever got manually copied binaries converges too (#544). +func TestBootSyncHealsLegacyPackagedScript(t *testing.T) { + useTempOpenWrtRelease(t, true) + path := useTempInitScript(t) + + entries, err := legacyInitFS.ReadDir("selfupdate_init/legacy") + if err != nil { + t.Fatal(err) + } + if len(entries) == 0 { + t.Fatal("no legacy init scripts embedded") + } + legacy, err := legacyInitFS.ReadFile("selfupdate_init/legacy/" + entries[0].Name()) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, legacy, 0755); err != nil { + t.Fatal(err) + } + + SyncInitScriptAtBoot("v0.76.99") + + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + want, err := canonicalInitScript() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Errorf("startup sync did not heal the legacy packaged script:\n--- got ---\n%s", got) + } +} + +// TestBootSyncSkipsOffRouter: without /etc/openwrt_release (dev machine, +// demo mode) the startup sync must not create or rewrite anything. +func TestBootSyncSkipsOffRouter(t *testing.T) { + useTempOpenWrtRelease(t, false) + path := useTempInitScript(t) + + legacy, err := legacyInitFS.ReadFile("selfupdate_init/legacy/" + "netgrip-v1.init") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, legacy, 0755); err != nil { + t.Fatal(err) + } + + SyncInitScriptAtBoot("v0.76.99") + + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, legacy) { + t.Errorf("off-router startup sync rewrote the init script") + } + if _, err := os.Stat(path + ".bak-0.76.99"); !os.IsNotExist(err) { + t.Errorf("off-router startup sync created a backup: %v", err) + } +}