software steht so weit.
jetzt build + docker build und testen
This commit is contained in:
1 parent
42f0c0accb
commit
ca6cdcca63
90 files changed
+10298
-480
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user