Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
217 lines
9.7 KiB
Go
217 lines
9.7 KiB
Go
// Package backupformat implementiert den versionierten Syncova Backup Container.
|
|
//
|
|
// Der Container ist die portable Form eines Backups: eine einzelne, in sich
|
|
// geschlossene Datei, die alles enthält, was zur Wiederherstellung nötig ist
|
|
// (PROMPT.md §13, SYNCOVA_ARCHITECTURE.md §8). Er dient der Übertragung an ein
|
|
// zweites Repository, der Archivierung und dem Austausch zwischen Installationen.
|
|
//
|
|
// Aufbau:
|
|
//
|
|
// +--------------------------------------------------+
|
|
// | Magic "SYNCOVA1" (8 Byte) |
|
|
// | Formatversion (uint16) |
|
|
// | Headerlänge (uint32) + Header (JSON) |
|
|
// +--------------------------------------------------+
|
|
// | Abschnitt: Typ, Flags, Länge, Prüfsumme, Inhalt |
|
|
// | ... beliebig viele Abschnitte ... |
|
|
// +--------------------------------------------------+
|
|
// | Footer: Magic, Manifest-Hash, Gesamt-Prüfsumme, |
|
|
// | Abschlussvermerk |
|
|
// +--------------------------------------------------+
|
|
//
|
|
// Zwei Eigenschaften prägen den Entwurf:
|
|
//
|
|
// - Der Footer steht am Ende. Ein abgeschnittener Container hat keinen und
|
|
// wird dadurch zuverlässig als unvollständig erkannt.
|
|
// - Jeder Abschnitt trägt seine Länge. Eine spätere Programmversion kann
|
|
// Abschnitte ergänzen, die eine ältere überspringt — sofern sie nicht als
|
|
// erforderlich gekennzeichnet sind.
|
|
package backupformat
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// containerMagic kennzeichnet den Beginn eines Syncova-Containers.
|
|
//
|
|
// Die Ziffer am Ende gehört zur Kennung und nicht zur Version: sie verhindert,
|
|
// dass ein Container einer künftigen, grundlegend anderen Formatfamilie
|
|
// versehentlich als lesbar gilt.
|
|
var containerMagic = [8]byte{'S', 'Y', 'N', 'C', 'O', 'V', 'A', '1'}
|
|
|
|
// footerMagic kennzeichnet den Beginn des Footers.
|
|
//
|
|
// Eine eigene Kennung erlaubt es, den Footer auch dann zu finden, wenn die
|
|
// Abschnittskette beschädigt ist — eine Notfallanalyse bleibt damit möglich.
|
|
var footerMagic = [8]byte{'S', 'Y', 'N', 'F', 'O', 'O', 'T', '1'}
|
|
|
|
// Versionen des Containerformats.
|
|
const (
|
|
// FormatVersion ist die Version, die diese Programmversion schreibt.
|
|
FormatVersion uint16 = 1
|
|
// MinimumReadableVersion ist die älteste noch lesbare Version.
|
|
//
|
|
// Ältere Container werden abgelehnt statt fehlerhaft gedeutet: ein
|
|
// falsch interpretiertes Backup ist schlimmer als ein nicht gelesenes.
|
|
MinimumReadableVersion uint16 = 1
|
|
// MaximumReadableVersion ist die neueste lesbare Version.
|
|
//
|
|
// Ein neuerer Container wird nicht angetastet, weil diese Programmversion
|
|
// seine Regeln nicht kennt (PROMPT.md §13).
|
|
MaximumReadableVersion uint16 = 1
|
|
)
|
|
|
|
// SectionType benennt die Art eines Abschnitts.
|
|
//
|
|
// Die Werte sind Teil des Formatvertrags und dürfen niemals neu belegt werden:
|
|
// ein bestehender Container würde sonst falsch gedeutet.
|
|
type SectionType uint8
|
|
|
|
const (
|
|
// SectionManifest enthält das Manifest des Backups als JSON.
|
|
SectionManifest SectionType = 1
|
|
// SectionChunkIndex enthält das Verzeichnis aller Chunks mit Länge und Prüfsumme.
|
|
SectionChunkIndex SectionType = 2
|
|
// SectionBlockMap ordnet logische Bereiche der Quelle den Chunks zu.
|
|
SectionBlockMap SectionType = 3
|
|
// SectionDataChunks enthält die eigentlichen Datenblöcke.
|
|
SectionDataChunks SectionType = 4
|
|
// SectionSourceMetadata enthält Angaben zum gesicherten System.
|
|
SectionSourceMetadata SectionType = 5
|
|
// SectionIntegrityInfo enthält ergänzende Integritätsangaben.
|
|
SectionIntegrityInfo SectionType = 6
|
|
)
|
|
|
|
// String liefert die sprechende Bezeichnung eines Abschnittstyps.
|
|
//
|
|
// Sie erscheint in Fehlermeldungen, damit ein Administrator ohne Formattabelle
|
|
// versteht, welcher Teil betroffen ist.
|
|
func (sectionType SectionType) String() string {
|
|
switch sectionType {
|
|
case SectionManifest:
|
|
return "Manifest"
|
|
case SectionChunkIndex:
|
|
return "Chunk-Verzeichnis"
|
|
case SectionBlockMap:
|
|
return "Blockzuordnung"
|
|
case SectionDataChunks:
|
|
return "Datenblöcke"
|
|
case SectionSourceMetadata:
|
|
return "Quellenangaben"
|
|
case SectionIntegrityInfo:
|
|
return "Integritätsangaben"
|
|
default:
|
|
return fmt.Sprintf("unbekannter Abschnitt (%d)", uint8(sectionType))
|
|
}
|
|
}
|
|
|
|
// SectionFlags beschreiben Eigenschaften eines Abschnitts.
|
|
type SectionFlags uint8
|
|
|
|
const (
|
|
// FlagRequired kennzeichnet einen Abschnitt, ohne den das Backup unbrauchbar ist.
|
|
//
|
|
// Das ist der Schlüssel zur Erweiterbarkeit: eine spätere Programmversion
|
|
// darf neue Abschnitte hinzufügen. Kennt eine ältere Version einen
|
|
// optionalen Abschnitt nicht, überspringt sie ihn. Einen als erforderlich
|
|
// gekennzeichneten dagegen darf sie nicht übergehen — sie verweigert die
|
|
// Verarbeitung, statt ein unvollständiges Backup vorzutäuschen.
|
|
FlagRequired SectionFlags = 1 << 0
|
|
// FlagCompressed meldet einen komprimierten Abschnittsinhalt.
|
|
FlagCompressed SectionFlags = 1 << 1
|
|
// FlagEncrypted meldet einen verschlüsselten Abschnittsinhalt.
|
|
FlagEncrypted SectionFlags = 1 << 2
|
|
// FlagDeferredDigest meldet einen Abschnitt ohne Prüfsumme im Kopf.
|
|
//
|
|
// Ein Abschnitt, der als Datenstrom entsteht, kennt seine Prüfsumme erst,
|
|
// wenn er vollständig geschrieben ist — der Kopf steht zu diesem Zeitpunkt
|
|
// aber längst in der Ausgabe. Ihn nachträglich zu überschreiben verlangte
|
|
// eine rückspulbare Senke und schlösse damit Netzwerk- und Pipe-Ziele aus.
|
|
//
|
|
// Die Unversehrtheit solcher Abschnitte ist doppelt gesichert: über die
|
|
// Prüfsumme jedes einzelnen Chunks im Chunk-Verzeichnis und über die
|
|
// Gesamtprüfsumme im Footer. Es entsteht dadurch keine Prüflücke.
|
|
FlagDeferredDigest SectionFlags = 1 << 3
|
|
)
|
|
|
|
// Has meldet, ob ein Flag gesetzt ist.
|
|
func (sectionFlags SectionFlags) Has(testedFlag SectionFlags) bool {
|
|
return sectionFlags&testedFlag != 0
|
|
}
|
|
|
|
// Feste Längen des Formats in Byte.
|
|
const (
|
|
// magicLength ist die Länge der Kennung.
|
|
magicLength = 8
|
|
// versionLength ist die Länge der Versionsangabe.
|
|
versionLength = 2
|
|
// headerLengthFieldLength ist die Länge des Längenfelds des Headers.
|
|
headerLengthFieldLength = 4
|
|
// digestLength ist die Länge einer SHA-256-Prüfsumme.
|
|
digestLength = 32
|
|
// sectionHeaderLength ist die Länge eines Abschnittskopfs:
|
|
// Typ (1) + Flags (1) + Länge (8) + Prüfsumme (32).
|
|
sectionHeaderLength = 1 + 1 + 8 + digestLength
|
|
)
|
|
|
|
// maximumHeaderLength begrenzt die Größe des Headers.
|
|
//
|
|
// Ohne Obergrenze könnte eine manipulierte Längenangabe den Arbeitsspeicher
|
|
// erschöpfen, noch bevor irgendeine Prüfung greift.
|
|
const maximumHeaderLength = 4 * 1024 * 1024
|
|
|
|
// maximumSectionLength begrenzt die Größe eines einzelnen Abschnitts.
|
|
//
|
|
// Der Wert ist großzügig gewählt, verhindert aber, dass eine verfälschte
|
|
// Längenangabe zu einer unbegrenzten Speicheranforderung führt.
|
|
const maximumSectionLength = 64 * 1024 * 1024 * 1024
|
|
|
|
// Fehler des Containerformats.
|
|
var (
|
|
// ErrNotAContainer meldet Daten, die kein Syncova-Container sind.
|
|
ErrNotAContainer = errors.New("die daten sind kein syncova backup container")
|
|
// ErrUnsupportedVersion meldet eine nicht lesbare Formatversion.
|
|
ErrUnsupportedVersion = errors.New("die formatversion des containers wird von dieser programmversion nicht unterstützt")
|
|
// ErrContainerTruncated meldet einen abgeschnittenen Container.
|
|
//
|
|
// Das ist der häufigste Fall eines abgebrochenen Übertragungsvorgangs und
|
|
// muss zuverlässig erkannt werden.
|
|
ErrContainerTruncated = errors.New("der container ist unvollständig: er endet vor dem abschlussvermerk")
|
|
// ErrSectionCorrupted meldet einen Abschnitt, dessen Inhalt nicht zu seiner Prüfsumme passt.
|
|
ErrSectionCorrupted = errors.New("ein abschnitt des containers ist beschädigt")
|
|
// ErrUnknownRequiredSection meldet einen unbekannten, aber erforderlichen Abschnitt.
|
|
ErrUnknownRequiredSection = errors.New("der container enthält einen erforderlichen abschnitt, den diese programmversion nicht kennt")
|
|
// ErrMissingSection meldet einen fehlenden Pflichtabschnitt.
|
|
ErrMissingSection = errors.New("dem container fehlt ein pflichtabschnitt")
|
|
// ErrIncompleteBackup meldet einen Container ohne gültigen Abschlussvermerk.
|
|
ErrIncompleteBackup = errors.New("der container trägt keinen gültigen abschlussvermerk und beschreibt kein vollständiges backup")
|
|
// ErrIntegrityMismatch meldet eine nicht passende Gesamtprüfsumme.
|
|
ErrIntegrityMismatch = errors.New("die prüfsumme des containers stimmt nicht mit seinem inhalt überein")
|
|
// ErrChunkNotInContainer meldet einen im Manifest benannten, aber nicht enthaltenen Chunk.
|
|
ErrChunkNotInContainer = errors.New("ein vom manifest benötigter chunk fehlt im container")
|
|
// ErrSectionTooLarge meldet eine unplausible Längenangabe.
|
|
ErrSectionTooLarge = errors.New("die längenangabe eines abschnitts ist unplausibel groß")
|
|
// ErrWriterClosed meldet die Verwendung eines bereits abgeschlossenen Schreibers.
|
|
ErrWriterClosed = errors.New("der container wurde bereits abgeschlossen")
|
|
)
|
|
|
|
// IsVersionReadable meldet, ob eine Formatversion gelesen werden kann.
|
|
func IsVersionReadable(formatVersion uint16) bool {
|
|
return formatVersion >= MinimumReadableVersion && formatVersion <= MaximumReadableVersion
|
|
}
|
|
|
|
// describeVersionMismatch erklärt eine nicht lesbare Version verständlich.
|
|
//
|
|
// Der Unterschied zwischen "zu alt" und "zu neu" ist für den Administrator
|
|
// entscheidend: im einen Fall hilft ein Konverter, im anderen ein Update.
|
|
func describeVersionMismatch(formatVersion uint16) error {
|
|
if formatVersion > MaximumReadableVersion {
|
|
return fmt.Errorf("%w: der container hat Version %d, diese Programmversion liest bis Version %d. "+
|
|
"Bitte Syncova aktualisieren", ErrUnsupportedVersion, formatVersion, MaximumReadableVersion)
|
|
}
|
|
|
|
return fmt.Errorf("%w: der container hat Version %d, diese Programmversion liest ab Version %d",
|
|
ErrUnsupportedVersion, formatVersion, MinimumReadableVersion)
|
|
}
|