Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions cmd/netgrip/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := ""
Expand Down
42 changes: 31 additions & 11 deletions internal/modules/selfupdate_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,38 +43,58 @@ 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
}

existing, err := os.ReadFile(initScriptPath)
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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down
81 changes: 81 additions & 0 deletions internal/modules/selfupdate_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading