software steht so weit.

jetzt build + docker build und testen
This commit is contained in:
br committed 2026-10-02 13:25:34 +02:00
1 parent 42f0c0accb
commit ca6cdcca63
90 files changed
+10298 -480

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
package agent
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"sultan/pkg/auth"
"sultan/pkg/license"
"sultan/pkg/server"
"sultan/pkg/stream"
)
// currentSigner liefert den aktuellen Signierer (rotate_key tauscht ihn aus).
func (a *Agent) currentSigner() *auth.Signer {
a.keyMu.RLock()
defer a.keyMu.RUnlock()
return a.signer
}
func (a *Agent) setSigner(s *auth.Signer) {
a.keyMu.Lock()
a.signer = s
a.keyMu.Unlock()
}
// handleEvent verarbeitet Events vom Server. Nur Events mit gültiger
// Signatur des Master-Keys werden beachtet; Fehler beenden nie den Stream.
func (a *Agent) handleEvent(ev stream.Event) error {
pub, err := auth.DecodePublicKey(a.id.MasterPublicKey)
if err != nil {
a.log.Error("Master-Key in node.json unlesbar – Event verworfen", "err", err)
return nil
}
if err := stream.VerifyEvent(ev, pub); err != nil {
a.log.Warn("Event ohne gültige Server-Signatur verworfen", "type", ev.Type, "err", err)
return nil
}
switch ev.Type {
case stream.EventLeaseRenew:
a.recordLease(ev.Payload, pub)
case stream.EventNodeCommand:
cmd, err := stream.ParseNodeCommand(ev)
if err != nil {
a.log.Warn("Befehl unlesbar", "err", err)
return nil
}
a.runCommand(cmd)
default:
a.log.Info("Ereignis vom Server", "type", ev.Type)
}
return nil
}
// recordLease prüft die Lease, legt sie als lease.json ab und protokolliert
// sie (vorerst ohne weitere Konsequenz, Spec 4a).
func (a *Agent) recordLease(doc []byte, pub ed25519.PublicKey) {
l, err := license.ParseLease(doc, pub)
if err != nil {
a.log.Warn("Lease ungültig – verworfen", "err", err)
return
}
if l.NodeID != a.id.NodeID {
a.log.Warn("Lease für anderen Knoten – verworfen", "node", l.NodeID)
return
}
tmp := a.path("lease.json.tmp")
if err := os.WriteFile(tmp, doc, 0o600); err == nil {
err = os.Rename(tmp, a.path("lease.json"))
}
if err != nil {
a.log.Warn("lease.json nicht gespeichert", "err", err)
}
a.log.Info("Lease erhalten", "valid_until", l.ValidUntil, "renew_after", l.RenewAfter, "tier", l.Tier)
}
func (a *Agent) runCommand(cmd stream.NodeCommand) {
switch cmd.Command {
case stream.CmdResync:
a.log.Info("Server fordert Zustand an (resync)")
if a.kick != nil {
a.kick()
}
case stream.CmdRotateKey:
a.log.Info("Server fordert Schlüsselwechsel an", "reason", cmd.Reason)
ctx := a.runCtx
if ctx == nil {
ctx = context.Background()
}
if err := a.rotateKey(ctx); err != nil {
a.log.Error("Schlüsselwechsel fehlgeschlagen", "err", err)
}
case stream.CmdConfig:
a.updateServerURLs(cmd.ServerURLs)
case stream.CmdDrain, stream.CmdMaintenance, stream.CmdResume:
a.log.Info("Hinweis vom Server", "command", cmd.Command, "reason", cmd.Reason)
default:
a.log.Warn("Unbekannter Befehl ignoriert", "command", cmd.Command)
}
}
// rotateKey erzeugt einen neuen Schlüssel (node.key.new) und meldet ihn.
func (a *Agent) rotateKey(ctx context.Context) error {
newPath := a.path("node.key.new")
if _, err := os.Stat(newPath); errors.Is(err, os.ErrNotExist) {
key, err := auth.GenerateKey()
if err != nil {
return err
}
if err := auth.WritePrivateKey(newPath, key); err != nil {
return err
}
}
return a.completeRotation(ctx)
}
// completeRotation meldet node.key.new beim Server und benennt die Datei erst
// danach um. Abbruch-sicher: Lehnt der Server den alten Schlüssel ab (er hat
// den neuen schon), wird mit dem neuen wiederholt.
func (a *Agent) completeRotation(ctx context.Context) error {
newPath := a.path("node.key.new")
newKey, err := auth.ReadPrivateKey(newPath)
if err != nil {
return err
}
body, _ := json.Marshal(server.RotateKeyRequest{
PublicKey: auth.EncodePublicKey(newKey.Public().(ed25519.PublicKey)),
Proof: auth.RotationProof(a.id.NodeID, newKey),
})
newSigner := &auth.Signer{Key: newKey}
status, err := a.postSigned(ctx, "/api/v1/rotate-key", body, a.currentSigner())
if err == nil && status == http.StatusUnauthorized {
status, err = a.postSigned(ctx, "/api/v1/rotate-key", body, newSigner)
}
if err != nil {
return err
}
if status != http.StatusNoContent {
return fmt.Errorf("Server lehnt Schlüsselwechsel ab (%d)", status)
}
if err := os.Rename(newPath, a.path("node.key")); err != nil {
return err
}
a.setSigner(newSigner)
a.log.Info("Knotenschlüssel gewechselt")
return nil
}
func (a *Agent) postSigned(ctx context.Context, path string, body []byte, signer *auth.Signer) (int, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.currentServerURL()+path, bytes.NewReader(body))
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/json")
if err := signer.Sign(req); err != nil {
return 0, err
}
resp, err := a.httpClient().Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 && resp.StatusCode != http.StatusUnauthorized {
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return resp.StatusCode, fmt.Errorf("Server antwortet %d: %s", resp.StatusCode, strings.TrimSpace(string(msg)))
}
return resp.StatusCode, nil
}