Skip to content

Commit

Permalink
Use HTTPS proxy in Edge Agent (#687)
Browse files Browse the repository at this point in the history
In Linux, .NET Core recognizes and honors the `https_proxy` environment variable when it is present in a module like Edge Agent or Edge Hub. The behavior is different in Windows, however, where the default WinInet proxy settings are used (as set via the Control Panel, or Internet Explorer) and `https_proxy` is ignored. This is especially a problem in RS5 nanoserver containers, which don't even expose the WinInet proxy settings.

The fix is to look for the environment variable ourselves, create a `WebProxy` object, and attach it to the `ITransportSettings` object that we pass into the SDK's `ModuleClient`. We technically only have to do this for Windows, but we'll do it for all platforms, for consistency.

This change updates Edge Agent. Edge Hub will come next.
  • Loading branch information
damonbarry authored Jan 8, 2019
1 parent d2023be commit fceef9f
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub
{
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Edge.Agent.Core;
Expand All @@ -10,17 +11,19 @@ namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub
public class EnvironmentModuleClientProvider : IModuleClientProvider
{
readonly Option<UpstreamProtocol> upstreamProtocol;
readonly Option<IWebProxy> proxy;
readonly Option<string> productInfo;

public EnvironmentModuleClientProvider(Option<UpstreamProtocol> upstreamProtocol, Option<string> productInfo)
public EnvironmentModuleClientProvider(Option<UpstreamProtocol> upstreamProtocol, Option<IWebProxy> proxy, Option<string> productInfo)
{
this.upstreamProtocol = upstreamProtocol;
this.proxy = proxy;
this.productInfo = productInfo;
}

public Task<IModuleClient> Create(
ConnectionStatusChangesHandler statusChangedHandler,
Func<IModuleClient, Task> initialize) =>
ModuleClient.Create(Option.None<string>(), this.upstreamProtocol, statusChangedHandler, initialize, this.productInfo);
ModuleClient.Create(Option.None<string>(), this.upstreamProtocol, statusChangedHandler, initialize, this.proxy, this.productInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Client.Exceptions;
using Microsoft.Azure.Devices.Client.Transport.Mqtt;
using Microsoft.Azure.Devices.Edge.Agent.Core;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Azure.Devices.Edge.Util.TransientFaultHandling;
Expand All @@ -21,14 +24,6 @@ public class ModuleClient : IModuleClient
static readonly RetryStrategy TransientRetryStrategy =
new ExponentialBackoff(int.MaxValue, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(4));

static readonly IDictionary<UpstreamProtocol, TransportType> UpstreamProtocolTransportTypeMap = new Dictionary<UpstreamProtocol, TransportType>
{
[UpstreamProtocol.Amqp] = TransportType.Amqp_Tcp_Only,
[UpstreamProtocol.AmqpWs] = TransportType.Amqp_WebSocket_Only,
[UpstreamProtocol.Mqtt] = TransportType.Mqtt_Tcp_Only,
[UpstreamProtocol.MqttWs] = TransportType.Mqtt_WebSocket_Only
};

readonly Client.ModuleClient deviceClient;

ModuleClient(Client.ModuleClient deviceClient)
Expand All @@ -41,8 +36,9 @@ public static Task<IModuleClient> Create(
Option<UpstreamProtocol> upstreamProtocol,
ConnectionStatusChangesHandler statusChangedHandler,
Func<ModuleClient, Task> initialize,
Option<IWebProxy> proxy,
Option<string> productInfo) =>
Create(upstreamProtocol, initialize, t => CreateAndOpenDeviceClient(t, connectionString, statusChangedHandler, productInfo));
Create(upstreamProtocol, initialize, u => CreateAndOpenDeviceClient(u, connectionString, statusChangedHandler, proxy, productInfo));

public Task SetDesiredPropertyUpdateCallbackAsync(DesiredPropertyUpdateCallback onDesiredPropertyChanged) =>
this.deviceClient.SetDesiredPropertyUpdateCallbackAsync(onDesiredPropertyChanged, null);
Expand All @@ -58,17 +54,17 @@ public Task SetMethodHandlerAsync(string methodName, MethodCallback callback) =>

internal static Task<Client.ModuleClient> CreateDeviceClientForUpstreamProtocol(
Option<UpstreamProtocol> upstreamProtocol,
Func<TransportType, Task<Client.ModuleClient>> deviceClientCreator)
Func<UpstreamProtocol, Task<Client.ModuleClient>> deviceClientCreator)
=> upstreamProtocol
.Map(u => deviceClientCreator(UpstreamProtocolTransportTypeMap[u]))
.Map(deviceClientCreator)
.GetOrElse(
async () =>
{
// The device SDK doesn't appear to be falling back to WebSocket from TCP,
// so we'll do it explicitly until we can get the SDK sorted out.
Try<Client.ModuleClient> result = await Fallback.ExecuteAsync(
() => deviceClientCreator(TransportType.Amqp_Tcp_Only),
() => deviceClientCreator(TransportType.Amqp_WebSocket_Only));
() => deviceClientCreator(UpstreamProtocol.Amqp),
() => deviceClientCreator(UpstreamProtocol.AmqpWs));
if (!result.Success)
{
Events.DeviceConnectionError(result.Exception);
Expand All @@ -78,7 +74,7 @@ public Task SetMethodHandlerAsync(string methodName, MethodCallback callback) =>
return result.Value;
});

static async Task<IModuleClient> Create(Option<UpstreamProtocol> upstreamProtocol, Func<ModuleClient, Task> initialize, Func<TransportType, Task<Client.ModuleClient>> deviceClientCreator)
static async Task<IModuleClient> Create(Option<UpstreamProtocol> upstreamProtocol, Func<ModuleClient, Task> initialize, Func<UpstreamProtocol, Task<Client.ModuleClient>> deviceClientCreator)
{
try
{
Expand All @@ -105,18 +101,60 @@ static async Task<IModuleClient> Create(Option<UpstreamProtocol> upstreamProtoco
}
}

static ITransportSettings GetTransportSettings(UpstreamProtocol protocol, Option<IWebProxy> proxy)
{
switch (protocol)
{
case UpstreamProtocol.Amqp:
{
var settings = new AmqpTransportSettings(TransportType.Amqp_Tcp_Only);
proxy.ForEach(p => settings.Proxy = p);
return settings;
}

case UpstreamProtocol.AmqpWs:
{
var settings = new AmqpTransportSettings(TransportType.Amqp_WebSocket_Only);
proxy.ForEach(p => settings.Proxy = p);
return settings;
}

case UpstreamProtocol.Mqtt:
{
var settings = new MqttTransportSettings(TransportType.Mqtt_Tcp_Only);
proxy.ForEach(p => settings.Proxy = p);
return settings;
}

case UpstreamProtocol.MqttWs:
{
var settings = new MqttTransportSettings(TransportType.Mqtt_WebSocket_Only);
proxy.ForEach(p => settings.Proxy = p);
return settings;
}

default:
{
throw new InvalidEnumArgumentException();
}
}
}

static async Task<Client.ModuleClient> CreateAndOpenDeviceClient(
TransportType transport,
UpstreamProtocol upstreamProtocol,
Option<string> connectionString,
ConnectionStatusChangesHandler statusChangedHandler,
Option<IWebProxy> proxy,
Option<string> productInfo)
{
Events.AttemptingConnectionWithTransport(transport);
Client.ModuleClient deviceClient = await connectionString.Map(cs => Task.FromResult(Client.ModuleClient.CreateFromConnectionString(cs, transport)))
.GetOrElse(() => Client.ModuleClient.CreateFromEnvironmentAsync(transport));
ITransportSettings settings = GetTransportSettings(upstreamProtocol, proxy);
Events.AttemptingConnectionWithTransport(settings.GetTransportType());
Client.ModuleClient deviceClient = await connectionString
.Map(cs => Task.FromResult(Client.ModuleClient.CreateFromConnectionString(cs, new[] { settings })))
.GetOrElse(() => Client.ModuleClient.CreateFromEnvironmentAsync(new[] { settings }));
productInfo.ForEach(p => deviceClient.ProductInfo = p);
await OpenAsync(statusChangedHandler, deviceClient);
Events.ConnectedWithTransport(transport);
Events.ConnectedWithTransport(settings.GetTransportType());
return deviceClient;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace Microsoft.Azure.Devices.Edge.Agent.IoTHub
{
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Edge.Agent.Core;
Expand All @@ -11,18 +12,20 @@ public class ModuleClientProvider : IModuleClientProvider
{
readonly string edgeAgentConnectionString;
readonly Option<UpstreamProtocol> upstreamProtocol;
readonly Option<IWebProxy> proxy;
readonly Option<string> productInfo;

public ModuleClientProvider(string edgeAgentConnectionString, Option<UpstreamProtocol> upstreamProtocol, Option<string> productInfo)
public ModuleClientProvider(string edgeAgentConnectionString, Option<UpstreamProtocol> upstreamProtocol, Option<IWebProxy> proxy, Option<string> productInfo)
{
this.edgeAgentConnectionString = Preconditions.CheckNonWhiteSpace(edgeAgentConnectionString, nameof(edgeAgentConnectionString));
this.upstreamProtocol = upstreamProtocol;
this.proxy = proxy;
this.productInfo = productInfo;
}

public Task<IModuleClient> Create(
ConnectionStatusChangesHandler statusChangedHandler,
Func<IModuleClient, Task> initialize) =>
ModuleClient.Create(Option.Some(this.edgeAgentConnectionString), this.upstreamProtocol, statusChangedHandler, initialize, this.productInfo);
ModuleClient.Create(Option.Some(this.edgeAgentConnectionString), this.upstreamProtocol, statusChangedHandler, initialize, this.proxy, this.productInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Service
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
Expand Down Expand Up @@ -99,13 +100,14 @@ public static async Task<int> MainAsync(IConfiguration configuration)
builder.RegisterModule(new LoggingModule(dockerLoggingDriver, dockerLoggingOptions));
Option<string> productInfo = versionInfo != VersionInfo.Empty ? Option.Some(versionInfo.ToString()) : Option.None<string>();
Option<UpstreamProtocol> upstreamProtocol = configuration.GetValue<string>(Constants.UpstreamProtocolKey).ToUpstreamProtocol();
Option<IWebProxy> proxy = Proxy.Parse(configuration.GetValue<string>("https_proxy"), logger);
switch (mode.ToLowerInvariant())
{
case Constants.DockerMode:
var dockerUri = new Uri(configuration.GetValue<string>("DockerUri"));
string deviceConnectionString = configuration.GetValue<string>("DeviceConnectionString");
builder.RegisterModule(new AgentModule(maxRestartCount, intensiveCareTime, coolOffTimeUnitInSeconds, usePersistentStorage, storagePath));
builder.RegisterModule(new DockerModule(deviceConnectionString, edgeDeviceHostName, dockerUri, dockerAuthConfig, upstreamProtocol, productInfo));
builder.RegisterModule(new DockerModule(deviceConnectionString, edgeDeviceHostName, dockerUri, dockerAuthConfig, upstreamProtocol, proxy, productInfo));
break;

case Constants.IotedgedMode:
Expand All @@ -116,7 +118,7 @@ public static async Task<int> MainAsync(IConfiguration configuration)
string moduleId = configuration.GetValue(Constants.ModuleIdVariableName, Constants.EdgeAgentModuleIdentityName);
string moduleGenerationId = configuration.GetValue<string>(Constants.EdgeletModuleGenerationIdVariableName);
builder.RegisterModule(new AgentModule(maxRestartCount, intensiveCareTime, coolOffTimeUnitInSeconds, usePersistentStorage, storagePath, Option.Some(new Uri(workloadUri)), moduleId, Option.Some(moduleGenerationId)));
builder.RegisterModule(new EdgeletModule(iothubHostname, edgeDeviceHostName, deviceId, new Uri(managementUri), new Uri(workloadUri), dockerAuthConfig, upstreamProtocol, productInfo));
builder.RegisterModule(new EdgeletModule(iothubHostname, edgeDeviceHostName, deviceId, new Uri(managementUri), new Uri(workloadUri), dockerAuthConfig, upstreamProtocol, proxy, productInfo));
break;

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Service.Modules
{
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Autofac;
using global::Docker.DotNet;
Expand All @@ -23,6 +24,7 @@ public class DockerModule : Module
readonly Uri dockerHostname;
readonly IEnumerable<AuthConfig> dockerAuthConfig;
readonly Option<UpstreamProtocol> upstreamProtocol;
readonly Option<IWebProxy> proxy;
readonly Option<string> productInfo;

public DockerModule(
Expand All @@ -31,6 +33,7 @@ public DockerModule(
Uri dockerHostname,
IEnumerable<AuthConfig> dockerAuthConfig,
Option<UpstreamProtocol> upstreamProtocol,
Option<IWebProxy> proxy,
Option<string> productInfo)
{
this.edgeDeviceConnectionString = Preconditions.CheckNonWhiteSpace(edgeDeviceConnectionString, nameof(edgeDeviceConnectionString));
Expand All @@ -41,14 +44,15 @@ public DockerModule(
this.dockerHostname = Preconditions.CheckNotNull(dockerHostname, nameof(dockerHostname));
this.dockerAuthConfig = Preconditions.CheckNotNull(dockerAuthConfig, nameof(dockerAuthConfig));
this.upstreamProtocol = Preconditions.CheckNotNull(upstreamProtocol, nameof(upstreamProtocol));
this.proxy = Preconditions.CheckNotNull(proxy, nameof(proxy));
this.productInfo = productInfo;
}

protected override void Load(ContainerBuilder builder)
{
// IDeviceClientProvider
string edgeAgentConnectionString = $"{this.edgeDeviceConnectionString};{Constants.ModuleIdKey}={Constants.EdgeAgentModuleIdentityName}";
builder.Register(c => new ModuleClientProvider(edgeAgentConnectionString, this.upstreamProtocol, this.productInfo))
builder.Register(c => new ModuleClientProvider(edgeAgentConnectionString, this.upstreamProtocol, this.proxy, this.productInfo))
.As<IModuleClientProvider>()
.SingleInstance();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Microsoft.Azure.Devices.Edge.Agent.Service.Modules
{
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Autofac;
using global::Docker.DotNet.Models;
Expand Down Expand Up @@ -30,6 +31,7 @@ public class EdgeletModule : Module
readonly Uri workloadUri;
readonly IEnumerable<AuthConfig> dockerAuthConfig;
readonly Option<UpstreamProtocol> upstreamProtocol;
readonly Option<IWebProxy> proxy;
readonly Option<string> productInfo;

public EdgeletModule(
Expand All @@ -40,6 +42,7 @@ public EdgeletModule(
Uri workloadUri,
IEnumerable<AuthConfig> dockerAuthConfig,
Option<UpstreamProtocol> upstreamProtocol,
Option<IWebProxy> proxy,
Option<string> productInfo)
{
this.iotHubHostName = Preconditions.CheckNonWhiteSpace(iotHubHostname, nameof(iotHubHostname));
Expand All @@ -49,13 +52,14 @@ public EdgeletModule(
this.workloadUri = Preconditions.CheckNotNull(workloadUri, nameof(workloadUri));
this.dockerAuthConfig = Preconditions.CheckNotNull(dockerAuthConfig, nameof(dockerAuthConfig));
this.upstreamProtocol = Preconditions.CheckNotNull(upstreamProtocol, nameof(upstreamProtocol));
this.proxy = Preconditions.CheckNotNull(proxy, nameof(proxy));
this.productInfo = productInfo;
}

protected override void Load(ContainerBuilder builder)
{
// IModuleClientProvider
builder.Register(c => new EnvironmentModuleClientProvider(this.upstreamProtocol, this.productInfo))
builder.Register(c => new EnvironmentModuleClientProvider(this.upstreamProtocol, this.proxy, this.productInfo))
.As<IModuleClientProvider>()
.SingleInstance();

Expand Down
Loading

0 comments on commit fceef9f

Please sign in to comment.