diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 191e06a44..fae63bbc2 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -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 diff --git a/util/winutil/vss/vss_windows.go b/util/winutil/vss/vss_windows.go new file mode 100644 index 000000000..a0baf2b36 --- /dev/null +++ b/util/winutil/vss/vss_windows.go @@ -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 "" + } + + 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() +} diff --git a/util/winutil/winrestore_windows.go b/util/winutil/winrestore_windows.go new file mode 100644 index 000000000..0c2ba4042 --- /dev/null +++ b/util/winutil/winrestore_windows.go @@ -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 +}