sources/Google.Solutions.IapDesktop.Core/ClientModel/Transport/IapTunnel.cs (245 lines of code) (raw):

// // Copyright 2023 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Apis.Locator; using Google.Solutions.Common.Diagnostics; using Google.Solutions.Common.Runtime; using Google.Solutions.Common.Util; using Google.Solutions.Iap; using Google.Solutions.Iap.Net; using Google.Solutions.Iap.Protocol; using Google.Solutions.IapDesktop.Core.ClientModel.Protocol; using System; using System.Diagnostics; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Google.Solutions.IapDesktop.Core.ClientModel.Transport { /// <summary> /// An IAP-based tunnel that can be used to implement transports. /// </summary> public interface IIapTunnel { /// <summary> /// Traffic statistics. /// </summary> IapTunnelStatistics Statistics { get; } /// <summary> /// Local endpoints for sessions/clients to connect to. /// </summary> IPEndPoint LocalEndpoint { get; } /// <summary> /// Flags characterizing this tunnel. /// </summary> IapTunnelFlags Flags { get; } /// <summary> /// Target instance. /// </summary> InstanceLocator TargetInstance { get; } /// <summary> /// Target port. /// </summary> ushort TargetPort { get; } /// <summary> /// Policy that controls which remote peers are allowed to /// connect to the listener. /// </summary> ITransportPolicy Policy { get; } /// <summary> /// Protocol that this transport is used for. /// </summary> IProtocol Protocol { get; } } [Flags] public enum IapTunnelFlags { None, /// <summary> /// Transport is using mTLS. /// </summary> Mtls } public struct IapTunnelStatistics { public ulong BytesReceived; public ulong BytesTransmitted; } //------------------------------------------------------------------------- // Implementation. //------------------------------------------------------------------------- /// <summary> /// A sharable tunnel that uses an IAP relay listener. /// </summary> public class IapTunnel : ReferenceCountedDisposableBase, IIapTunnel { private readonly CancellationTokenSource stopListenerSource; private readonly IIapListener listener; private readonly Task listenTask; internal event EventHandler? Closed; internal Profile Details { get; } internal IapTunnel( IIapListener listener, Profile profile, IapTunnelFlags flags) { this.listener = listener.ExpectNotNull(nameof(listener)); this.stopListenerSource = new CancellationTokenSource(); this.listenTask = this.listener.ListenAsync(this.stopListenerSource.Token); this.Details = profile; this.Flags = flags; Debug.Assert( profile.LocalEndpoint == null || // Auto-assigned Equals(profile.LocalEndpoint, listener.LocalEndpoint)); } internal Task CloseAsync() { this.stopListenerSource.Cancel(); this.Closed?.Invoke(this, EventArgs.Empty); #pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks return this.listenTask; #pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks } protected override void Dispose(bool disposing) { if (disposing) { // // Last reference is gone, stop the listener. // _ = CloseAsync(); } base.Dispose(disposing); } //----------------------------------------------------------------- // IIapTunnel. //----------------------------------------------------------------- public IPEndPoint LocalEndpoint => this.listener.LocalEndpoint; public IapTunnelFlags Flags { get; } public IapTunnelStatistics Statistics { get { var stats = this.listener.Statistics; return new IapTunnelStatistics() { // // NB. If we send something, the listener receives it, // so the listener's statistics are reversed. // BytesReceived = stats.BytesTransmitted, BytesTransmitted = stats.BytesReceived }; } } public InstanceLocator TargetInstance => this.Details.TargetInstance; public ushort TargetPort => this.Details.TargetPort; public ITransportPolicy Policy => this.Details.Policy; public IProtocol Protocol => this.Details.Protocol; //----------------------------------------------------------------- // Inner classes. //----------------------------------------------------------------- /// <summary> /// Defines all the parameters that define a tunnel. If the profile /// is the same, a tunnel can be shared. /// </summary> public class Profile : IEquatable<Profile> { internal IProtocol Protocol { get; } internal ITransportPolicy Policy { get; } /// <summary> /// Instance to connect to. /// </summary> public InstanceLocator TargetInstance { get; } /// <summary> /// Port to connect to. /// </summary> public ushort TargetPort { get; } /// <summary> /// Custom local endpoint. If null, an endpoint is assigned /// automatically. /// </summary> public IPEndPoint? LocalEndpoint { get; } internal Profile( IProtocol protocol, ITransportPolicy policy, InstanceLocator targetInstance, ushort targetPort, IPEndPoint? localEndpoint = null) { this.Policy = policy.ExpectNotNull(nameof(policy)); this.Protocol = protocol.ExpectNotNull(nameof(protocol)); this.TargetInstance = targetInstance.ExpectNotNull(nameof(targetInstance)); this.TargetPort = targetPort; this.LocalEndpoint = localEndpoint; // Optional. } public override int GetHashCode() { return this.Policy.GetHashCode() ^ this.Protocol.GetHashCode() ^ this.TargetInstance.GetHashCode() ^ this.TargetPort ^ (this.LocalEndpoint?.GetHashCode() ?? 0); } public bool Equals(Profile? other) { return other != null && Equals(this.Policy, other.Policy) && Equals(this.Protocol, other.Protocol) && Equals(this.TargetInstance, other.TargetInstance) && this.TargetPort == other.TargetPort && Equals(this.LocalEndpoint, other.LocalEndpoint); } public override bool Equals(object obj) { return Equals((Profile)obj); } public static bool operator ==(Profile? obj1, Profile? obj2) { if (obj1 is null) { return obj2 is null; } return obj1.Equals(obj2); } public static bool operator !=(Profile? obj1, Profile? obj2) { return !(obj1 == obj2); } public override string ToString() { return $"{this.TargetInstance}, port: {this.TargetPort}, " + $"protocol: {this.Protocol.Name}, policy: {this.Policy.Name}"; } } /// <summary> /// Factory for tunnels. Can be derived/overridden in unit tests. /// </summary> public class Factory { private readonly IIapClient client; public Factory(IIapClient client) { this.client = client.ExpectNotNull(nameof(client)); } protected internal virtual IIapListener CreateListener( ISshRelayTarget target, ITransportPolicy policy, IPEndPoint localEndpoint) { return new IapListener( target, policy, localEndpoint); } internal virtual IapTunnel CreateTunnel( Profile profile, ISshRelayTarget target, CancellationToken cancellationToken) { using (CoreTraceSource.Log.TraceMethod().WithParameters(profile, target)) { if (profile.LocalEndpoint != null) { // // Use requested endpoint. // var listener = CreateListener( target, profile.Policy, profile.LocalEndpoint); return new IapTunnel( listener, profile, target.IsMutualTlsEnabled ? IapTunnelFlags.Mtls : IapTunnelFlags.None); } else { // // Dynamically allocate an endpoint. // // Try to use the same port number every time. For // client apps, this helps avoid polluting their // connection history and possibly to save credentials. // var portFinder = new PortFinder(); portFinder.AddSeed(Encoding.ASCII.GetBytes(profile.TargetInstance.ProjectId)); portFinder.AddSeed(Encoding.ASCII.GetBytes(profile.TargetInstance.Zone)); portFinder.AddSeed(Encoding.ASCII.GetBytes(profile.TargetInstance.Name)); portFinder.AddSeed(BitConverter.GetBytes(profile.TargetPort)); for (var attempt = 0; ; attempt++) { var localEndpoint = new IPEndPoint( IPAddress.Loopback, portFinder.FindPort(out var _)); try { var listener = CreateListener( target, profile.Policy, localEndpoint); return new IapTunnel( listener, profile, target.IsMutualTlsEnabled ? IapTunnelFlags.Mtls : IapTunnelFlags.None); } catch (PortAccessDeniedException) when (attempt + 1 < 5) { // // This port didn't work, probably because HNS (or some // other application) has a persistent port reservation // in that range. // // Amend the seed and try again. As long as the port reservation // stays the same, we'll still get deterministic results. // portFinder.AddSeed(BitConverter.GetBytes(localEndpoint.Port)); } } } } } public virtual async Task<IapTunnel> CreateTunnelAsync( Profile profile, TimeSpan probeTimeout, CancellationToken cancellationToken) { using (CoreTraceSource.Log.TraceMethod().WithParameters(profile)) { if (profile.LocalEndpoint != null && profile.LocalEndpoint.Address != IPAddress.Loopback) { throw new ArgumentException( "This implementation only supports loopback tunnels"); } var target = this.client.GetTarget( profile.TargetInstance, profile.TargetPort, IapClient.DefaultNetworkInterface); // // Check if we can actually connect to this instance before we // start a local listener. // await target .ProbeAsync(probeTimeout) .ConfigureAwait(false); return CreateTunnel(profile, target, cancellationToken); } } } } }