server: refuse unsigned releases and polluted reserved addresses
Self-update now verifies SHA256SUMS.sig (ed25519, relsign package) against a public key baked into the binary; the private key exists only in the CI secret store, so a compromised release host can withhold updates but not inject one. CI signs on every server-v* tag and hard-fails without the secret. Operators with their own pipeline override the key via ECHOLOT_SELF_UPDATE_PUBKEY (mint a pair with release-sign -gen). Startup also now proves 80/443 are actually free on the reserved measurement addresses by asking the OS (throwaway bind), not the config - CheckReserved could never see a stray process, and the adb-beacon receiver on 0.0.0.0:443 was exactly that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
20cfecf566
commit
a49bef5821
@@ -0,0 +1,65 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package selftest
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The real check runs against ports 80/443, which a test cannot bind without privileges; the
|
||||
// port list is what varies here, the mechanism is identical.
|
||||
|
||||
func TestOccupiedPortIsDetected(t *testing.T) {
|
||||
// The stray-process scenario: someone else holds the port before we look.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
port := strconv.Itoa(ln.Addr().(*net.TCPAddr).Port)
|
||||
|
||||
occupied, unverifiable := portsFree([]net.IP{net.ParseIP("127.0.0.1")}, []string{port})
|
||||
if len(occupied) != 1 || !strings.HasSuffix(occupied[0], ":"+port) {
|
||||
t.Fatalf("a listening port must be reported occupied, got occupied=%v unverifiable=%v",
|
||||
occupied, unverifiable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreePortPassesAndStaysFree(t *testing.T) {
|
||||
// Find a port that is free by construction, then check it.
|
||||
probe, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port := strconv.Itoa(probe.Addr().(*net.TCPAddr).Port)
|
||||
probe.Close()
|
||||
|
||||
occupied, unverifiable := portsFree([]net.IP{net.ParseIP("127.0.0.1")}, []string{port})
|
||||
if len(occupied) != 0 || len(unverifiable) != 0 {
|
||||
t.Fatalf("a free port must pass silently, got occupied=%v unverifiable=%v",
|
||||
occupied, unverifiable)
|
||||
}
|
||||
// The check must not keep the port: it proves the state and gets out of the way.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:"+port)
|
||||
if err != nil {
|
||||
t.Fatalf("the check left the port unusable: %v", err)
|
||||
}
|
||||
ln.Close()
|
||||
}
|
||||
|
||||
func TestUnassignedAddressIsUnverifiableNotOccupied(t *testing.T) {
|
||||
// 192.0.2.0/24 is TEST-NET-1: never assigned to this host, so the bind fails with something
|
||||
// other than EADDRINUSE. That is "could not answer", not "occupied" — conflating them would
|
||||
// refuse startup over a typo in ECHOLOT_RESERVED_ADDRS.
|
||||
occupied, unverifiable := portsFree([]net.IP{net.ParseIP("192.0.2.1")}, []string{"65001"})
|
||||
if len(occupied) != 0 {
|
||||
t.Fatalf("an unassigned address must not be reported occupied: %v", occupied)
|
||||
}
|
||||
if len(unverifiable) != 1 {
|
||||
t.Fatalf("an unassigned address must be reported unverifiable, got %v", unverifiable)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user