mirror of
https://github.com/dorssel/usbipd-win.git
synced 2026-11-10 14:43:56 +08:00
382 lines
14 KiB
C#
382 lines
14 KiB
C#
// SPDX-FileCopyrightText: Microsoft Corporation
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
using System.Net;
|
|
using System.Security;
|
|
using System.Security.AccessControl;
|
|
using System.Security.Principal;
|
|
using Microsoft.Win32;
|
|
using Usbipd.Automation;
|
|
using Windows.Win32;
|
|
|
|
namespace Usbipd;
|
|
|
|
sealed class UsbipdRegistry : IDisposable
|
|
{
|
|
public static UsbipdRegistry Instance { get => TestInstance ?? field; } = new(Registry.LocalMachine);
|
|
|
|
#pragma warning disable CS0649 // Field is never assigned to. Used only by UnitTests.
|
|
internal static UsbipdRegistry? TestInstance;
|
|
#pragma warning restore CS0649
|
|
|
|
readonly RegistryKey? ReadOnlyBaseKey;
|
|
readonly RegistryKey? WritableBaseKey;
|
|
|
|
public void Dispose()
|
|
{
|
|
ReadOnlyBaseKey?.Dispose();
|
|
WritableBaseKey?.Dispose();
|
|
}
|
|
|
|
const string RegistryPath = @"SOFTWARE\usbipd-win";
|
|
|
|
public UsbipdRegistry(RegistryKey parentKey)
|
|
{
|
|
try
|
|
{
|
|
ReadOnlyBaseKey = parentKey.OpenSubKey(RegistryPath, false);
|
|
}
|
|
catch (Exception ex) when (ex is SecurityException or UnauthorizedAccessException) { }
|
|
try
|
|
{
|
|
WritableBaseKey = parentKey.OpenSubKey(RegistryPath, true);
|
|
}
|
|
catch (Exception ex) when (ex is SecurityException or UnauthorizedAccessException) { }
|
|
}
|
|
|
|
RegistryKey BaseKey(bool writable)
|
|
{
|
|
return ReadOnlyBaseKey is null
|
|
? throw new UnexpectedResultException("Registry key not found; try reinstalling the software.")
|
|
: writable ? WritableBaseKey ?? throw new SecurityException("No write access to registry key.") : ReadOnlyBaseKey;
|
|
}
|
|
|
|
const string ApplicationFolderName = "APPLICATIONFOLDER";
|
|
const string DevicesName = "Devices";
|
|
const string InstanceIdName = "InstanceId";
|
|
const string DescriptionName = "Description";
|
|
const string AttachedName = "Attached";
|
|
const string BusIdName = "BusId";
|
|
const string IPAddressName = "IPAddress";
|
|
const string PolicyName = "Policy";
|
|
const string EffectName = "Effect";
|
|
const string OperationName = "Operation";
|
|
|
|
/// <summary>
|
|
/// <see langword="null"/> if not installed
|
|
/// </summary>
|
|
public string? InstallationFolder => ReadOnlyBaseKey?.GetValue(ApplicationFolderName) as string;
|
|
|
|
RegistryKey GetDevicesKey(bool writable)
|
|
{
|
|
return BaseKey(writable).OpenSubKey(DevicesName, writable)
|
|
?? throw new UnexpectedResultException("Registry key not found; try reinstalling the software.");
|
|
}
|
|
|
|
RegistryKey? GetDeviceKey(Guid guid, bool writable)
|
|
{
|
|
using var devicesKey = GetDevicesKey(writable);
|
|
return devicesKey.OpenSubKey(guid.ToString("B"), writable);
|
|
}
|
|
|
|
RegistryKey GetPolicyKey(bool writable)
|
|
{
|
|
return BaseKey(writable).OpenSubKey(PolicyName, writable)
|
|
?? throw new UnexpectedResultException("Registry key not found; try reinstalling the software.");
|
|
}
|
|
|
|
RegistryKey? GetPolicyRuleKey(Guid guid, bool writable)
|
|
{
|
|
using var devicesKey = GetPolicyKey(writable);
|
|
return devicesKey.OpenSubKey(guid.ToString("B"), writable);
|
|
}
|
|
|
|
public void Persist(string instanceId, string description)
|
|
{
|
|
var guid = Guid.NewGuid();
|
|
using var deviceKey = GetDevicesKey(true).CreateSubKey($"{guid:B}");
|
|
deviceKey.SetValue(InstanceIdName, instanceId);
|
|
deviceKey.SetValue(DescriptionName, description);
|
|
}
|
|
|
|
public void StopSharingDevice(Guid guid)
|
|
{
|
|
using var devicesKey = GetDevicesKey(true);
|
|
devicesKey.DeleteSubKeyTree(guid.ToString("B"), false);
|
|
}
|
|
|
|
public void StopSharingAllDevices()
|
|
{
|
|
using var devicesKey = GetDevicesKey(true);
|
|
foreach (var subKeyName in devicesKey.GetSubKeyNames())
|
|
{
|
|
devicesKey.DeleteSubKeyTree(subKeyName, false);
|
|
}
|
|
}
|
|
|
|
public RegistryKey SetDeviceAsAttached(Guid guid, BusId busId, IPAddress address, string stubInstanceId)
|
|
{
|
|
using var key = GetDeviceKey(guid, true)
|
|
?? throw new UnexpectedResultException($"{nameof(SetDeviceAsAttached)}: Device key not found");
|
|
var attached = key.CreateSubKey(AttachedName, true, RegistryOptions.Volatile)
|
|
?? throw new UnexpectedResultException($"{nameof(SetDeviceAsAttached)}: Unable to create ${AttachedName} subkey");
|
|
// Allow users that are logged in on the console to delete the key (detach).
|
|
var registrySecurity = attached.GetAccessControl(AccessControlSections.All);
|
|
registrySecurity.AddAccessRule(new RegistryAccessRule(new SecurityIdentifier(WellKnownSidType.WinConsoleLogonSid, null),
|
|
RegistryRights.Delete, AccessControlType.Allow));
|
|
// Required for Windows 11 (WinConsoleLogonSid is not enough)
|
|
registrySecurity.AddAccessRule(new RegistryAccessRule(new SecurityIdentifier(WellKnownSidType.InteractiveSid, null),
|
|
RegistryRights.Delete, AccessControlType.Allow));
|
|
attached.SetAccessControl(registrySecurity);
|
|
try
|
|
{
|
|
attached.SetValue(BusIdName, busId);
|
|
attached.SetValue(IPAddressName, address.ToString());
|
|
attached.SetValue(InstanceIdName, stubInstanceId);
|
|
return attached;
|
|
}
|
|
catch
|
|
{
|
|
attached.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
static bool RemoveAttachedSubKey(RegistryKey deviceKey)
|
|
{
|
|
// .NET does not have this functionality: delete a key to which you have rights while
|
|
// you do not have rights to the containing key. So, we must use the API directly.
|
|
// Instead of checking the return value we will check if the Attached key is actually gone.
|
|
_ = PInvoke.RegDeleteKey(deviceKey.Handle, AttachedName);
|
|
using var attached = deviceKey.OpenSubKey(AttachedName, false);
|
|
return attached is null;
|
|
}
|
|
|
|
public bool SetDeviceAsDetached(Guid guid)
|
|
{
|
|
using var deviceKey = GetDeviceKey(guid, false);
|
|
return deviceKey is null || RemoveAttachedSubKey(deviceKey);
|
|
}
|
|
|
|
public bool SetAllDevicesAsDetached()
|
|
{
|
|
using var devicesKey = GetDevicesKey(false);
|
|
var deviceKeyNames = devicesKey?.GetSubKeyNames() ?? [];
|
|
var failure = false;
|
|
foreach (var deviceKeyName in deviceKeyNames)
|
|
{
|
|
using var deviceKey = devicesKey?.OpenSubKey(deviceKeyName, false);
|
|
if (deviceKey is null)
|
|
{
|
|
continue;
|
|
}
|
|
if (!RemoveAttachedSubKey(deviceKey))
|
|
{
|
|
failure = true;
|
|
}
|
|
}
|
|
return !failure;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates all bound devices.
|
|
/// <para>
|
|
/// This retrieves the entire (valid) registry state.
|
|
/// </para>
|
|
/// </summary>
|
|
public IEnumerable<Device> GetBoundDevices()
|
|
{
|
|
var guids = new SortedSet<Guid>();
|
|
using var devicesKey = GetDevicesKey(false);
|
|
foreach (var subKeyName in devicesKey.GetSubKeyNames())
|
|
{
|
|
if (Guid.TryParseExact(subKeyName, "B", out var guid))
|
|
{
|
|
// Sanitize uniqueness.
|
|
_ = guids.Add(guid);
|
|
}
|
|
}
|
|
var ignoreAttached = !Server.IsRunning();
|
|
var persistedDevices = new Dictionary<string, Device>();
|
|
foreach (var guid in guids)
|
|
{
|
|
using var deviceKey = GetDeviceKey(guid, false);
|
|
if (deviceKey is null)
|
|
{
|
|
continue;
|
|
}
|
|
if (deviceKey.GetValue(InstanceIdName) is not string instanceId)
|
|
{
|
|
// Must exist.
|
|
continue;
|
|
}
|
|
// In the past, instance IDs where not normalized to upper case. Entries may still exist that have lower case letters.
|
|
instanceId = instanceId.ToUpperInvariant();
|
|
if (persistedDevices.ContainsKey(instanceId))
|
|
{
|
|
// Sanitize uniqueness.
|
|
continue;
|
|
}
|
|
if (deviceKey.GetValue(DescriptionName) is not string description)
|
|
{
|
|
// Must exist.
|
|
continue;
|
|
}
|
|
if (WindowsDevice.TryCreate(instanceId, out var device))
|
|
{
|
|
// The device exists. Note that it may not be present (as the original device), it could be attached via a stub device.
|
|
// If the server is not running, ignore any left-over attaches as they are no longer valid.
|
|
if (!ignoreAttached)
|
|
{
|
|
using var attachedKey = deviceKey.OpenSubKey(AttachedName, false);
|
|
if (attachedKey is not null)
|
|
{
|
|
if (BusId.TryParse(attachedKey.GetValue(BusIdName) as string ?? "", out var busId)
|
|
&& IPAddress.TryParse(attachedKey.GetValue(IPAddressName) as string ?? "", out var ipAddress)
|
|
&& attachedKey.GetValue(InstanceIdName) is string stubInstanceId)
|
|
{
|
|
// In the past, instance IDs where not normalized to upper case. Entries may still exist that have lower case letters.
|
|
stubInstanceId = stubInstanceId.ToUpperInvariant();
|
|
|
|
// Everything checks out, report the device as attached.
|
|
persistedDevices.Add(instanceId, new(
|
|
instanceId: instanceId,
|
|
description: description,
|
|
isForced: device.HasVBoxDriver,
|
|
busId: busId,
|
|
persistedGuid: guid,
|
|
stubInstanceId: stubInstanceId,
|
|
clientIPAddress: ipAddress
|
|
));
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
// This device is not attached.
|
|
persistedDevices.Add(instanceId, new(
|
|
instanceId: instanceId,
|
|
description: description,
|
|
isForced: device.HasVBoxDriver,
|
|
busId: device.IsPresent ? device.BusId : null,
|
|
persistedGuid: guid,
|
|
stubInstanceId: null,
|
|
clientIPAddress: null
|
|
));
|
|
}
|
|
else
|
|
{
|
|
// This device no longer exists (uninstalled), but we still have it persisted, it could be installed again.
|
|
persistedDevices.Add(instanceId, new(
|
|
instanceId: instanceId,
|
|
description: description,
|
|
isForced: false,
|
|
busId: null,
|
|
persistedGuid: guid,
|
|
stubInstanceId: null,
|
|
clientIPAddress: null
|
|
));
|
|
}
|
|
}
|
|
return persistedDevices.Values;
|
|
}
|
|
|
|
public bool HasWriteAccess => WritableBaseKey is not null;
|
|
|
|
public Guid AddPolicyRule(PolicyRule rule)
|
|
{
|
|
if (!rule.IsValid())
|
|
{
|
|
throw new ArgumentException("Invalid policy rule", nameof(rule));
|
|
}
|
|
if (GetPolicyRules().ContainsValue(rule))
|
|
{
|
|
throw new ArgumentException("Duplicate policy rule", nameof(rule));
|
|
}
|
|
var guid = Guid.NewGuid();
|
|
using var ruleKey = GetPolicyKey(true).CreateSubKey($"{guid:B}");
|
|
ruleKey.SetValue(EffectName, rule.Effect.ToString());
|
|
ruleKey.SetValue(OperationName, rule.Operation.ToString());
|
|
rule.Save(ruleKey);
|
|
return guid;
|
|
}
|
|
|
|
public void RemovePolicyRule(Guid guid)
|
|
{
|
|
using var policyKey = GetPolicyKey(true);
|
|
policyKey.DeleteSubKeyTree(guid.ToString("B"), false);
|
|
}
|
|
|
|
public void RemovePolicyRuleAll()
|
|
{
|
|
using var policyKey = GetPolicyKey(true);
|
|
foreach (var subKeyName in policyKey.GetSubKeyNames())
|
|
{
|
|
policyKey.DeleteSubKeyTree(subKeyName, false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumerates all rules.
|
|
/// <para>
|
|
/// This retrieves the entire (valid) registry state; it ignores invalid rules, as well as any duplicates.
|
|
/// </para>
|
|
/// </summary>
|
|
public SortedDictionary<Guid, PolicyRule> GetPolicyRules()
|
|
{
|
|
var guids = new SortedSet<Guid>();
|
|
using var policyKey = GetPolicyKey(false);
|
|
foreach (var subKeyName in policyKey.GetSubKeyNames())
|
|
{
|
|
if (Guid.TryParseExact(subKeyName, "B", out var guid))
|
|
{
|
|
// Sanitize uniqueness.
|
|
_ = guids.Add(guid);
|
|
}
|
|
}
|
|
|
|
var rules = new SortedDictionary<Guid, PolicyRule>();
|
|
foreach (var guid in guids)
|
|
{
|
|
using var ruleKey = GetPolicyRuleKey(guid, false);
|
|
if (ruleKey is null)
|
|
{
|
|
continue;
|
|
}
|
|
if (!Enum.TryParse<PolicyRuleEffect>(ruleKey.GetValue(EffectName) as string, true, out var effect))
|
|
{
|
|
// Must exist and be a valid enum string.
|
|
continue;
|
|
}
|
|
if (!Enum.TryParse<PolicyRuleOperation>(ruleKey.GetValue(OperationName) as string, true, out var operation))
|
|
{
|
|
// Must exist and be a valid enum string.
|
|
continue;
|
|
}
|
|
PolicyRule rule;
|
|
switch (operation)
|
|
{
|
|
case PolicyRuleOperation.AutoBind:
|
|
rule = PolicyRuleAutoBind.Load(effect, ruleKey);
|
|
break;
|
|
default:
|
|
// Invalid, ignore.
|
|
continue;
|
|
}
|
|
if (!rule.IsValid())
|
|
{
|
|
// Invalid, ignore.
|
|
continue;
|
|
}
|
|
if (rules.ContainsValue(rule))
|
|
{
|
|
// Duplicate, ignore.
|
|
continue;
|
|
}
|
|
rules.Add(guid, rule);
|
|
}
|
|
// All unique and valid.
|
|
return rules;
|
|
}
|
|
}
|