server: multi-address listeners, env-file config, self-update timer + checksums
- Comma-separated ECHOLOT_{CONTROL,UDP,TCP}_LISTEN; one listener/socket per
address. Explicit binds matter on multi-IP hosts (a wildcard would also
claim the SSH-only management address) and per-address UDP sockets are
the substrate stun-5780 needs.
- systemd unit reads /etc/echolot-server.env (seeded once, never
overwritten); --install-systemd with --self-update-api also installs a
daily randomized update timer that try-restarts the service.
- selfupdate: SHA256SUMS verification is now mandatory before the atomic
replace (integrity, not authenticity — signing still TODO).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4de3064f71
commit
43e1ba778a
@@ -8,6 +8,8 @@
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -69,6 +71,35 @@ func Run(api, currentVersion string) error {
|
||||
return fmt.Errorf("release %s has no asset %q", rel.TagName, want)
|
||||
}
|
||||
|
||||
// The release must carry SHA256SUMS; refuse to update without it. This
|
||||
// protects download integrity (truncation, proxy mangling). It is NOT a
|
||||
// defense against a compromised Gitea — both files come from the same
|
||||
// place; a detached signature would be needed for that (still TODO).
|
||||
var sums string
|
||||
for _, a := range rel.Assets {
|
||||
if a.Name == "SHA256SUMS" {
|
||||
resp, err := client.Get(a.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching SHA256SUMS: %w", err)
|
||||
}
|
||||
b, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sums = string(b)
|
||||
}
|
||||
}
|
||||
wantSum := ""
|
||||
for _, line := range strings.Split(sums, "\n") {
|
||||
if fields := strings.Fields(line); len(fields) == 2 && fields[1] == want {
|
||||
wantSum = fields[0]
|
||||
}
|
||||
}
|
||||
if wantSum == "" {
|
||||
return fmt.Errorf("release %s has no SHA256SUMS entry for %q — refusing to update", rel.TagName, want)
|
||||
}
|
||||
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -85,15 +116,18 @@ func Run(api, currentVersion string) error {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, dl.Body)
|
||||
h := sha256.New()
|
||||
_, err = io.Copy(io.MultiWriter(f, h), dl.Body)
|
||||
dl.Body.Close()
|
||||
f.Close()
|
||||
if err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
// TODO(security): verify a detached signature/checksum asset before the
|
||||
// rename — a Gitea compromise currently equals code execution here.
|
||||
if got := hex.EncodeToString(h.Sum(nil)); got != wantSum {
|
||||
os.Remove(tmp)
|
||||
return fmt.Errorf("checksum mismatch for %s: got %s want %s", want, got, wantSum)
|
||||
}
|
||||
if err := os.Rename(tmp, self); err != nil {
|
||||
os.Remove(tmp)
|
||||
return fmt.Errorf("atomic replace failed (filesystem boundaries?): %w", err)
|
||||
|
||||
Reference in New Issue
Block a user