using System.Net;
using System.Net.Sockets;
namespace DodoSSH.Api.Features.Relay;
///
/// The two things the relay asks of the network: what a name resolves to, and a connection to an address.
///
///
///
/// A seam for tests, and a narrow one on purpose. is not behind it:
/// whatever this answers, the policy judges, and a test that replaces this still runs every address
/// through the real rules. What a test cannot do without the seam is dial anything, because every
/// address a test machine can reach on its own — loopback — is one the policy refuses outright.
///
///
internal interface IRelayNetwork
{
/// Resolves a stored hostname, which may be an address literal.
Task ResolveAsync(string host, CancellationToken cancellationToken);
/// Opens a TCP connection to one address.
/// A stream that owns the connection.
Task ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken);
}
/// The operating system's resolver and sockets.
internal sealed class SystemRelayNetwork : IRelayNetwork
{
///
public Task ResolveAsync(string host, CancellationToken cancellationToken) =>
// A literal parses to itself without a lookup, which is what an address typed into a host
// editor should do — and what makes "10.0.0.5" mean the same thing on every node.
IPAddress.TryParse(host, out var literal)
? Task.FromResult([literal])
: Dns.GetHostAddressesAsync(host, cancellationToken);
///
public async Task ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken)
{
var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
// An interactive shell is a stream of keystrokes; Nagle would hold each one back for the
// acknowledgement of the last, which is the latency the user feels as a sticky terminal.
NoDelay = true,
};
try
{
await socket.ConnectAsync(new IPEndPoint(address, port), cancellationToken).ConfigureAwait(false);
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
}