mirror of
https://github.com/aphrody-labs/PowerShell.git
synced 2026-10-14 13:49:09 +00:00
807 lines
41 KiB
C#
807 lines
41 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Management.Automation.Language;
|
|
using System.Management.Automation.Subsystem;
|
|
using System.Management.Automation.Subsystem.Prediction;
|
|
using System.Threading;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace PSTests.Sequential
|
|
{
|
|
public class RemoteHyperVTests
|
|
{
|
|
private static ITestOutputHelper _output;
|
|
|
|
public RemoteHyperVTests(ITestOutputHelper output)
|
|
{
|
|
if (!System.Management.Automation.Platform.IsWindows)
|
|
{
|
|
throw new SkipException("RemoteHyperVTests are only supported on Windows.");
|
|
}
|
|
|
|
_output = output;
|
|
}
|
|
|
|
// Helper method to connect with retries
|
|
private static void ConnectWithRetry(Socket client, IPAddress address, int port, ITestOutputHelper output, int maxRetries = 10)
|
|
{
|
|
int retryDelayMs = 500;
|
|
int attempt = 0;
|
|
bool connected = false;
|
|
while (attempt < maxRetries && !connected)
|
|
{
|
|
try
|
|
{
|
|
client.Connect(address, port);
|
|
connected = true;
|
|
}
|
|
catch (SocketException)
|
|
{
|
|
attempt++;
|
|
if (attempt < maxRetries)
|
|
{
|
|
output?.WriteLine($"Connect attempt {attempt} failed, retrying in {retryDelayMs}ms...");
|
|
Thread.Sleep(retryDelayMs);
|
|
retryDelayMs *= 2;
|
|
}
|
|
else
|
|
{
|
|
output?.WriteLine($"Failed to connect after {maxRetries} attempts. This is most likely an intermittent failure due to environmental issues.");
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void SendResponse(string name, Socket client, Queue<(byte[] bytes, int delayMs)> serverResponses)
|
|
{
|
|
if (serverResponses.Count > 0)
|
|
{
|
|
_output.WriteLine($"Mock {name} ----------------------------------------------------");
|
|
var respTuple = serverResponses.Dequeue();
|
|
var resp = respTuple.bytes;
|
|
|
|
if (respTuple.delayMs > 0)
|
|
{
|
|
_output.WriteLine($"Mock {name} - delaying response by {respTuple.delayMs} ms");
|
|
Thread.Sleep(respTuple.delayMs);
|
|
}
|
|
if (resp.Length > 0) {
|
|
client.Send(resp, resp.Length, SocketFlags.None);
|
|
_output.WriteLine($"Mock {name} - sent response: " + Encoding.ASCII.GetString(resp));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void StartHandshakeServer(
|
|
string name,
|
|
int port,
|
|
IEnumerable<(string message, Encoding encoding)> expectedClientSends,
|
|
IEnumerable<(string message, Encoding encoding)> serverResponses,
|
|
bool verifyConnectionClosed,
|
|
CancellationToken cancellationToken,
|
|
bool sendFirst = false)
|
|
{
|
|
IEnumerable<(string message, Encoding encoding, int delayMs)> serverResponsesWithDelay = new List<(string message, Encoding encoding, int delayMs)>();
|
|
foreach (var item in serverResponses)
|
|
{
|
|
((List<(string message, Encoding encoding, int delayMs)>)serverResponsesWithDelay).Add((item.message, item.encoding, 1));
|
|
}
|
|
StartHandshakeServer(name, port, expectedClientSends, serverResponsesWithDelay, verifyConnectionClosed, cancellationToken, sendFirst);
|
|
}
|
|
|
|
private static void StartHandshakeServer(
|
|
string name,
|
|
int port,
|
|
IEnumerable<(string message, Encoding encoding)> expectedClientSends,
|
|
IEnumerable<(string message, Encoding encoding, int delayMs)> serverResponses,
|
|
bool verifyConnectionClosed,
|
|
CancellationToken cancellationToken,
|
|
bool sendFirst = false)
|
|
{
|
|
var expectedMessages = new Queue<(string message, byte[] bytes, Encoding encoding)>();
|
|
foreach (var item in expectedClientSends)
|
|
{
|
|
var itemBytes = item.encoding.GetBytes(item.message);
|
|
expectedMessages.Enqueue((message: item.message, bytes: itemBytes, encoding: item.encoding));
|
|
}
|
|
|
|
var serverResponseBytes = new Queue<(byte[] bytes, int delayMs)>();
|
|
foreach (var item in serverResponses)
|
|
{
|
|
(byte[] bytes, int delayMs) queueItem = (item.encoding.GetBytes(item.message), item.delayMs);
|
|
serverResponseBytes.Enqueue(queueItem);
|
|
}
|
|
|
|
_output.WriteLine($"Mock {name} - starting listener on port {port} with {expectedMessages.Count} expected messages and {serverResponseBytes.Count} responses.");
|
|
StartHandshakeServerImplementation(name, port, expectedMessages, serverResponseBytes, verifyConnectionClosed, cancellationToken, sendFirst);
|
|
}
|
|
|
|
private static void StartHandshakeServerImplementation(
|
|
string name,
|
|
int port,
|
|
Queue<(string message, byte[] bytes, Encoding encoding)> expectedClientSends,
|
|
Queue<(byte[] bytes, int delayMs)> serverResponses,
|
|
bool verifyConnectionClosed,
|
|
CancellationToken cancellationToken,
|
|
bool sendFirst = false)
|
|
{
|
|
DateTime startTime = DateTime.UtcNow;
|
|
var buffer = new byte[1024];
|
|
var listener = new TcpListener(IPAddress.Loopback, port);
|
|
listener.Start();
|
|
try
|
|
{
|
|
using (var client = listener.AcceptSocket())
|
|
{
|
|
if (sendFirst)
|
|
{
|
|
// Send the first message from the serverResponses queue
|
|
SendResponse(name, client, serverResponses);
|
|
}
|
|
|
|
while (expectedClientSends.Count > 0)
|
|
{
|
|
_output.WriteLine($"Mock {name} - time elapsed: {(DateTime.UtcNow - startTime).TotalMilliseconds} milliseconds");
|
|
client.ReceiveTimeout = 2 * 1000; // 2 seconds timeout for receiving data
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var expectedMessage = expectedClientSends.Dequeue();
|
|
_output.WriteLine($"Mock {name} - remaining expected messages: {expectedClientSends.Count}");
|
|
var expected = expectedMessage.bytes;
|
|
Array.Clear(buffer, 0, buffer.Length);
|
|
int received = client.Receive(buffer);
|
|
// Optionally validate received data matches expected
|
|
string expectedString = expectedMessage.message;
|
|
string bufferString = expectedMessage.encoding.GetString(buffer, 0, received);
|
|
string alternativeEncodedString = string.Empty;
|
|
if (expectedMessage.encoding == Encoding.Unicode)
|
|
{
|
|
alternativeEncodedString = Encoding.UTF8.GetString(buffer, 0, received);
|
|
}
|
|
else if (expectedMessage.encoding == Encoding.UTF8)
|
|
{
|
|
alternativeEncodedString = Encoding.Unicode.GetString(buffer, 0, received);
|
|
}
|
|
|
|
if (received != expected.Length)
|
|
{
|
|
string errorMessage = $"Mock {name} - Expected {expected.Length} bytes, but received {received} bytes: `{bufferString}`(alt encoding: `{alternativeEncodedString}`); expected: {expectedString}";
|
|
_output.WriteLine(errorMessage);
|
|
throw new Exception(errorMessage);
|
|
}
|
|
if (!string.Equals(bufferString, expectedString, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string errorMessage = $"Mock {name} - Expected `{expectedString}`; length {expected.Length}, but received; length {received}; `{bufferString}`(alt encoding: `{alternativeEncodedString}`) instead.";
|
|
_output.WriteLine(errorMessage);
|
|
throw new Exception(errorMessage);
|
|
}
|
|
_output.WriteLine($"Mock {name} - received expected message: " + expectedString);
|
|
SendResponse(name, client, serverResponses);
|
|
}
|
|
|
|
if (verifyConnectionClosed)
|
|
{
|
|
_output.WriteLine($"Mock {name} - verifying client connection is closed.");
|
|
// Wait for the client to close the connection synchronously (no timeout)
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
int bytesRead = client.Receive(buffer, SocketFlags.None);
|
|
if (bytesRead == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// If we receive any data, log and throw (assume UTF8 encoding)
|
|
string unexpectedData = Encoding.UTF8.GetString(buffer, 0, bytesRead);
|
|
_output.WriteLine($"Mock {name} - received unexpected data after handshake: {unexpectedData}");
|
|
throw new Exception($"Mock {name} - received unexpected data after handshake: {unexpectedData}");
|
|
}
|
|
_output.WriteLine($"Mock {name} - client closed the connection.");
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
_output.WriteLine($"Mock {name} - socket exception while waiting for client close: {ex.Message} {ex.GetType().FullName}");
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
_output.WriteLine($"Mock {name} - socket already closed.");
|
|
// Socket already closed
|
|
}
|
|
}
|
|
}
|
|
|
|
_output.WriteLine($"Mock {name} - on port {port} completed successfully.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_output.WriteLine($"Mock {name} - Exception: {ex.Message} {ex.GetType().FullName}");
|
|
_output.WriteLine(ex.StackTrace);
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
_output.WriteLine($"Mock {name} - remaining expected messages: {expectedClientSends.Count}");
|
|
_output.WriteLine($"Mock {name} - stopping listener on port {port}.");
|
|
listener.Stop();
|
|
}
|
|
}
|
|
|
|
// Helper function to create a random 4-character ASCII response
|
|
private static string CreateRandomAsciiResponse()
|
|
{
|
|
var rand = new Random();
|
|
// Randomly return either "PASS" or "FAIL"
|
|
return rand.Next(0, 2) == 0 ? "PASS" : "FAIL";
|
|
}
|
|
|
|
// Helper method to create test data
|
|
private static (List<(string, Encoding)> expectedClientSends, List<(string, Encoding)> serverResponses) CreateHandshakeTestData(NetworkCredential cred)
|
|
{
|
|
var expectedClientSends = new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: cred.Domain, encoding: Encoding.Unicode),
|
|
(message: cred.UserName, encoding: Encoding.Unicode),
|
|
(message: "NONEMPTYPW", encoding: Encoding.ASCII),
|
|
(message: cred.Password, encoding: Encoding.Unicode)
|
|
};
|
|
|
|
var serverResponses = new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: CreateRandomAsciiResponse(), encoding: Encoding.ASCII), // Response to domain
|
|
(message: CreateRandomAsciiResponse(), encoding: Encoding.ASCII), // Response to username
|
|
(message: CreateRandomAsciiResponse(), encoding: Encoding.ASCII) // Response to non-empty password
|
|
};
|
|
|
|
return (expectedClientSends, serverResponses);
|
|
}
|
|
|
|
private static List<(string message, Encoding encoding)> CreateVersionNegotiationClientSends()
|
|
{
|
|
return new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: "VERSION", encoding: Encoding.UTF8),
|
|
(message: "VERSION_2", encoding: Encoding.UTF8),
|
|
};
|
|
}
|
|
|
|
private static List<(string, Encoding)> CreateV2Sends(NetworkCredential cred, string configurationName)
|
|
{
|
|
var sends = CreateVersionNegotiationClientSends();
|
|
var password = cred.Password;
|
|
var emptyPassword = string.IsNullOrEmpty(password);
|
|
|
|
sends.AddRange(new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: cred.Domain, encoding: Encoding.Unicode),
|
|
(message: cred.UserName, encoding: Encoding.Unicode)
|
|
});
|
|
|
|
if (!emptyPassword)
|
|
{
|
|
sends.AddRange(new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: "NONEMPTYPW", encoding: Encoding.UTF8),
|
|
(message: cred.Password, encoding: Encoding.Unicode)
|
|
});
|
|
}
|
|
else
|
|
{
|
|
sends.Add((message: "EMPTYPW", encoding: Encoding.UTF8)); // Empty password and we don't expect a response
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(configurationName))
|
|
{
|
|
sends.Add((message: "NONEMPTYCF", encoding: Encoding.UTF8));
|
|
sends.Add((message: configurationName, encoding: Encoding.Unicode)); // Configuration string and we don't expect a response
|
|
}
|
|
else
|
|
{
|
|
sends.Add((message: "EMPTYCF", encoding: Encoding.UTF8)); // Configuration string and we don't expect a response
|
|
}
|
|
|
|
sends.Add((message: "PASS", encoding: Encoding.ASCII)); // Response to TOKEN
|
|
|
|
return sends;
|
|
}
|
|
|
|
private static List<(string, Encoding)> CreateV2Responses(string version = "VERSION_2", bool emptyConfig = false, string token = "FakeToken0+/=", bool emptyPassword = false)
|
|
{
|
|
var responses = new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: version, encoding: Encoding.ASCII), // Response to VERSION
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to domain
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to username
|
|
};
|
|
|
|
if (!emptyPassword)
|
|
{
|
|
responses.Add((message: "PASS", encoding: Encoding.ASCII)); // Response to non-empty password
|
|
}
|
|
|
|
responses.Add((message: "CONF", encoding: Encoding.ASCII)); // Response to configuration
|
|
|
|
if (!emptyConfig)
|
|
{
|
|
responses.Add((message: "PASS", encoding: Encoding.ASCII)); // Response to non-empty configuration
|
|
}
|
|
responses.Add((message: "TOKEN " + token, encoding: Encoding.ASCII)); // Response to with a token than uses each class of character in base 64 encoding
|
|
|
|
return responses;
|
|
}
|
|
|
|
// Helper method to create test data
|
|
private static (List<(string, Encoding)> expectedClientSends, List<(string, Encoding)> serverResponses)
|
|
CreateHandshakeTestDataV2(NetworkCredential cred, string version, string configurationName, string token)
|
|
{
|
|
bool emptyConfig = string.IsNullOrEmpty(configurationName);
|
|
bool emptyPassword = string.IsNullOrEmpty(cred.Password);
|
|
return (CreateV2Sends(cred, configurationName), CreateV2Responses(version, emptyConfig, token, emptyPassword));
|
|
}
|
|
|
|
// Helper method to create test data
|
|
private static (List<(string, Encoding)> expectedClientSends, List<(string, Encoding)> serverResponses) CreateHandshakeTestDataForFallback(NetworkCredential cred)
|
|
{
|
|
var expectedClientSends = new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: "VERSION", encoding: Encoding.UTF8),
|
|
(message: @"?<PSDirectVMLegacy>", encoding: Encoding.Unicode),
|
|
(message: "EMPTYPW", encoding: Encoding.UTF8), // Response to domain
|
|
(message: "FAIL", encoding: Encoding.UTF8), // Response to domain
|
|
};
|
|
|
|
List<(string message, Encoding encoding)> serverResponses = new List<(string message, Encoding encoding)>
|
|
{
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to VERSION but v1 server expects domain so it says "PASS"
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to username
|
|
(message: "FAIL", encoding: Encoding.ASCII) // Response to EMPTYPW
|
|
};
|
|
|
|
return (expectedClientSends, serverResponses);
|
|
}
|
|
|
|
// Helper to create a password with at least one non-ASCII Unicode character
|
|
public static string CreateRandomUnicodePassword(string prefix)
|
|
{
|
|
var rand = new Random();
|
|
var asciiPart = new char[6 + prefix.Length];
|
|
// Copy prefix into asciiPart
|
|
Array.Copy(prefix.ToCharArray(), 0, asciiPart, 0, prefix.Length);
|
|
for (int i = prefix.Length; i < asciiPart.Length; i++)
|
|
{
|
|
asciiPart[i] = (char)rand.Next(33, 127); // ASCII printable
|
|
}
|
|
// Add a random Unicode character outside ASCII range (e.g., U+0100 to U+017F)
|
|
char unicodeChar = (char)rand.Next(0x0100, 0x017F);
|
|
// Insert the unicode character at a random position
|
|
int insertPos = rand.Next(0, asciiPart.Length + 1);
|
|
var passwordChars = new List<char>(asciiPart);
|
|
passwordChars.Insert(insertPos, unicodeChar);
|
|
return new string(passwordChars.ToArray());
|
|
}
|
|
|
|
public static NetworkCredential CreateTestCredential()
|
|
{
|
|
return new NetworkCredential(CreateRandomUnicodePassword("username"), CreateRandomUnicodePassword("password"), CreateRandomUnicodePassword("domain"));
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task PerformCredentialAndConfigurationHandshake_V1_Pass()
|
|
{
|
|
// Arrange
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
var cred = CreateTestCredential();
|
|
string configurationName = CreateRandomUnicodePassword("config");
|
|
|
|
var (expectedClientSends, serverResponses) = CreateHandshakeTestData(cred);
|
|
expectedClientSends.Add(("PASS", Encoding.ASCII));
|
|
serverResponses.Add(("PASS", Encoding.ASCII));
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Broker", port, expectedClientSends, serverResponses, verifyConnectionClosed: false, cts.Token), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
ConnectWithRetry(client, IPAddress.Loopback, port, _output);
|
|
var exchangeResult = System.Management.Automation.Remoting.RemoteSessionHyperVSocketClient.ExchangeCredentialsAndConfiguration(cred, configurationName, client, true);
|
|
var result = exchangeResult.success;
|
|
_output.WriteLine($"Exchange result: {result}, Token: {exchangeResult.authenticationToken}");
|
|
System.Threading.Thread.Sleep(100); // Allow time for server to process
|
|
Assert.True(result, $"Expected Exchange to pass");
|
|
}
|
|
|
|
await serverTask;
|
|
}
|
|
|
|
[SkippableTheory]
|
|
[InlineData("VERSION_2", "configurationname1", "FakeTokenaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAA0FakeTokenaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAA0+/==")] // a fake base64 token about 512 bits long (double the size when this was spec'ed)
|
|
[InlineData("VERSION_10", null, "FakeTokenaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAA0+/=")] // a fake base64 token about 256 bits Long (the size when this was spec'ed)
|
|
public async Task PerformCredentialAndConfigurationHandshake_V2_Pass(string versionResponse, string configurationName, string token)
|
|
{
|
|
// Arrange
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
var cred = CreateTestCredential();
|
|
|
|
var (expectedClientSends, serverResponses) = CreateHandshakeTestDataV2(cred, versionResponse, configurationName, token);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Broker", port, expectedClientSends, serverResponses, verifyConnectionClosed: true, cts.Token), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
client.Connect(IPAddress.Loopback, port);
|
|
var exchangeResult = System.Management.Automation.Remoting.RemoteSessionHyperVSocketClient.ExchangeCredentialsAndConfiguration(cred, configurationName, client, false);
|
|
var result = exchangeResult.success;
|
|
System.Threading.Thread.Sleep(100); // Allow time for server to process
|
|
Assert.True(result, $"Expected Exchange to pass for version response '{versionResponse}'");
|
|
Assert.Equal(token, exchangeResult.authenticationToken);
|
|
}
|
|
|
|
await serverTask;
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task PerformCredentialAndConfigurationHandshake_V1_Fallback()
|
|
{
|
|
// Arrange
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
var cred = CreateTestCredential();
|
|
string configurationName = CreateRandomUnicodePassword("config");
|
|
|
|
var (expectedClientSends, serverResponses) = CreateHandshakeTestDataForFallback(cred);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Broker", port, expectedClientSends, serverResponses, verifyConnectionClosed: false, cts.Token), cts.Token);
|
|
|
|
bool isFallback = false;
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
_output.WriteLine("Starting handshake with V2 protocol.");
|
|
client.Connect(IPAddress.Loopback, port);
|
|
var exchangeResult = System.Management.Automation.Remoting.RemoteSessionHyperVSocketClient.ExchangeCredentialsAndConfiguration(cred, configurationName, client, false);
|
|
isFallback = !exchangeResult.success;
|
|
|
|
System.Threading.Thread.Sleep(100); // Allow time for server to process
|
|
_output.WriteLine("Handshake indicated fallback to V1.");
|
|
Assert.True(isFallback, "Expected fallback to V1.");
|
|
}
|
|
_output.WriteLine("Handshake completed successfully with fallback to V1.");
|
|
|
|
await serverTask;
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task PerformCredentialAndConfigurationHandshake_V2_InvalidResponse()
|
|
{
|
|
// Arrange
|
|
int port = 51000 + (int)(DateTime.Now.Ticks % 10000);
|
|
var cred = CreateTestCredential();
|
|
|
|
var (expectedClientSends, serverResponses) = CreateHandshakeTestData(cred);
|
|
//expectedClientSends.Add("FAI1");
|
|
serverResponses.Add(("FAI1", Encoding.ASCII));
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
|
|
|
//cts.Token.Register(() => throw new OperationCanceledException("Test timed out."));
|
|
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Broker", port, expectedClientSends, serverResponses, verifyConnectionClosed: false, cts.Token), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
_output.WriteLine("connecting on port " + port);
|
|
ConnectWithRetry(client, IPAddress.Loopback, port, _output);
|
|
|
|
var ex = Record.Exception(() => System.Management.Automation.Remoting.RemoteSessionHyperVSocketClient.ExchangeCredentialsAndConfiguration(cred, "config", client, true));
|
|
|
|
try
|
|
{
|
|
await serverTask;
|
|
}
|
|
catch (AggregateException exAgg)
|
|
{
|
|
Assert.Null(exAgg.Flatten().InnerExceptions[1].Message);
|
|
}
|
|
cts.Token.ThrowIfCancellationRequested();
|
|
|
|
Assert.NotNull(ex);
|
|
Assert.NotNull(ex.Message);
|
|
Assert.Contains("Hyper-V Broker sent an invalid Credential response", ex.Message);
|
|
}
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task PerformCredentialAndConfigurationHandshake_V1_Fail()
|
|
{
|
|
// Arrange
|
|
int port = 51000 + (int)(DateTime.Now.Ticks % 10000);
|
|
var cred = CreateTestCredential();
|
|
|
|
var (expectedClientSends, serverResponses) = CreateHandshakeTestData(cred);
|
|
expectedClientSends.Add(("FAIL", Encoding.ASCII));
|
|
serverResponses.Add(("FAIL", Encoding.ASCII));
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
|
|
|
// This scenario does not close the connection in a timely manner, so we set verifyConnectionClosed to false
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Broker", port, expectedClientSends, serverResponses, verifyConnectionClosed: false, cts.Token), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
client.Connect(IPAddress.Loopback, port);
|
|
|
|
var ex = Record.Exception(() => System.Management.Automation.Remoting.RemoteSessionHyperVSocketClient.ExchangeCredentialsAndConfiguration(cred, "config", client, true));
|
|
|
|
try
|
|
{
|
|
await serverTask;
|
|
}
|
|
catch (AggregateException exAgg)
|
|
{
|
|
Assert.Null(exAgg.Flatten().InnerExceptions[1].Message);
|
|
}
|
|
|
|
cts.Token.ThrowIfCancellationRequested();
|
|
|
|
Assert.NotNull(ex);
|
|
Assert.NotNull(ex.Message);
|
|
Assert.Contains("The credential is invalid.", ex.Message);
|
|
}
|
|
}
|
|
|
|
[SkippableTheory]
|
|
[InlineData("VERSION_2", "FakeTokenaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAA0FakeTokenaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAA0+/==")] // a fake base64 token about 512 bits long (double the size when this was spec'ed)
|
|
[InlineData("VERSION_10", "FakeTokenaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAA0+/=")] // a fake base64 token about 256 bits Long (the size when this was spec'ed)
|
|
public async Task PerformTransportVersionAndTokenExchange_Pass(string version, string token)
|
|
{
|
|
// Arrange
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
var cred = CreateTestCredential();
|
|
|
|
var expectedClientSends = CreateVersionNegotiationClientSends();
|
|
expectedClientSends.Add((message: "TOKEN " + token, encoding: Encoding.ASCII));
|
|
|
|
var serverResponses = new List<(string message, Encoding encoding)>{
|
|
(message: version, encoding: Encoding.ASCII), // Response to VERSION
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII) // Response to token
|
|
};
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Server", port, expectedClientSends, serverResponses, verifyConnectionClosed: true, cts.Token), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
ConnectWithRetry(client, IPAddress.Loopback, port, _output);
|
|
System.Management.Automation.Remoting.RemoteSessionHyperVSocketClient.PerformTransportVersionAndTokenExchange(client, token);
|
|
System.Threading.Thread.Sleep(100); // Allow time for server to process
|
|
}
|
|
|
|
await serverTask;
|
|
}
|
|
|
|
[SkippableTheory]
|
|
[InlineData(1, true)]
|
|
[InlineData(2, true)]
|
|
[InlineData(0, false)]
|
|
[InlineData(null, false)]
|
|
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
|
public void IsRequirePsDirectAuthenticationEnabled(int? regValue, bool expected)
|
|
{
|
|
const string testKeyPath = @"SOFTWARE\Microsoft\TestRequirePsDirectAuthentication";
|
|
const string valueName = "RequirePsDirectAuthentication";
|
|
if (!System.Management.Automation.Platform.IsWindows)
|
|
{
|
|
throw new SkipException("RemoteHyperVTests are only supported on Windows.");
|
|
}
|
|
|
|
// Clean up any previous test key
|
|
var regHive = Microsoft.Win32.RegistryHive.CurrentUser;
|
|
var baseKey = Microsoft.Win32.RegistryKey.OpenBaseKey(regHive, Microsoft.Win32.RegistryView.Registry64);
|
|
baseKey.DeleteSubKeyTree(testKeyPath, false);
|
|
|
|
bool? result = null;
|
|
|
|
// Create the test key
|
|
using (var key = baseKey.CreateSubKey(testKeyPath))
|
|
{
|
|
if (regValue.HasValue)
|
|
{
|
|
key.SetValue(valueName, regValue.Value, Microsoft.Win32.RegistryValueKind.DWord);
|
|
}
|
|
else
|
|
{
|
|
// Ensure the value does not exist
|
|
key.DeleteValue(valueName, false);
|
|
}
|
|
|
|
result = System.Management.Automation.Remoting.RemoteSessionHyperVSocketClient.IsRequirePsDirectAuthenticationEnabled(testKeyPath, regHive);
|
|
}
|
|
|
|
Assert.True(result.HasValue, "IsRequirePsDirectAuthenticationEnabled should return a value.");
|
|
Assert.True(expected == result.Value,
|
|
$"Expected IsRequirePsDirectAuthenticationEnabled to return {expected} when registry value is {(regValue.HasValue ? regValue.ToString() : "not set")}.");
|
|
|
|
return;
|
|
}
|
|
|
|
[SkippableTheory]
|
|
[InlineData("testToken", "testToken")]
|
|
[InlineData("testToken\0", "testToken")]
|
|
public async Task ValidatePassesWhenTokensMatch(string token, string expectedToken)
|
|
{
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
|
|
var expectedClientSends = new List<(string message, Encoding encoding)>{
|
|
(message: "VERSION", encoding: Encoding.ASCII), // Response to VERSION
|
|
(message: "VERSION_2", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: $"TOKEN {token}", encoding: Encoding.ASCII)
|
|
};
|
|
|
|
var serverResponses = new List<(string message, Encoding encoding)>{
|
|
(message: "VERSION_2", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII) // Response to token
|
|
};
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Client", port, serverResponses, expectedClientSends, verifyConnectionClosed: true, cts.Token, sendFirst: true), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
ConnectWithRetry(client, IPAddress.Loopback, port, _output);
|
|
System.Management.Automation.Remoting.RemoteSessionHyperVSocketServer.ValidateToken(client, expectedToken, DateTimeOffset.UtcNow, 1);
|
|
System.Threading.Thread.Sleep(100); // Allow time for server to process
|
|
}
|
|
|
|
await serverTask;
|
|
}
|
|
|
|
[SkippableTheory]
|
|
[InlineData(5500, "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.", "SocketException")] // test the socket timeout
|
|
[InlineData(3200, "canceled", "System.OperationCanceledException")] // test the cancellation token
|
|
[InlineData(10, "", "")]
|
|
public async Task ValidateTokenTimeoutFails(int timeoutMs, string expectedMessage, string expectedExceptionType = "SocketException")
|
|
{
|
|
string token = "testToken";
|
|
string expectedToken = token;
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
|
|
var expectedClientSends = new List<(string message, Encoding encoding, int delayMs)>{
|
|
(message: "VERSION", encoding: Encoding.ASCII, delayMs: timeoutMs), // Response to VERSION
|
|
(message: "VERSION_2", encoding: Encoding.ASCII, delayMs: timeoutMs), // Response to VERSION_2
|
|
(message: $"TOKEN {token}", encoding: Encoding.ASCII, delayMs: 1)
|
|
};
|
|
|
|
var serverResponses = new List<(string message, Encoding encoding)>{
|
|
(message: "VERSION_2", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII) // Response to token
|
|
};
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Client", port, serverResponses, expectedClientSends, verifyConnectionClosed: true, cts.Token, sendFirst: true), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
ConnectWithRetry(client, IPAddress.Loopback, port, _output);
|
|
if (expectedMessage.Length > 0)
|
|
{
|
|
var exception = Record.Exception(
|
|
() => System.Management.Automation.Remoting.RemoteSessionHyperVSocketServer.ValidateToken(client, expectedToken, DateTimeOffset.UtcNow, 5)); // set the timeout to 5 seconds or 5000 ms
|
|
Assert.NotNull(exception);
|
|
string exceptionType = exception.GetType().FullName;
|
|
_output.WriteLine($"Caught exception of type {exceptionType} with message: {exception.Message}");
|
|
Assert.Contains(expectedExceptionType, exceptionType, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains(expectedMessage, exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else
|
|
{
|
|
System.Management.Automation.Remoting.RemoteSessionHyperVSocketServer.ValidateToken(client, expectedToken, DateTimeOffset.UtcNow, 5);
|
|
}
|
|
System.Threading.Thread.Sleep(100); // Allow time for server to process
|
|
}
|
|
|
|
if (expectedMessage.Length == 0)
|
|
{
|
|
await serverTask;
|
|
}
|
|
}
|
|
|
|
[SkippableFact]
|
|
public async Task ValidateTokenTimeoutDoesAffectSession()
|
|
{
|
|
string token = "testToken";
|
|
string expectedToken = token;
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
|
|
var expectedClientSends = new List<(string message, Encoding encoding, int delayMs)>{
|
|
(message: "VERSION", encoding: Encoding.ASCII, delayMs: 1), // Response to VERSION
|
|
(message: "VERSION_2", encoding: Encoding.ASCII, delayMs: 1), // Response to VERSION_2
|
|
(message: $"TOKEN {token}", encoding: Encoding.ASCII, delayMs: 1),
|
|
(message: string.Empty, encoding: Encoding.ASCII, delayMs: 99), // Send some data after the handshake
|
|
(message: string.Empty, encoding: Encoding.ASCII, delayMs: 100), // Send some data after the handshake
|
|
(message: string.Empty, encoding: Encoding.ASCII, delayMs: 101), // Send some data after the handshake
|
|
(message: string.Empty, encoding: Encoding.ASCII, delayMs: 102), // Send some data after the handshake
|
|
(message: string.Empty, encoding: Encoding.ASCII, delayMs: 103) // Send some data after the handshake
|
|
};
|
|
|
|
var serverResponses = new List<(string message, Encoding encoding)>{
|
|
(message: "VERSION_2", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to token
|
|
(message: "PSRP-Message0", encoding: Encoding.ASCII), // Indicate server is ready to receive data
|
|
(message: "PSRP-Message1", encoding: Encoding.ASCII), // Indicate server is ready to receive data
|
|
(message: "PSRP-Message2", encoding: Encoding.ASCII), // Indicate server is ready to receive data
|
|
(message: "PSRP-Message3", encoding: Encoding.ASCII), // Indicate server is ready to receive data
|
|
(message: "PSRP-Message4", encoding: Encoding.ASCII) //
|
|
|
|
};
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Client", port, serverResponses, expectedClientSends, verifyConnectionClosed: false, cts.Token, sendFirst: true), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
ConnectWithRetry(client, IPAddress.Loopback, port, _output);
|
|
System.Management.Automation.Remoting.RemoteSessionHyperVSocketServer.ValidateToken(client, expectedToken, DateTimeOffset.UtcNow, 5);
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
System.Threading.Thread.Sleep(1500);
|
|
client.Send(Encoding.ASCII.GetBytes($"PSRP-Message{i}")); // Send some data after the handshake
|
|
}
|
|
}
|
|
|
|
await serverTask;
|
|
}
|
|
|
|
[SkippableTheory]
|
|
[InlineData("abc", "xyz")]
|
|
[InlineData("abc", "abcdef")]
|
|
[InlineData("abcdef", "abc")]
|
|
[InlineData("abc\0def", "abc")]
|
|
public async Task ValidateFailsWhenTokensMismatch(string token, string expectedToken)
|
|
{
|
|
int port = 50000 + (int)(DateTime.Now.Ticks % 10000);
|
|
|
|
var expectedClientSends = new List<(string message, Encoding encoding)>{
|
|
(message: "VERSION", encoding: Encoding.ASCII), // Initial request
|
|
(message: "VERSION_2", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: $"TOKEN {token}", encoding: Encoding.ASCII)
|
|
};
|
|
|
|
var serverResponses = new List<(string message, Encoding encoding)>{
|
|
(message: "VERSION_2", encoding: Encoding.ASCII), // Response to VERSION
|
|
(message: "PASS", encoding: Encoding.ASCII), // Response to VERSION_2
|
|
(message: "FAIL", encoding: Encoding.ASCII) // Response to token
|
|
};
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
|
var serverTask = Task.Run(() => StartHandshakeServer("Client", port, serverResponses, expectedClientSends, verifyConnectionClosed: true, cts.Token, sendFirst: true), cts.Token);
|
|
|
|
using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
|
|
{
|
|
ConnectWithRetry(client, IPAddress.Loopback, port, _output);
|
|
DateTimeOffset tokenCreationTime = DateTimeOffset.UtcNow; // Token created 10 minutes ago
|
|
var exception = Assert.Throws<System.Management.Automation.Remoting.PSDirectException>(
|
|
() => System.Management.Automation.Remoting.RemoteSessionHyperVSocketServer.ValidateToken(client, expectedToken, tokenCreationTime, 5));
|
|
System.Threading.Thread.Sleep(100); // Allow time for server to process
|
|
Assert.Contains("The credential is invalid.", exception.Message);
|
|
}
|
|
|
|
await serverTask;
|
|
}
|
|
}
|
|
}
|