utils/winutil utils/winutil/vss: add utility function for extracting data from Windows System Restore Point backups.

utils/winutil/vss contains just enough COM wrapping to query the Volume Shadow Copy service for snapshots.
WalkSnapshotsForLegacyStateDir is the friendlier interface that adds awareness of our actual use case,
mapping the snapshots and locating our legacy state directory.

Updates #3011

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
pull/3138/head
Aaron Klotz 3 years ago
parent 6425f497b1
commit c6ea282b3f

@ -226,6 +226,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
tailscale.com/util/winutil from tailscale.com/cmd/tailscaled+
W 💣 tailscale.com/util/winutil/vss from tailscale.com/util/winutil
tailscale.com/version from tailscale.com/client/tailscale+
tailscale.com/version/distro from tailscale.com/cmd/tailscaled+
W tailscale.com/wf from tailscale.com/cmd/tailscaled

@ -0,0 +1,359 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package vss provides a minimal set of wrappers for the COM interfaces used for
// accessing Windows's Volume Shadow Copy Service.
package vss
import (
"errors"
"fmt"
"io"
"sort"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// Type representing a C pointer to a null-terminated UTF-16 string that was allocated
// by the COM runtime.
type COMAllocatedString uintptr
type VSS_TIMESTAMP int64
// SnapshotProperties is the Go representation of the VSS_SNAPSHOT_PROP structure from the Windows SDK
type SnapshotProperties struct {
SnapshotId windows.GUID
SnapshotSetId windows.GUID
SnapshotsCount int32
SnapshotDeviceObject COMAllocatedString
OriginalVolumeName COMAllocatedString
OriginatingMachine COMAllocatedString
ServiceMachine COMAllocatedString
ExposedName COMAllocatedString
ExposedPath COMAllocatedString
ProviderId windows.GUID
SnapshotAttributes int32
CreationTimestamp VSS_TIMESTAMP
Status int32
}
// Because of the constraints that this package applies to queries, the objType
// field may be ignored when reading its data.
type ObjectProperties struct {
objType int32
Obj SnapshotProperties
}
type SnapshotList []ObjectProperties
// SnapshotEnumerator is the interface that enables execution of a VSS query.
// QuerySnapshots returns a SnapshotList of all available snapshots.
// The elements of the SnapshotList should be returned in reverse chronological order.
type SnapshotEnumerator interface {
io.Closer
QuerySnapshots() (SnapshotList, error)
}
var (
vssApi = windows.NewLazySystemDLL("VssApi.dll")
procCreateVssBackupComponentsInternal = vssApi.NewProc("CreateVssBackupComponentsInternal")
)
const vssCtxClientAccessibleWriters = 0x0000000d
// NewSnapshotEnumerator instantiates the necessary OS facilities for accessing
// the Volume Shadow Copy service, and then returns a SnapshotEnumerator that
// may then be used for executing a query against the service.
func NewSnapshotEnumerator() (SnapshotEnumerator, error) {
var result vssBackupComponentsWrap
hresult, _, _ := procCreateVssBackupComponentsInternal.Call(uintptr(unsafe.Pointer(&result.iface)))
err := errorFromHRESULT(hresult)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
result.Close()
}
}()
err = result.iface.InitializeForBackup()
if err != nil {
return nil, err
}
// vssCtxClientAccessibleWriters is the context that we need to be able to access
// system restore points.
err = result.iface.SetContext(vssCtxClientAccessibleWriters)
if err != nil {
return nil, err
}
return &result, nil
}
func (s *COMAllocatedString) Close() error {
if s != nil {
windows.CoTaskMemFree(unsafe.Pointer(*s))
*s = 0
}
return nil
}
func (s *COMAllocatedString) String() string {
if s == nil {
return "<nil>"
}
return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(*s)))
}
func (ts VSS_TIMESTAMP) ToFiletime() windows.Filetime {
return *((*windows.Filetime)(unsafe.Pointer(&ts)))
}
// Converts a windows.Filetime to a VSS_TIMESTAMP
func VSSTimestampFromFiletime(ft windows.Filetime) VSS_TIMESTAMP {
return *((*VSS_TIMESTAMP)(unsafe.Pointer(&ft)))
}
func (p *SnapshotProperties) Close() error {
if p == nil {
return nil
}
p.SnapshotDeviceObject.Close()
p.OriginalVolumeName.Close()
p.OriginatingMachine.Close()
p.ServiceMachine.Close()
p.ExposedName.Close()
p.ExposedPath.Close()
return nil
}
func (props *ObjectProperties) Close() error {
if props == nil {
return nil
}
return props.Obj.Close()
}
func (snapList *SnapshotList) Close() error {
if snapList == nil {
return nil
}
for _, snap := range *snapList {
snap.Close()
}
return nil
}
func errorFromHRESULT(value uintptr) error {
// In C, HRESULTS are typedef'd as LONG, which on Windows is always int32
hr := int32(value)
if hr < 0 {
return windows.Errno(hr)
}
return nil
}
type unknownVtbl struct {
QueryInterface uintptr
AddRef uintptr
Release uintptr
}
// The complete vtable for IVssEnumObject.
// We only call Release and Next, so most of these fields, while populated by
// Windows, are unused by us.
type vssEnumObjectVtbl struct {
unknownVtbl
Next uintptr
Skip uintptr
Reset uintptr
Clone uintptr
}
type vssEnumObjectABI struct {
vtbl *vssEnumObjectVtbl
}
func (iface *vssEnumObjectABI) Release() int32 {
result, _, _ := syscall.Syscall(iface.vtbl.Release, 1, uintptr(unsafe.Pointer(iface)), 0, 0)
return int32(result)
}
func (iface *vssEnumObjectABI) Next() ([]ObjectProperties, error) {
var props [16]ObjectProperties
var numFetched uint32
hresult, _, _ := syscall.Syscall6(iface.vtbl.Next, 4, uintptr(unsafe.Pointer(iface)),
uintptr(len(props)), uintptr(unsafe.Pointer(&props[0])), uintptr(unsafe.Pointer(&numFetched)), 0, 0)
err := errorFromHRESULT(hresult)
if err != nil {
return nil, err
}
// For some reason x/sys/windows gives HRESULT error codes a type of
// windows.Handle, which is wrong, so we're being explicit here.
if int32(hresult) == int32(windows.S_FALSE) {
err = io.EOF
}
return props[:numFetched], err
}
// The complete vtable for IVssBackupComponents.
// We only call Release, InitializeForBackup, SetContext, and Query,
// so most of these fields, while populated by Windows, are unused by us.
type vssBackupComponentsVtbl struct {
unknownVtbl
GetWriterComponentsCount uintptr
GetWriterComponents uintptr
InitializeForBackup uintptr
SetBackupState uintptr
InitializeForRestore uintptr
SetRestoreState uintptr
GatherWriterMetadata uintptr
GetWriterMetadataCount uintptr
GetWriterMetadata uintptr
FreeWriterMetadata uintptr
AddComponent uintptr
PrepareForBackup uintptr
AbortBackup uintptr
GatherWriterStatus uintptr
GetWriterStatusCount uintptr
FreeWriterStatus uintptr
GetWriterStatus uintptr
SetBackupSucceeded uintptr
SetBackupOptions uintptr
SetSelectedForRestore uintptr
SetRestoreOptions uintptr
SetAdditionalRestores uintptr
SetPreviousBackupStamp uintptr
SaveAsXML uintptr
BackupComplete uintptr
AddAlternativeLocationMapping uintptr
AddRestoreSubcomponent uintptr
SetFileRestoreStatus uintptr
AddNewTarget uintptr
SetRangesFilePath uintptr
PreRestore uintptr
PostRestore uintptr
SetContext uintptr
StartSnapshotSet uintptr
AddToSnapshotSet uintptr
DoSnapshotSet uintptr
DeleteSnapshots uintptr
ImportSnapshots uintptr
BreakSnapshotSet uintptr
GetSnapshotProperties uintptr
Query uintptr
IsVolumeSupported uintptr
DisableWriterClasses uintptr
EnableWriterClasses uintptr
DisableWriterInstances uintptr
ExposeSnapshot uintptr
RevertToSnapshot uintptr
QueryRevertStatus uintptr
}
type vssBackupComponentsABI struct {
vtbl *vssBackupComponentsVtbl
}
func (iface *vssBackupComponentsABI) Release() int32 {
result, _, _ := syscall.Syscall(iface.vtbl.Release, 1, uintptr(unsafe.Pointer(iface)), 0, 0)
return int32(result)
}
func (iface *vssBackupComponentsABI) InitializeForBackup() error {
// Note that we pass a second argument that is a null C pointer, i.e. 0
hresult, _, _ := syscall.Syscall(iface.vtbl.InitializeForBackup, 2, uintptr(unsafe.Pointer(iface)), 0, 0)
return errorFromHRESULT(hresult)
}
func (iface *vssBackupComponentsABI) SetContext(context int32) error {
hresult, _, _ := syscall.Syscall(iface.vtbl.SetContext, 2, uintptr(unsafe.Pointer(iface)), uintptr(context), 0)
return errorFromHRESULT(hresult)
}
const (
vssObjectUnknown = 0
vssObjectNone = 1
vssObjectSnapshotSet = 2
vssObjectSnapshot = 3
vssObjectProvider = 4
)
// QuerySnapshots returns the list of applicable snapshots as a SnapshotList
// (as opposed to a channel-based implementation) because we need to be able to
// access the snapshot information in reverse-chronological order.
func (iface *vssBackupComponentsABI) QuerySnapshots() (SnapshotList, error) {
// Perform the Query. If successful, the query will produce an enumeration object.
var GUID_NULL windows.GUID
var enumObj *vssEnumObjectABI
hresult, _, _ := syscall.Syscall6(iface.vtbl.Query, 5, uintptr(unsafe.Pointer(iface)), uintptr(unsafe.Pointer(&GUID_NULL)),
vssObjectNone, vssObjectSnapshot, uintptr(unsafe.Pointer(&enumObj)), 0)
err := errorFromHRESULT(hresult)
if err != nil {
return nil, err
}
defer enumObj.Release()
// Build up the complete list of snapshots from the enumerator object.
var result SnapshotList
chunk, err := enumObj.Next()
for ok := err == nil || errors.Is(err, io.EOF); ok; ok = err == nil {
if result == nil {
result = chunk
} else {
for _, item := range chunk {
result = append(result, item)
}
}
chunk, err = enumObj.Next()
}
if err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
// Sort in reverse chronological order so we may easily iterate from newest to oldest
sort.Slice(result, func(i, j int) bool {
return result[i].Obj.CreationTimestamp > result[j].Obj.CreationTimestamp
})
return result, nil
}
type vssBackupComponentsWrap struct {
iface *vssBackupComponentsABI
}
func (vss *vssBackupComponentsWrap) Close() error {
if vss == nil || vss.iface == nil {
return nil
}
vss.iface.Release()
vss.iface = nil
return nil
}
func (vss *vssBackupComponentsWrap) QuerySnapshots() (SnapshotList, error) {
if vss == nil {
return nil, fmt.Errorf("Called QuerySnapshots on a nil vssBackupComponentsWrap")
}
return vss.iface.QuerySnapshots()
}

@ -0,0 +1,210 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package winutil
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/sys/windows"
"tailscale.com/paths"
"tailscale.com/util/winutil/vss"
)
// StopWalking is the error value that a WalkSnapshotsFunc should return when
// it successfully completes and no longer needs to examine any more snapshots.
var StopWalking error = errors.New("Stop walking")
// WalkSnapshotsFunc is the type of the function called by WalkSnapshotsForLegacyStateDir
// to visit each mapped VSS snapshot.
// The path argument is the path of the directory containing the Tailscale state.
// The props argument contains the snapshot properties of the current snapshot, and
// should be treated as read-only.
// The function may return StopWalking if further walking is no longer necessary.
// Otherwise it should return nil to proceed with the walk, or an error.
type WalkSnapshotsFunc func(path string, props vss.SnapshotProperties) error
// WalkSnapshotsForLegacyStateDir enumerates available snapshots from the
// Volume Shadow Copy service. For each snapshot originating from this computer's
// C: volume, the snapshot is mounted to a temporary location inside the
// Tailscaled state directory.
// If the mounted snapshot contains a path to a legacy state directory (located under
// C:\Windows\System32\config\systemprofile\AppData\Local), the fn argument is
// invoked with the fully-qualified path to the mounted state directory, as well
// as the properties of the snapshot itself.
// A mounted snapshot that does not contain a path to a legacy state directory is
// not considered to be an error, the snapshot is ignored, and the walk continues.
// If fn returns StopWalking, then the walk is terminated but is considered to
// have been successful and nil is returned.
// If fn returns a different error, then the walk is terminated and fn's error
// is wrapped and then returned to the caller.
func WalkSnapshotsForLegacyStateDir(fn WalkSnapshotsFunc) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Ideally COM would be initialized process-wide, but until we have that
// conversation this should be okay, especially given that this function will
// only be called when a migration is necessary.
err := windows.CoInitializeEx(0, windows.COINIT_MULTITHREADED)
if err != nil {
return err
}
defer windows.CoUninitialize()
sysVol, err := getSystemVolumeName()
if err != nil {
return err
}
thisMachine, err := getFullyQualifiedComputerName()
if err != nil {
return err
}
// We'll map each snapshot to a subdir inside our tailscaled state dir
mountPt := filepath.Dir(paths.DefaultTailscaledStateFile())
vssSnapshotEnumerator, err := vss.NewSnapshotEnumerator()
if err != nil {
return err
}
defer vssSnapshotEnumerator.Close()
snapshots, err := vssSnapshotEnumerator.QuerySnapshots()
if err != nil {
return err
}
defer snapshots.Close()
for _, snap := range snapshots {
if !strings.EqualFold(snap.Obj.OriginalVolumeName.String(), sysVol) ||
!strings.EqualFold(snap.Obj.OriginatingMachine.String(), thisMachine) {
// These snapshots do not belong to our computer's C: volume, so we should skip them.
continue
}
mounted, err := mountSnapshotDevice(snap.Obj, mountPt)
if err != nil {
return fmt.Errorf("Mapping snapshot device %v: %w", snap.Obj.SnapshotDeviceObject.String(), err)
}
defer mounted.Close()
legacyStateDir, err := mounted.findLegacyStateDir()
if err != nil {
// Not all snapshots will necessarily contain the state dir, so this is not fatal
continue
}
err = fn(legacyStateDir, snap.Obj)
if errors.Is(err, StopWalking) {
return nil
}
if err != nil {
return fmt.Errorf("WalkSnapshotsFunc returned error %w", err)
}
}
return nil
}
func getSystemVolumeName() (string, error) {
// This is the exact length of a volume name, including nul terminator (per MSDN)
var volName [50]uint16
// Modern Windows always requires that the OS be installed on C:
mountPt, err := windows.UTF16PtrFromString("C:\\")
if err != nil {
return "", err
}
err = windows.GetVolumeNameForVolumeMountPoint(mountPt, &volName[0], uint32(len(volName)))
if err != nil {
return "", err
}
return windows.UTF16ToString(volName[:len(volName)-1]), nil
}
type mountedSnapshot string
func (snap *mountedSnapshot) Close() error {
os.Remove(string(*snap))
*snap = ""
return nil
}
func mountSnapshotDevice(snap vss.SnapshotProperties, mountPath string) (mountedSnapshot, error) {
fi, err := os.Stat(mountPath)
if err != nil {
return "", err
}
if !fi.IsDir() {
return "", os.ErrInvalid
}
devPath := snap.SnapshotDeviceObject.String()
linkPath := filepath.Join(mountPath, filepath.Base(devPath))
linkPathUTF16, err := windows.UTF16PtrFromString(linkPath)
if err != nil {
return "", err
}
// The target needs to end with a backslash or else the symlink won't resolve correctly
deviceUTF16, err := windows.UTF16PtrFromString(devPath + "\\")
if err != nil {
return "", err
}
err = windows.CreateSymbolicLink(linkPathUTF16, deviceUTF16, windows.SYMBOLIC_LINK_FLAG_DIRECTORY)
if err != nil {
return "", err
}
return mountedSnapshot(linkPath), nil
}
func (snap *mountedSnapshot) findLegacyStateDir() (string, error) {
legacyStateDir := filepath.Dir(paths.LegacyStateFilePath())
relPath, err := filepath.Rel("C:\\", legacyStateDir)
if err != nil {
return "", err
}
snapStateDir := filepath.Join(string(*snap), relPath)
fi, err := os.Stat(snapStateDir)
if err != nil {
return "", err
}
if !fi.IsDir() {
return "", os.ErrInvalid
}
return snapStateDir, nil
}
func getFullyQualifiedComputerName() (string, error) {
var desiredLen uint32
err := windows.GetComputerNameEx(windows.ComputerNamePhysicalDnsFullyQualified, nil, &desiredLen)
if !errors.Is(err, windows.ERROR_MORE_DATA) {
return "", err
}
buf := make([]uint16, desiredLen+1)
// Note: bufLen includes nul terminator on input, but excludes nul terminator as output
bufLen := uint32(len(buf))
err = windows.GetComputerNameEx(windows.ComputerNamePhysicalDnsFullyQualified, &buf[0], &bufLen)
if err != nil {
return "", err
}
return windows.UTF16ToString(buf[:bufLen]), nil
}
Loading…
Cancel
Save