package proxmox import ( "context" "fmt" "io" "os" "path/filepath" "strings" ) // LocalArchiveTransport liest Sicherungsarchive über das Dateisystem. // // Der einfachste und schnellste Weg — er gilt in zwei Fällen: // // 1. Syncova läuft auf dem Proxmox-Knoten selbst. // 2. Der Proxmox-Sicherungsspeicher ist auf dem Syncova-Server eingehängt // (NFS oder CIFS, wie es bei einem gemeinsamen Speicher ohnehin üblich ist). // // Er überträgt nichts über das Netz und braucht keine Anmeldedaten — die Datei // wird gelesen, wo sie liegt. In einem Verbund mit gemeinsamem Speicher ist das // die Bauart, die man ohnehin hat. type LocalArchiveTransport struct { // mountRoots ordnet Proxmox-Speicherkennungen lokalen Pfaden zu. // // Beispiel: {"local": "/var/lib/vz", "nfs-backup": "/mnt/proxmox-backup"}. // // Die Zuordnung ist ausdrücklich, nicht geraten: Ein Speicher heißt auf dem // Knoten anders als auf dem Server, der ihn einhängt, und ein geratener // Pfad führte entweder ins Leere oder — schlimmer — auf fremde Daten. mountRoots map[string]string } // NewLocalArchiveTransport erzeugt den Dateizugriff. func NewLocalArchiveTransport(mountRoots map[string]string) *LocalArchiveTransport { copiedRoots := make(map[string]string, len(mountRoots)) for storageIdentifier, mountPath := range mountRoots { copiedRoots[storageIdentifier] = filepath.Clean(mountPath) } return &LocalArchiveTransport{mountRoots: copiedRoots} } // OpenArchive öffnet ein Sicherungsarchiv über das Dateisystem. func (transport *LocalArchiveTransport) OpenArchive(_ context.Context, nodeName string, volumeIdentifier string) (io.ReadCloser, error) { storageIdentifier, relativePath, parseError := SplitVolumeIdentifier(volumeIdentifier) if parseError != nil { return nil, parseError } mountRoot, isMapped := transport.mountRoots[storageIdentifier] if !isMapped { return nil, fmt.Errorf("fuer den proxmox-speicher %q ist kein lokaler pfad hinterlegt "+ "(knoten %s). tragen sie ihn ein oder verwenden sie den ssh-zugriff", storageIdentifier, nodeName) } archivePath := filepath.Join(mountRoot, filepath.FromSlash(relativePath)) // Der zusammengesetzte Pfad muss unterhalb des Einhängepunkts bleiben. // // Die Volumenkennung kommt vom Proxmox-Knoten. Vertrauenswuerdig ist sie // nur so weit, wie man dem Knoten vertraut — und ein Pfad wie // "backup/../../../etc/shadow" laese dann eine beliebige Datei des // Syncova-Servers ins Backup (dieselbe Ueberlegung wie bei // validateManifestPath, Phase 5). if !isWithinDirectory(archivePath, mountRoot) { return nil, fmt.Errorf("die volumenkennung %q fuehrt aus dem speicherverzeichnis heraus", volumeIdentifier) } archiveFile, openError := os.Open(archivePath) if openError != nil { return nil, fmt.Errorf("das sicherungsarchiv %q liess sich nicht oeffnen: %w", archivePath, openError) } return archiveFile, nil } // SplitVolumeIdentifier zerlegt eine Proxmox-Volumenkennung. // // Sie hat die Form ":", etwa // "local:backup/vzdump-qemu-100-2026_08_11-02_00_00.vma". Der Doppelpunkt // trennt genau einmal — ein Pfad darf selbst keinen enthalten. func SplitVolumeIdentifier(volumeIdentifier string) (storageIdentifier string, relativePath string, parseError error) { trimmedIdentifier := strings.TrimSpace(volumeIdentifier) storageIdentifier, relativePath, hasSeparator := strings.Cut(trimmedIdentifier, ":") if !hasSeparator || storageIdentifier == "" || relativePath == "" { return "", "", fmt.Errorf("die volumenkennung %q hat nicht die form :", volumeIdentifier) } return storageIdentifier, relativePath, nil } // isWithinDirectory meldet, ob ein Pfad in einem Verzeichnis liegt. // // Ueber die Pfadtrennung, nicht ueber strings.HasPrefix: Sonst gaelte // "/mnt/backups-alt" als Teil von "/mnt/backup" (derselbe Fund wie in Phase 19). func isWithinDirectory(candidatePath string, directoryPath string) bool { cleanedCandidate := filepath.Clean(candidatePath) cleanedDirectory := filepath.Clean(directoryPath) if cleanedCandidate == cleanedDirectory { return true } relativePath, relativeError := filepath.Rel(cleanedDirectory, cleanedCandidate) if relativeError != nil { return false } return relativePath != ".." && !strings.HasPrefix(relativePath, ".."+string(filepath.Separator)) }