mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
c6ea282b3f
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>
211 lines
6.2 KiB
Go
211 lines
6.2 KiB
Go
// 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
|
|
}
|