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; } } }